Compare commits

..

170 Commits

Author SHA1 Message Date
Dave Richer
0595d58154 - enable sourcemap / manifest
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-31 12:24:16 -05:00
Dave Richer
e0855d370b - Package updates
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-31 12:13:20 -05:00
Dave Richer
6e8d55cad9 - updates
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-31 12:08:57 -05:00
Dave Richer
4e03756e6e Merge branch 'feature/IO-1828-Front-End-Package-Updates' into vite 2024-01-31 12:06:00 -05:00
Dave Richer
edc2a0f122 - replace require with import
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-31 11:59:25 -05:00
Dave Richer
419ae32d32 - Merge and update
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-31 11:58:17 -05:00
Patrick Fic
530032b5ea Remove Jira from CI. 2024-01-30 17:39:46 -08:00
Patrick Fic
3ca48350a6 Update old orbs on CI 2024-01-30 17:37:59 -08:00
Patrick Fic
c8979091fd CI update. 2024-01-30 17:28:45 -08:00
Patrick Fic
7358dee3e0 CI adjustment. 2024-01-30 17:17:10 -08:00
Patrick Fic
ff151444d2 Customer runner. 2024-01-30 14:44:38 -08:00
Patrick Fic
000a2e471e Use self hosted runner. 2024-01-30 14:14:06 -08:00
Patrick Fic
94bcfa2afc Increase CI allocation for beta builds. 2024-01-30 14:01:22 -08:00
Patrick Fic
bcb319d571 Update S3 sync in CI for beta builds. 2024-01-30 13:44:14 -08:00
Dave Richer
ca246fa6fa Merge remote-tracking branch 'origin/feature/Sentry-Improvements' into feature/IO-1828-Front-End-Package-Updates 2024-01-30 16:33:45 -05:00
Dave Richer
2dcfa87fa6 - Merge and update
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-30 16:33:25 -05:00
Dave Richer
5f8c878bec - fix EULA
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-30 13:29:58 -05:00
Dave Richer
fb47b45eb6 Merge remote-tracking branch 'origin/release/2024-01-29' into feature/IO-1828-Front-End-Package-Updates 2024-01-29 12:12:15 -05:00
Dave Richer
dc1c492c0b - fix keys in job lifecycle table
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 18:15:17 -05:00
Dave Richer
bdff9f62c0 Merge branch 'release/2024-01-26' into feature/IO-1828-Front-End-Package-Updates 2024-01-26 18:08:41 -05:00
Dave Richer
76b25a716c - updates from lifecyle component.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 17:46:37 -05:00
Dave Richer
86c3806e28 Merge branch 'feature/IO-1532-Tracking-Department-Cycle-Times' into feature/IO-1828-Front-End-Package-Updates 2024-01-26 16:35:25 -05:00
Dave Richer
548cfefc41 - Finish department cycle times.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 16:29:46 -05:00
Dave Richer
ae98566bbd Merge branch 'release/2024-01-26' into feature/IO-1828-Front-End-Package-Updates
# Conflicts:
#	client/src/pages/jobs-detail/jobs-detail.page.component.jsx
#	package-lock.json
#	package.json
2024-01-26 16:28:18 -05:00
Dave Richer
29d5465afc Merge branch 'release/2024-01-26' into feature/IO-1828-Front-End-Package-Updates 2024-01-26 11:49:29 -05:00
Dave Richer
2b0ecbdd91 - Fix CC Cart stuff
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-25 16:03:34 -05:00
Dave Richer
461bc726aa - progress update.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-25 15:23:42 -05:00
Dave Richer
80483b617b Merge branch 'release/2024-01-26' into feature/IO-1828-Front-End-Package-Updates
# Conflicts:
#	client/src/components/jobs-admin-change-status/jobs-admin-change.status.component.jsx
#	client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx
#	client/src/components/jobs-admin-unvoid/jobs-admin-unvoid.component.jsx
#	client/src/components/report-center-modal/report-center-modal.component.jsx
2024-01-25 14:11:33 -05:00
Dave Richer
f9528c5ff7 Merge branch 'release/2024-01-26' into feature/IO-1828-Front-End-Package-Updates 2024-01-25 13:59:18 -05:00
Dave Richer
16b0381007 - Fix empty strings passing validation.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-25 13:04:53 -05:00
Dave Richer
da5739be8f - Progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-24 17:27:49 -05:00
Dave Richer
397ae72626 - Fix issues
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-24 12:49:18 -05:00
Dave Richer
9668a01415 - Update vite build
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-24 11:50:40 -05:00
Dave Richer
6d7f49a858 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into vite
# Conflicts:
#	client/package-lock.json
#	client/package.json
2024-01-24 11:22:47 -05:00
Dave Richer
f138ab82fb - update npm packages
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-24 11:18:34 -05:00
Dave Richer
56256be106 - Reconcile vite
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-24 10:22:09 -05:00
Dave Richer
8dfe98d9d7 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into vite
# Conflicts:
#	client/package-lock.json
2024-01-24 10:15:31 -05:00
Dave Richer
9e127d1c71 Merge branch 'feature/IO-1828-Front-End-Package-Updates' of bitbucket.org:snaptsoft/bodyshop into feature/IO-1828-Front-End-Package-Updates 2024-01-24 10:07:27 -05:00
Dave Richer
cdec9840bf Merged in feature/IO-2477 (pull request #1207)
Feature/IO-2477 - EULA

Approved-by: Patrick Fic
2024-01-23 23:23:56 +00:00
Dave Richer
9deab7d5d5 - Theme provider update
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 15:30:43 -05:00
Dave Richer
e46be6c12b - Add OnlyToday
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 14:38:56 -05:00
Dave Richer
e1c325c11d Merge branch 'feature/IO-1828-Front-End-Package-Updates' into vite
# Conflicts:
#	client/src/App/App.container.jsx
2024-01-20 21:22:03 -05:00
Dave Richer
f02aa36b8d Merge branch 'feature/IO-1828-Front-End-Package-Updates' into feature/IO-2477 2024-01-20 21:20:53 -05:00
Dave Richer
d7faf11e27 - Restore the color diffs between dev and prod
- Clean up Craco config
- Updated SplitFactoryProvider

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-20 21:20:36 -05:00
Dave Richer
c72d474dc7 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into vite 2024-01-20 19:23:59 -05:00
Dave Richer
73058c4405 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into feature/IO-2477 2024-01-20 19:22:42 -05:00
Dave Richer
72e8aba546 - Merge master
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-20 19:21:31 -05:00
Dave Richer
039f1e6a91 - move lib
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-19 22:30:35 -05:00
Dave Richer
ddea46ddef - Remove test data
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-19 22:28:47 -05:00
Dave Richer
2ada4ac44b - Eula finished
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-19 22:27:03 -05:00
Dave Richer
6937f33134 - Eula Progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-19 20:55:15 -05:00
Dave Richer
06a8d4257a Merge branch 'feature/IO-1828-Front-End-Package-Updates' into feature/IO-2477 2024-01-19 12:31:46 -05:00
Dave Richer
b478d26c4c Merge branch 'feature/IO-1828-Front-End-Package-Updates' into vite 2024-01-19 12:31:06 -05:00
Dave Richer
44a13ca0d5 Merge branch 'release/2024-01-19' into feature/IO-1828-Front-End-Package-Updates
# Conflicts:
#	client/src/App/App.jsx
#	client/src/components/shop-employees/shop-employees-list.component.jsx
2024-01-19 12:08:34 -05:00
Dave Richer
0665bade1b Merge branch 'feature/IO-1828-Front-End-Package-Updates' into feature/IO-2477 2024-01-18 20:57:42 -05:00
Dave Richer
51483f62e1 - optimize schedule out today in dashboard
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 20:49:50 -05:00
Dave Richer
48a08819f3 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into vite
# Conflicts:
#	client/package-lock.json
#	client/package.json
2024-01-18 16:53:24 -05:00
Dave Richer
8dc41519ce Merge branch 'feature/IO-1828-Front-End-Package-Updates' into feature/IO-2477 2024-01-18 16:52:20 -05:00
Dave Richer
e255f0a664 - update handleBeta
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 16:50:30 -05:00
Dave Richer
7da5d1a4fd - node versions
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 16:04:53 -05:00
Dave Richer
43621bc6a2 - add a reload on beta switch
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 15:57:15 -05:00
Dave Richer
909f6e8eb5 - add a reload on beta switch
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 15:55:23 -05:00
Dave Richer
1e78106224 - add a reload on beta switch
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 15:31:46 -05:00
Dave Richer
bb872a2b18 - vite adjustments
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 15:30:23 -05:00
Dave Richer
3fb3773744 - vite adjustments
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 14:58:13 -05:00
Dave Richer
7a708e32e4 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into vite 2024-01-18 14:43:47 -05:00
Dave Richer
d1cd0ea126 Merge branch 'release/2024-01-19' into feature/IO-1828-Front-End-Package-Updates
# Conflicts:
#	client/src/App/App.jsx
#	client/src/components/header/header.component.jsx
2024-01-18 14:41:29 -05:00
Dave Richer
ee23d83858 - fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 14:34:06 -05:00
Dave Richer
61d6fdee95 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into vite 2024-01-18 13:51:20 -05:00
Patrick Fic
ae9bff7e75 Update CI for test. 2024-01-18 10:48:17 -08:00
Dave Richer
8031b4f91a - remove source maps from prod
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 13:15:02 -05:00
Dave Richer
d7cb8d3753 - update
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 13:14:28 -05:00
Dave Richer
eb3560bd34 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into vite 2024-01-18 13:11:43 -05:00
Dave Richer
2fc82aad62 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into feature/IO-2477 2024-01-18 13:07:29 -05:00
Dave Richer
4f93b5af13 - remove the word faster
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 13:05:59 -05:00
Dave Richer
71663d64e4 - Updates
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 11:43:57 -05:00
Dave Richer
67e475ce21 - Merge Release
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-17 22:26:32 -05:00
Dave Richer
86b84e75cb Merge branch 'feature/IO-1828-Front-End-Package-Updates' into feature/IO-2477 2024-01-17 20:59:09 -05:00
Dave Richer
8de4ebd30a - Merge Release
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-17 20:56:53 -05:00
Dave Richer
94dd7c6f69 - Update build resource
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-17 20:25:09 -05:00
Dave Richer
bbbb7867a2 - Generate sourcemaps
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-17 20:21:34 -05:00
Dave Richer
1cb4b228ce - fix issue
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-17 20:05:38 -05:00
Dave Richer
eb96f6467f - adjust circle ci
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-17 20:03:55 -05:00
Dave Richer
2699b80e1a - adjust circle ci
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-17 20:02:41 -05:00
Dave Richer
c72b4a25cf - progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-17 19:54:03 -05:00
Dave Richer
4a9684ba87 - progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-17 19:36:21 -05:00
Dave Richer
1a93e1de41 ### Eula V1
- Front end logic implemented

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-17 00:38:02 -05:00
Dave Richer
69791a3cdd - Fix spacing on so me alerts
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-17 00:19:51 -05:00
Dave Richer
c2d6c980ed - Fix the page layout, the footer was in the content section causing it not to remain at the bottom and just reside 'at the bottom' of the content section. Also added a 100% on the outer container height (100vh) so the grey background fills the page
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-16 15:32:35 -05:00
Dave Richer
78a136e277 - Fix the page layout, the footer was in the content section causing it not to remain at the bottom and just reside 'at the bottom' of the content section. Also added a 100% on the outer container height (100vh) so the grey background fills the page
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-16 15:17:00 -05:00
Dave Richer
690e65df0b - A little refactor cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-16 14:14:00 -05:00
Dave Richer
a38611a584 - Fix chat list item selected background (regression)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-16 14:12:54 -05:00
Dave Richer
b6b445dc21 - Update the look and feel of the chat message list (prevents breaking when zoomed in and out like previous implementation)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-16 14:02:54 -05:00
Dave Richer
0d0f24802f - Scoreboard fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-15 18:23:37 -05:00
Dave Richer
cf9b03d073 - Chat formatting
- Scroll to Top Button

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-15 17:28:24 -05:00
Dave Richer
7b61c24461 - Add hover style
- Add default theme reference
- Revert react-grid-layout to 1.3.4

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-15 15:59:00 -05:00
Dave Richer
f3db5d83c7 Merge branch 'master' into feature/IO-1828-Front-End-Package-Updates 2024-01-12 20:30:12 -05:00
Dave Richer
63ae37e5a9 - small refactor
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-12 20:29:35 -05:00
Dave Richer
76bcfce312 - Fix bug with lowercase yyyy formatting.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-12 19:29:14 -05:00
Dave Richer
34c332e077 - random cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-12 18:21:21 -05:00
Dave Richer
d360bcbb71 - random cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-12 17:40:23 -05:00
Dave Richer
ba32a71786 - nuke visible from the face of the earth with fire.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-12 17:25:25 -05:00
Dave Richer
7d6e61043e - Change Delete Scheduled jobs to a modal and not a popover
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-12 15:07:52 -05:00
Dave Richer
9fc586434e - IO-2684 (Flex issues, taken care of here and a few other places)
- Add Log Rocket on beta.imex.online
- Updated Frontend and Backend npm packages, please do a NPM install on both

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-12 13:58:05 -05:00
Dave Richer
ef0bc8c313 Fix Danger Buttons
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-12 12:11:14 -05:00
Dave Richer
aa8a719154 Job Close fixed
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-11 15:58:35 -05:00
Dave Richer
5aa3612e52 Misc Issues
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-11 15:50:13 -05:00
Dave Richer
7d16ae5194 Misc Issues
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-11 15:36:17 -05:00
Dave Richer
663dfe0441 Misc Issues
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-11 15:11:28 -05:00
Dave Richer
49131ba68b fix linting errors
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-11 12:32:51 -05:00
Dave Richer
c2dbdbd6cf Progress Update.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-11 12:24:58 -05:00
Dave Richer
0635c651a2 Progress Update.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-11 12:21:59 -05:00
Dave Richer
05667dd322 Progress Update.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-11 12:19:20 -05:00
Dave Richer
d3654ec16e Progress Update.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-11 01:29:36 -05:00
Dave Richer
ab299619dd Update Packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-10 13:32:07 -05:00
Dave Richer
e9d1f3af67 Update Packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-09 19:04:53 -05:00
Dave Richer
46b58a6e1b Fix IO-1828
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-09 17:14:30 -05:00
Dave Richer
3dcc1fe7e0 Merge branch 'master' into feature/IO-1828-Front-End-Package-Updates 2024-01-09 12:29:57 -05:00
Dave Richer
c6012f7335 additional cleanup of kanban utils.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-08 17:32:20 -05:00
Dave Richer
0c7b5087f1 Fix random undocumented bug in kanban utils.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-08 17:28:27 -05:00
Dave Richer
7589f78fe1 Fix for IO-2534
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-08 17:03:24 -05:00
Dave Richer
2e589c44a6 Fix IO-2535
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-08 15:44:04 -05:00
Dave Richer
0074c73c2a Fix IO-2537
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-08 15:16:53 -05:00
Dave Richer
213a02d5f2 Fix IO-2533
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-08 14:11:49 -05:00
Dave Richer
6e377f98a7 Fix IO-2538
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-08 12:51:44 -05:00
Dave Richer
3a3b3af13f Merge master
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-08 11:18:22 -05:00
Dave Richer
1f14688199 Update packages
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-05 15:10:45 -05:00
Dave Richer
bc50f5e983 Fix Tab Icon Spacing.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-05 13:37:54 -05:00
Dave Richer
33a1ac9be4 Fix tabs.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-04 16:34:51 -05:00
Dave Richer
2c8d1accea Additional Cleanup on Job Details Header Actions Menu
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-04 16:03:19 -05:00
Dave Richer
351d6f274b Fix / Merge / Rewrite Job Details Header Actions Menu
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-04 15:51:44 -05:00
Dave Richer
e43bfe0d3a Additional Menu Refactors
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-04 13:34:22 -05:00
Dave Richer
61ed0087e7 Additional Menu Refactors
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-04 13:31:44 -05:00
Dave Richer
17791ac971 Additional Menu Refactors
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-29 15:34:53 -05:00
Dave Richer
a0cb30f986 Additional Menu Refactors
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-29 15:18:38 -05:00
Dave Richer
07b46ed92b Majority of Dropdown Overlay Menu refactors.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-29 13:26:37 -05:00
Dave Richer
79dce5d069 Allow for Component Token Overrides.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-28 17:33:21 -05:00
Dave Richer
e5d8cc2bea Allow for Component Token Overrides.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-28 15:16:29 -05:00
Dave Richer
7b83430c02 Update both server and client deps
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-28 14:53:19 -05:00
Dave Richer
ed0da08326 Merge branch 'master' into client-update 2023-12-28 14:32:02 -05:00
Dave Richer
c8460f6092 Scoreboard and Dashboard now function as expected!
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-21 19:26:20 -05:00
Dave Richer
d2e4b7d9ec merge test (release pending tomorrow) into client-update and make required changes.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-21 13:12:24 -05:00
Dave Richer
1c5f74e4f0 Additional stable refactors, redux deprecation, substr deprecation. Clearing stage as to not lose low risk work.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-20 16:30:22 -05:00
Dave Richer
46d514ad1c Patch updates on packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-20 15:58:55 -05:00
Dave Richer
0514fbe89d even more updates.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-15 12:43:11 -05:00
Dave Richer
87391ff06a even more updates.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-15 12:26:02 -05:00
Dave Richer
b2c8e45d5e even more updates.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-14 16:27:00 -05:00
Dave Richer
1261e8001b Updates
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-14 13:27:58 -05:00
Dave Richer
83e4fb3dc4 Updates
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-14 13:20:43 -05:00
Dave Richer
a67fb3576c Issues with Scoreboard. Most things check out with dayjs besides that.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-13 19:46:37 -05:00
Dave Richer
65157a094f This is a breaking change, moment is no longer with us, let us have a dayjs of silence.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-13 19:06:15 -05:00
Dave Richer
25173b0903 This marks the Antd upgrades, it is not in a stable state.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-13 16:19:04 -05:00
Dave Richer
64f56d20dd Additional Cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-12 16:13:47 -05:00
Dave Richer
5b3c547316 Additional Cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-12 15:59:54 -05:00
Dave Richer
16d040daf9 Fix prompt, and modal.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-12 15:18:51 -05:00
Dave Richer
a22c4bdf8c Updates, bug fix, prompt refactor
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-12 14:48:37 -05:00
Dave Richer
9cb2a4a021 Update StylizedComponents package, no breaking changes.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2023-12-12 13:54:56 -05:00
Dave Richer
95c9978ee7 Fix found bugs during demo 2023-12-12 13:38:23 -05:00
Dave Richer
fe80256a40 Big progress! 2023-12-12 12:37:50 -05:00
Dave Richer
b0d1a7b65e Big progress! 2023-12-11 19:28:27 -05:00
Dave Richer
ad79344709 Progress 2023-12-11 17:34:05 -05:00
Dave Richer
5c164f807d Progress 2023-12-06 17:35:27 -05:00
Dave Richer
a043f7be24 Workbox 2023-12-06 13:21:37 -05:00
Dave Richer
8d9611333c web vitals migration 2023-12-05 15:07:20 -05:00
Dave Richer
02bb2c06eb Cypress migration 2023-12-05 15:01:52 -05:00
Dave Richer
008bcaf41b Cypress migration 2023-12-05 15:01:07 -05:00
Dave Richer
a9dbfbd231 Firebase 2023-12-05 14:56:00 -05:00
Dave Richer
1df870ee4e Some additional progress / start on updating future deprecations 2023-12-04 16:48:10 -05:00
Dave Richer
517a087186 Upgrade reselect 2023-12-04 15:08:28 -05:00
Dave Richer
51747c554e Initial Changes 2023-12-04 15:04:00 -05:00
377 changed files with 26030 additions and 49709 deletions

View File

@@ -2,9 +2,8 @@ version: 2.1
orbs:
#snyk: snyk/snyk@0.0.8
#cypress: cypress-io/cypress@1.23.0
aws-s3: circleci/aws-s3@2.0.0
eb: circleci/aws-elastic-beanstalk@1.0.2
jira: circleci/jira@1.3.1
aws-s3: circleci/aws-s3@4.0.0
eb: circleci/aws-elastic-beanstalk@2.0.1
jobs:
api-deploy:
docker:
@@ -18,7 +17,6 @@ jobs:
eb status --verbose
eb deploy
eb status
- jira/notify
hasura-migrate:
docker:
@@ -42,7 +40,7 @@ jobs:
app-build:
docker:
- image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client
steps:
@@ -58,7 +56,26 @@ jobs:
from: build
to: "s3://imex-online-production/"
arguments: "--exclude '*.map'"
- jira/notify
app-beta-build:
docker:
- image: cimg/node:18.18.2
resource_class: xlarge
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- run:
name: Install Dependencies
command: npm i
- run: npm run build
- aws-s3/sync:
from: build
to: "s3://imex-online-beta/"
arguments: "--exclude '*.map'"
test-hasura-migrate:
docker:
@@ -83,7 +100,7 @@ jobs:
test-app-build:
docker:
- image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client
steps:
@@ -99,7 +116,27 @@ jobs:
from: build
to: "s3://imex-online-test/"
arguments: "--exclude '*.map'"
- jira/notify
test-app-beta-build:
docker:
- image: cimg/node:18.18.2
resource_class: snaptsoft/pfic
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- run:
name: Install Dependencies
command: npm i
- run: npm run build:test
- aws-s3/sync:
from: build
to: "s3://imex-online-test-beta/"
arguments: "--exclude '*.map'"
admin-app-build:
docker:
@@ -142,6 +179,10 @@ workflows:
filters:
branches:
only: master
- app-beta-build:
filters:
branches:
only: master-beta
- hasura-migrate:
secret: ${HASURA_PROD_SECRET}
filters:
@@ -151,6 +192,10 @@ workflows:
filters:
branches:
only: test
- test-app-beta-build:
filters:
branches:
only: test-beta
- test-hasura-migrate:
secret: ${HASURA_TEST_SECRET}
filters:
@@ -159,4 +204,4 @@ workflows:
#- admin-app-build:
#filters:
#branches:
#only: master
#only: master

View File

@@ -1,120 +0,0 @@
# Filters and Sorters
This documentation details the schema required for `.filters` files on the report server. It is used to dynamically
modify the graphQL query and provide the user more power over their reports.
## High level Schema Overview
```javascript
const schema = {
"filters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more filters
],
"sorters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more sorters
],
"dates": {
// This is not yet implemented and will be added in a future release
}
}
```
## Filters
Filters effect the where clause of the graphQL query. They are used to filter the data returned from the server.
A note on special notation used in the `name` field.
### Path without brackets, multi level
`"name": "jobs.joblines.mod_lb_hrs",`
This will produce a where clause at the `joblines` level of the graphQL query,
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}, mod_lb_hrs: {_lt: 3}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
### Path with brackets,top level
`"name": "[jobs].joblines.mod_lb_hrs",`
This will produce a where clause at the `jobs` level of the graphQL query.
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}, joblines: {mod_lb_hrs: {_gt: 4}}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
## Known Caveats
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
- The `dates` object is not yet implemented and will be added in a future release.
- The type object must be 'string' or 'number' and is case-sensitive.
- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used.
- Do not add the ability to filter things that are already filtered as part of the original query, this would be redundant and could cause issues.
- Do not add the ability to filter on things like FK constraints, must like the above example.
## Sorters
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level.
- Most of the reports currently do sorting on a template level, this will need to change to actually see the results using the sorters.

View File

@@ -1,13 +1,13 @@
REACT_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
REACT_APP_GA_CODE=231099835
REACT_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
REACT_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
REACT_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
REACT_APP_CLOUDINARY_API_KEY=957865933348715
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
REACT_APP_AXIOS_BASE_API_URL=http://localhost:4000
REACT_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc

1
client/.npmrc Normal file
View File

@@ -0,0 +1 @@
legacy-peer-deps=true

View File

@@ -1,73 +1,68 @@
// craco.config.js
const TerserPlugin = require("terser-webpack-plugin");
const CracoLessPlugin = require("craco-less");
//const SentryWebpackPlugin = require("@sentry/webpack-plugin");
const {convertLegacyToken} = require('@ant-design/compatible/lib');
const {theme} = require('antd/lib');
const {defaultAlgorithm, defaultSeed} = theme;
const mapToken = defaultAlgorithm(defaultSeed);
const v4Token = convertLegacyToken(mapToken);
// TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely.
module.exports = {
plugins: [
// {
// plugin: SentryWebpackPlugin,
// options: {
// // sentry-cli configuration
// authToken:
// "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
// org: "snapt-software",
// project: "imexonline",
// release: process.env.REACT_APP_GIT_SHA,
// // webpack-specific configuration
// include: ".",
// ignore: ["node_modules", "webpack.config.js"],
// },
// },
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {
...(process.env.NODE_ENV === "development"
? { "@primary-color": "#a51d1d" }
: {
//"@primary-color": "#1DA57A"
}),
// "@primary-color": " #1890ff", // primary color for all components
// "@link-color": "#1890ff", // link color
// "@success-color": "#52c41a", // success state color
// "@warning-color": "#faad14", // warning state color
// "@error-color": "#f5222d", // error state color
// "@font-size-base": "14px", // major text font size
// " @heading-color": "rgba(0, 0, 0, 0.85)", // heading text color
// "@text-color": "rgba(0, 0, 0, 0.65)", // major text color
// "@text-color-secondary": "rgba(0, 0, 0, 0.45)", // secondary text color
// "@disabled-color": "rgba(0, 0, 0, 0.25)", // disable state color
// "@border-radius-base": "2px", // major border radius
// "@border-color-base": "#d9d9d9", // major border color
// "@box-shadow-base":
// "0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),0 9px 28px 8px rgba(0, 0, 0, 0.05); // major shadow for layers }",
plugins: [
// {
// plugin: SentryWebpackPlugin,
// options: {
// // sentry-cli configuration
// authToken:
// "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
// org: "snapt-software",
// project: "imexonline",
// release: process.env.REACT_APP_GIT_SHA,
//
// // webpack-specific configuration
// include: ".",
// ignore: ["node_modules", "webpack.config.js"],
// },
// },
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {...v4Token},
javascriptEnabled: true,
},
},
},
javascriptEnabled: true,
},
},
},
},
],
webpack: {
configure: (webpackConfig) => ({
...webpackConfig,
optimization: {
...webpackConfig.optimization,
// Workaround for CircleCI bug caused by the number of CPUs shown
// https://github.com/facebook/create-react-app/issues/8320
minimizer: webpackConfig.optimization.minimizer.map((item) => {
if (item instanceof TerserPlugin) {
item.options.parallel = 2;
}
],
webpack: {
configure: (webpackConfig) => {
return {
...webpackConfig,
// Required for Dev Server
devServer: {
...webpackConfig.devServer,
allowedHosts: 'all',
},
optimization: {
...webpackConfig.optimization,
// Workaround for CircleCI bug caused by the number of CPUs shown
// https://github.com/facebook/create-react-app/issues/8320
minimizer: webpackConfig.optimization.minimizer.map((item) => {
if (item instanceof TerserPlugin) {
item.options.parallel = 2;
}
return item;
}),
},
}),
},
devtool: "source-map",
return item;
}),
},
};
},
},
devtool: "source-map",
};

17
client/cypress.config.js Normal file
View File

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

View File

@@ -1,8 +0,0 @@
{
"baseUrl": "http://localhost:3000",
"experimentalStudio": true,
"env": {
"FIREBASE_USERNAME": "cypress@imex.test",
"FIREBASE_PASSWORD": "cypress"
}
}

View File

@@ -2,12 +2,12 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#002366" />
<meta name="description" content="ImEX Online" />
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
<link rel="apple-touch-icon" href="logo192.png" />
<link rel="apple-touch-icon" href="public/logo192.png" />
<script type="text/javascript">
window.$crisp = [];
window.CRISP_WEBSITE_ID = "36724f62-2eb0-4b29-9cdd-9905fb99913e";
@@ -67,7 +67,7 @@
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="manifest" href="/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
@@ -82,5 +82,7 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="src/index.js"></script>
</body>
</html>

View File

@@ -8872,13 +8872,13 @@
│ ├─ email: luis@luisrudge.net
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes/LICENSE
├─ postcss-focus-visible@4.0.0
├─ postcss-focus-open@4.0.0
│ ├─ licenses: CC0-1.0
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-visible
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-open
│ ├─ publisher: Jonathan Neal
│ ├─ email: jonathantneal@hotmail.com
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible/LICENSE.md
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open/LICENSE.md
├─ postcss-focus-within@3.0.0
│ ├─ licenses: CC0-1.0
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-within

24511
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,102 +1,111 @@
{
"name": "bodyshop",
"version": "0.2.1",
"engines": {
"node": "18.18.2"
},
"type": "commonjs",
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@apollo/client": "^3.7.9",
"@ant-design/compatible": "^5.1.2",
"@ant-design/pro-layout": "^7.17.16",
"@apollo/client": "^3.9.0",
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^7.0.0",
"@fingerprintjs/fingerprintjs": "^3.4.2",
"@craco/craco": "^7.1.0",
"@fingerprintjs/fingerprintjs": "^4.2.2",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.1.0",
"@sentry/cli": "^2.27.0",
"@sentry/react": "^7.99.0",
"@sentry/tracing": "^7.40.0",
"@splitsoftware/splitio-react": "^1.8.1",
"@tanem/react-nprogress": "^5.0.8",
"antd": "^4.24.8",
"@sentry/tracing": "^7.99.0",
"@splitsoftware/splitio-react": "^1.11.0",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-legacy": "^5.3.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitejs/plugin-react-refresh": "^1.3.6",
"@vitejs/plugin-react-swc": "^3.6.0",
"antd": "^5.13.3",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"axios": "^1.3.4",
"craco-less": "^2.0.0",
"axios": "^1.6.7",
"consola": "^3.2.3",
"dayjs": "^1.11.10",
"dayjs-business-days2": "^1.2.2",
"dinero.js": "^1.9.1",
"dotenv": "^16.0.1",
"dotenv": "^16.4.1",
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"esbuild": "^0.20.0",
"exifr": "^7.1.3",
"firebase": "^9.17.1",
"firebase": "^10.7.2",
"graphql": "^16.6.0",
"i18next": "^22.4.10",
"i18next-browser-languagedetector": "^7.0.1",
"jsoneditor": "^9.9.0",
"i18next": "^23.8.1",
"i18next-browser-languagedetector": "^7.0.2",
"jsoneditor": "^10.0.0",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.21",
"logrocket": "^3.0.1",
"markerjs2": "^2.28.1",
"moment-business-days": "^1.2.0",
"moment-timezone": "^0.5.41",
"libphonenumber-js": "^1.10.54",
"logrocket": "^7.0.0",
"markerjs2": "^2.32.0",
"normalize-url": "^8.0.0",
"phone": "^3.1.35",
"phone": "^3.1.42",
"preval.macro": "^5.0.0",
"prop-types": "^15.8.1",
"query-string": "^7.1.3",
"query-string": "^8.1.0",
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"react": "^17.0.2",
"react-big-calendar": "^1.6.8",
"react": "^18.2.0",
"react-big-calendar": "^1.8.7",
"react-color": "^2.19.3",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2",
"react-drag-listview": "^0.2.1",
"react-cookie": "^7.0.2",
"react-dom": "^18.2.0",
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.0",
"react-grid-layout": "^1.3.4",
"react-i18next": "^12.2.0",
"react-icons": "^4.7.1",
"react-grid-layout": "1.3.4",
"react-i18next": "^14.0.1",
"react-icons": "^5.0.1",
"react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.4.3",
"react-number-format": "^5.1.3",
"react-redux": "^8.0.5",
"react-resizable": "^3.0.4",
"react-router-dom": "^5.3.0",
"react-scripts": "^5.0.1",
"react-intersection-observer": "^9.5.3",
"react-markdown": "^9.0.1",
"react-number-format": "^5.1.4",
"react-redux": "^9.1.0",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.21.2",
"react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.3",
"recharts": "^2.4.3",
"redux": "^4.2.1",
"react-virtualized": "^9.22.5",
"recharts": "^2.11.0",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
"redux-saga": "^1.2.2",
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^4.1.7",
"sass": "^1.58.3",
"socket.io-client": "^4.6.1",
"styled-components": "^5.3.6",
"reselect": "^5.1.0",
"sass": "^1.70.0",
"socket.io-client": "^4.7.4",
"styled-components": "^6.1.8",
"subscriptions-transport-ws": "^0.11.0",
"web-vitals": "^2.1.4",
"workbox-background-sync": "^6.5.3",
"workbox-broadcast-update": "^6.5.3",
"workbox-cacheable-response": "^6.5.3",
"workbox-core": "^6.5.3",
"workbox-expiration": "^6.5.3",
"workbox-google-analytics": "^6.5.3",
"workbox-navigation-preload": "^6.5.3",
"workbox-precaching": "^6.5.3",
"workbox-range-requests": "^6.5.3",
"workbox-routing": "^6.5.3",
"workbox-strategies": "^6.5.3",
"workbox-streams": "^6.5.3",
"terser-webpack-plugin": "^5.3.10",
"vite-plugin-svgr": "^4.2.0",
"web-vitals": "^3.5.2",
"workbox-core": "^7.0.0",
"workbox-expiration": "^7.0.0",
"workbox-navigation-preload": "^7.0.0",
"workbox-precaching": "^7.0.0",
"workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0",
"yauzl": "^2.10.0"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "craco start",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build && npm run sentry:sourcemaps",
"start": "vite",
"build": "cross-env-shell VITE_APP_GIT_SHA=\\\"`git rev-parse --short HEAD`\\\" vite build && npm run sentry:sourcemaps",
"build:test": "env-cmd -f .env.test npm run build",
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"buildcra": "cross-env-shell VITE_APP_GIT_SHA=\\\"`git rev-parse --short HEAD`\\\" vite build",
"test": "cypress open",
"eject": "react-scripts eject",
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
"eulaize": "node src/utils/eulaize.js",
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build"
},
"eslintConfig": {
@@ -122,12 +131,29 @@
"react-error-overlay": "6.0.9"
},
"devDependencies": {
"@sentry/webpack-plugin": "^1.20.0",
"@testing-library/cypress": "^8.0.3",
"cypress": "^10.3.1",
"eslint-plugin-cypress": "^2.12.1",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.23.3",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/react": "^11.11.3",
"@sentry/webpack-plugin": "^2.10.3",
"@swc/core": "^1.3.107",
"@swc/plugin-styled-components": "^1.5.108",
"@testing-library/cypress": "^10.0.1",
"browserslist": "^4.22.3",
"browserslist-to-esbuild": "^2.1.1",
"craco-less": "^3.0.1",
"cross-env": "^7.0.3",
"cypress": "^13.6.4",
"eslint-plugin-cypress": "^2.15.1",
"memfs": "^4.6.0",
"os-browserify": "^0.3.0",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.2"
"source-map-explorer": "^2.5.3",
"vite": "^5.0.11",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.19.0",
"vite-plugin-style-import": "^2.0.0"
}
}

View File

@@ -190,7 +190,7 @@ This package contains the following license and notice below:
# @firebase/logger
This package serves as the base of all logging in the JS SDK. Any logging that
is intended to be visible to Firebase end developers should go through this
is intended to be open to Firebase end developers should go through this
module.
## Basic Usage
@@ -9375,7 +9375,7 @@ parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
to the extent that it includes a convenient and prominently open
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the

View File

@@ -1029,7 +1029,7 @@ The following NPM packages may be included in this product:
- postcss-dir-pseudo-class@5.0.0
- postcss-double-position-gradients@1.0.0
- postcss-env-function@2.0.2
- postcss-focus-visible@4.0.0
- postcss-focus-open@4.0.0
- postcss-focus-within@3.0.0
- postcss-gap-properties@2.0.0
- postcss-image-set-function@3.0.1
@@ -1699,7 +1699,7 @@ This package contains the following license and notice below:
# @firebase/logger
This package serves as the base of all logging in the JS SDK. Any logging that
is intended to be visible to Firebase end developers should go through this
is intended to be open to Firebase end developers should go through this
module.
## Basic Usage
@@ -24029,7 +24029,7 @@ parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
to the extent that it includes a convenient and prominently open
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the

View File

@@ -1,47 +1,53 @@
import { ApolloProvider } from "@apollo/client";
import { SplitFactory, SplitSdk } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd";
import {ApolloProvider} from "@apollo/client";
import {SplitFactoryProvider, SplitSdk,} from '@splitsoftware/splitio-react';
import {ConfigProvider} from "antd";
import enLocale from "antd/es/locale/en_US";
import moment from "moment";
import dayjs from "../utils/day";
import 'dayjs/locale/en';
import React from "react";
import { useTranslation } from "react-i18next";
import {useTranslation} from "react-i18next";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import client from "../utils/GraphQLClient";
import App from "./App";
import * as Sentry from "@sentry/react";
moment.locale("en-US");
import themeProvider from "./themeProvider";
dayjs.locale("en");
const config = {
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon",
},
};
export const factory = SplitSdk(config);
export const factory = SplitSdk({
core: {
authorizationKey: process.env.REACT_APP_SPLIT_API,
key: "anon",
},
});
function AppContainer() {
const { t } = useTranslation();
const {t} = useTranslation();
return (
<ApolloProvider client={client}>
<ConfigProvider
//componentSize="small"
input={{ autoComplete: "new-password" }}
locale={enLocale}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", { label: "${label}" }),
},
}}
>
<GlobalLoadingBar />
<SplitFactory factory={factory}>
<App />
</SplitFactory>
</ConfigProvider>
</ApolloProvider>
);
return (
<ApolloProvider client={client}>
<ConfigProvider
//componentSize="small"
input={{autoComplete: "new-password"}}
locale={enLocale}
theme={themeProvider}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", {label: "${label}"}),
},
}}
>
<GlobalLoadingBar/>
<SplitFactoryProvider factory={factory}>
<App/>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
);
}
export default Sentry.withProfiler(AppContainer);

View File

@@ -1,164 +1,156 @@
import { useClient } from "@splitsoftware/splitio-react";
import { Button, Result } from "antd";
import {useSplitClient} from "@splitsoftware/splitio-react";
import {Button, Result} from "antd";
import LogRocket from "logrocket";
import React, { lazy, Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Route, Switch } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import React, {lazy, Suspense, useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {Route, Routes} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
//Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page";
import TechPageContainer from "../pages/tech/tech.page.container";
import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../redux/user/user.selectors";
import PrivateRoute from "../utils/private-route";
import {setOnline} from "../redux/application/application.actions";
import {selectOnline} from "../redux/application/application.selectors";
import {checkUserSession} from "../redux/user/user.actions";
import {selectBodyshop, selectCurrentEula, selectCurrentUser,} from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute";
import "./App.styles.scss";
import handleBeta from "../utils/handleBeta";
import handleBeta from "../utils/betaHandler";
import Eula from "../components/eula/eula.component";
const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component")
import("../pages/reset-password/reset-password.component")
);
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() =>
import("../pages/mobile-payment/mobile-payment.container")
import("../pages/mobile-payment/mobile-payment.container")
);
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
currentEula: selectCurrentEula
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
});
export function App({
bodyshop,
checkUserSession,
currentUser,
online,
setOnline,
}) {
const client = useClient();
export function App({bodyshop, checkUserSession, currentUser, online, setOnline, currentEula}) {
const client = useSplitClient().client;
const [listenersAdded, setListenersAdded] = useState(false)
const {t} = useTranslation();
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
}
checkUserSession();
}, [checkUserSession, setOnline]);
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
const { t } = useTranslation();
window.addEventListener("offline", function (e) {
setOnline(false);
});
window.addEventListener("online", function (e) {
setOnline(true);
});
useEffect(() => {
if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid);
if (client.getTreatment("LogRocket_Tracking") === "on") {
console.log("LR Start");
LogRocket.init("gvfvfw/bodyshopapp");
}
}
}, [bodyshop, client, currentUser.authorized]);
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />;
}
if (!online)
return (
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
}
/>
checkUserSession();
}, [checkUserSession, setOnline]);
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
// Associate event listeners, memoize to prevent multiple listeners being added
useEffect(() => {
const offlineListener = (e) => {
setOnline(false);
}
const onlineListener = (e) => {
setOnline(true);
}
if (!listenersAdded) {
console.log('Added events for offline and online');
window.addEventListener("offline", offlineListener);
window.addEventListener("online", onlineListener);
setListenersAdded(true);
}
return () => {
window.removeEventListener("offline", offlineListener);
window.removeEventListener("online", onlineListener);
}
}, [setOnline, listenersAdded]);
useEffect(() => {
if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid);
if (
client.getTreatment("LogRocket_Tracking") === "on" ||
window.location.hostname === 'beta.imex.online'
) {
console.log("LR Start");
LogRocket.init("gvfvfw/bodyshopapp");
}
}
}, [bodyshop, client, currentUser.authorized]);
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")}/>;
}
handleBeta();
if (!online)
return (
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
}
/>
);
if (currentEula && !currentUser.eulaIsAccepted) {
return <Eula/>
}
// Any route that is not assigned and matched will default to the Landing Page component
return (
<Suspense fallback={<LoadingSpinner message="ImEX Online"/>}>
<Routes>
<Route path="*" element={<ErrorBoundary><LandingPage/></ErrorBoundary>}/>
<Route path="/signin" element={<ErrorBoundary><SignInPage/></ErrorBoundary>}/>
<Route path="/resetpassword" element={<ErrorBoundary><ResetPassword/></ErrorBoundary>}/>
<Route path="/csi/:surveyId" element={<ErrorBoundary><CsiPage/></ErrorBoundary>}/>
<Route path="/disclaimer" element={<ErrorBoundary><DisclaimerPage/></ErrorBoundary>}/>
<Route path="/mp/:paymentIs" element={<ErrorBoundary><MobilePaymentContainer/></ErrorBoundary>}/>
<Route path="/manage/*"
element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route path="*" element={<ManagePage/>}/>
</Route>
<Route path="/tech/*"
element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route path="*" element={<TechPageContainer/>}/>
</Route>
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized}/>}>
<Route path="*" element={<DocumentEditorContainer/>}/>
</Route>
</Routes>
</Suspense>
);
handleBeta();
return (
<Switch>
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}>
<ErrorBoundary>
<Route exact path="/" component={LandingPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/signin" component={SignInPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/resetpassword" component={ResetPassword} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/csi/:surveyId" component={CsiPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/disclaimer" component={DisclaimerPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route
exact
path="/mp/:paymentIs"
component={MobilePaymentContainer}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/manage"
component={ManagePage}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/tech"
component={TechPageContainer}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/edit"
component={DocumentEditorContainer}
/>
</ErrorBoundary>
</Suspense>
</Switch>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);

View File

@@ -1,6 +1,10 @@
//Global Styles.
@import "react-big-calendar/lib/sass/styles";
.ant-menu-item-divider {
border-bottom: 1px solid #74695c !important;
}
.imex-table-header {
display: flex;
flex-wrap: wrap;
@@ -143,23 +147,11 @@
}
}
//Update row highlighting on production board.
.ant-table-tbody > tr.ant-table-row:hover > td {
background: #e7f3ff !important;
}
.ant-table-tbody > tr.ant-table-row-selected > td {
background: #e6f7ff !important;
}
.job-line-manual {
color: tomato;
font-style: italic;
}
td.ant-table-column-sort {
background-color: transparent;
}
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
background-color: #f4f4f4;

View File

@@ -0,0 +1,60 @@
import {defaultsDeep} from "lodash";
import {theme} from "antd";
const {defaultAlgorithm, darkAlgorithm} = theme;
let isDarkMode = false;
/**
* Default theme
* @type {{components: {Menu: {itemDividerBorderColor: string}}}}
*/
const defaultTheme = {
components: {
Table: {
rowHoverBg: '#e7f3ff',
rowSelectedBg: '#e6f7ff',
headerSortHoverBg: 'transparent',
},
Menu: {
darkItemHoverBg: '#1677ff',
itemHoverBg: '#1677ff',
horizontalItemHoverBg: '#1677ff',
}
},
token: {
colorPrimary: '#1677ff'
}
};
/**
* Development theme
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
*/
const devTheme = {
components: {
Menu: {
darkItemHoverBg: '#a51d1d',
itemHoverBg: '#a51d1d',
horizontalItemHoverBg: '#a51d1d',
}
},
token: {
colorPrimary: '#a51d1d'
}
};
/**
* Production theme
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
*/
const prodTheme = {};
const currentTheme = import.meta.env.DEV ? devTheme
: prodTheme;
const finaltheme = {
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
...defaultsDeep(currentTheme, defaultTheme)
}
export default finaltheme;

View File

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

View File

@@ -2,5 +2,5 @@ import { Alert } from "antd";
import React from "react";
export default function AlertComponent(props) {
return <Alert {...props} />;
return <Alert {...props} />;
}

View File

@@ -61,7 +61,7 @@ export function AllocationsAssignmentComponent({
);
return (
<Popover content={popContent} visible={visibility}>
<Popover content={popContent} open={visibility}>
<Button onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")}
</Button>

View File

@@ -59,7 +59,7 @@ export default connect(
);
return (
<Popover content={popContent} visible={visibility}>
<Popover content={popContent} open={visibility}>
<Button disabled={disabled} onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")}
</Button>

View File

@@ -1,6 +1,6 @@
import { useMutation, useQuery } from "@apollo/client";
import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
import moment from "moment";
import {useMutation, useQuery} from "@apollo/client";
import {Button, Form, Popconfirm, Space} from "antd";
import dayjs from "../../utils/day";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect";
import {
DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE,
UPDATE_BILL_LINE
} from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
@@ -26,143 +26,142 @@ import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-galler
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillDetailEditReturn from "./bill-detail-edit-return.component";
import {PageHeader} from "@ant-design/pro-layout";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
});
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
}) {
const search = queryString.parse(useLocation().search);
export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) {
const search = queryString.parse(useLocation().search);
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const {t} = useTranslation();
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
variables: { billid: search.billid },
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const handleSave = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setVisible(true);
else {
form.submit();
}
};
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
const { billlines, upload, ...bill } = values;
const updates = [];
updates.push(
update_bill({
variables: { billId: search.billid, bill: bill },
})
);
billlines.forEach((l) => {
delete l.selected;
const {loading, error, data, refetch} = useQuery(QUERY_BILL_BY_PK, {
variables: {billid: search.billid},
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
//Find bill lines that were deleted.
const deletedJobLines = [];
// ... rest of the code remains the same
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
const handleSave = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setOpen(true);
else {
form.submit();
}
};
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({ variables: { id: d.id } }));
});
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
billlines.forEach((billline) => {
const { deductedfromlbr, inventories, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {
const {billlines, upload, ...bill} = values;
const updates = [];
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
update_bill({
variables: {billId: search.billid, bill: bill},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
await Promise.all(updates);
billlines.forEach((l) => {
delete l.selected;
});
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
//Find bill lines that were deleted.
const deletedJobLines = [];
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setVisible(false);
setUpdateLoading(false);
};
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
if (error) return <AlertComponent message={error.message} type="error" />;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({variables: {id: d.id}}));
});
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
billlines.forEach((billline) => {
const {deductedfromlbr, inventories, jobline, ...il} = billline;
delete il.__typename;
if (il.id) {
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
await Promise.all(updates);
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setOpen(false);
setUpdateLoading(false);
};
if (error) return <AlertComponent message={error.message} type="error"/>;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
return (
<>
@@ -179,9 +178,9 @@ export function BillDetailEditcontainer({
<BillDetailEditReturn data={data} />
<BillPrintButton billid={search.billid} />
<Popconfirm
visible={visible}
open={open}
onConfirm={() => form.submit()}
onCancel={() => setVisible(false)}
onCancel={() => setOpen(false)}
okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")}
>
@@ -208,45 +207,45 @@ export function BillDetailEditcontainer({
>
<BillFormContainer form={form} billEdit disabled={exported} />
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{ id: data ? data.bills_by_pk.jobid : null }}
invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null}
/>
) : (
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{id: data ? data.bills_by_pk.jobid : null}}
invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null}
/>
) : (
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
)}
</Form>
</>
)}
</Form>
</>
)}
</>
);
);
}
const transformData = (data) => {
return data
? {
...data.bills_by_pk,
return data
? {
...data.bills_by_pk,
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
};
}),
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
}
: {};
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
};
}),
date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null,
}
: {};
};

View File

@@ -3,7 +3,7 @@ import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
@@ -33,10 +33,10 @@ export function BillDetailEditReturn({
disabled,
}) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const history = useNavigate();
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [open, setOpen] = useState(false);
const handleFinish = ({ billlines }) => {
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
@@ -67,18 +67,18 @@ export function BillDetailEditReturn({
});
delete search.billid;
history.push({ search: queryString.stringify(search) });
setVisible(false);
history({ search: queryString.stringify(search) });
setOpen(false);
};
useEffect(() => {
if (visible === false) form.resetFields();
}, [visible, form]);
if (open === false) form.resetFields();
}, [open, form]);
return (
<>
<Modal
visible={visible}
onCancel={() => setVisible(false)}
open={open}
onCancel={() => setOpen(false)}
destroyOnClose
title={t("bills.actions.return")}
onOk={() => form.submit()}
@@ -175,7 +175,7 @@ export function BillDetailEditReturn({
<Button
disabled={data.bills_by_pk.is_credit_memo || disabled}
onClick={() => {
setVisible(true);
setOpen(true);
}}
>
{t("bills.actions.return")}

View File

@@ -1,12 +1,12 @@
import { Drawer, Grid } from "antd";
import queryString from "query-string";
import React from "react";
import { useHistory, useLocation } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import BillDetailEditComponent from "./bill-detail-edit-component";
export default function BillDetailEditcontainer() {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const history = useNavigate();
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
@@ -29,10 +29,10 @@ export default function BillDetailEditcontainer() {
width={drawerPercentage}
onClose={() => {
delete search.billid;
history.push({ search: queryString.stringify(search) });
history({ search: queryString.stringify(search) });
}}
destroyOnClose
visible={search.billid}
open={search.billid}
>
<BillDetailEditComponent />
</Drawer>

View File

@@ -94,7 +94,6 @@ function BillEnterModalContainer({
location,
outstanding_returns,
inventory,
federal_tax_exempt,
...remainingValues
} = values;
@@ -347,18 +346,18 @@ function BillEnterModalContainer({
}, [enterAgain, form]);
useEffect(() => {
if (billEnterModal.visible) {
if (billEnterModal.open) {
form.setFieldsValue(formValues);
} else {
form.resetFields();
}
}, [billEnterModal.visible, form, formValues]);
}, [billEnterModal.open, form, formValues]);
return (
<Modal
title={t("bills.labels.new")}
width={"98%"}
visible={billEnterModal.visible}
open={billEnterModal.open}
okText={t("general.actions.save")}
keyboard="false"
onOk={() => form.submit()}

View File

@@ -1,26 +1,16 @@
import Icon, { UploadOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import {
Alert,
Divider,
Form,
Input,
Select,
Space,
Statistic,
Switch,
Upload,
} from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { MdOpenInNew } from "react-icons/md";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import Icon, {UploadOutlined} from "@ant-design/icons";
import {useApolloClient} from "@apollo/client";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import {Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload,} from "antd";
import dayjs from "../../utils/day";
import React, {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {MdOpenInNew} from "react-icons/md";
import {connect} from "react-redux";
import {Link} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import {CHECK_BILL_INVOICE_NUMBER} from "../../graphql/bills.queries";
import {selectBodyshop} from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
@@ -30,54 +20,38 @@ import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility";
import {CalculateBillTotal} from "./bill-form.totals.utility";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({});
export function BillFormComponent({
bodyshop,
disabled,
form,
vendorAutoCompleteOptions,
lineData,
responsibilityCenters,
loadLines,
billEdit,
disableInvNumber,
job,
loadOutstandingReturns,
loadInventory,
preferredMake,
}) {
const { t } = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
const { Extended_Bill_Posting } = useTreatments(
["Extended_Bill_Posting"],
{},
bodyshop.imexshopid
);
const { ClosingPeriod } = useTreatments(
["ClosingPeriod"],
{},
bodyshop.imexshopid
);
export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteOptions, lineData, responsibilityCenters, loadLines, billEdit, disableInvNumber, job, loadOutstandingReturns, loadInventory, preferredMake}) {
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
const {t} = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
});
};
const { treatments: {Extended_Bill_Posting, ClosingPeriod} } = useSplitTreatments({
attributes: {},
names: ["Extended_Bill_Posting", "ClosingPeriod"],
splitKey: bodyshop.imexshopid,
});
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
});
};
const handleFederalTaxExemptSwitchToggle = (checked) => {
// Early gate
@@ -97,256 +71,256 @@ export function BillFormComponent({
if (job) form.validateFields(["is_credit_memo"]);
}, [job, form]);
useEffect(() => {
const vendorId = form.getFieldValue("vendorid");
if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter(
(v) => v.id === vendorId
);
if (matchingVendors.length === 1) {
setDiscount(matchingVendors[0].discount);
}
}
const jobId = form.getFieldValue("jobid");
if (jobId) {
loadLines({ variables: { id: jobId } });
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({
variables: {
jobId: jobId,
vendorId: vendorId,
},
});
}
}
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
return (
<div>
<FormFieldsChanged form={form} />
<Form.Item
style={{ display: "none" }}
name="isinhouse"
valuePropName="checked"
>
<Switch />
</Form.Item>
<LayoutFormRow grow>
<Form.Item
name="jobid"
label={t("bills.fields.ro_number")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelect
disabled={billEdit || disabled}
convertedOnly
notExported={false}
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
if (form.getFieldValue("vendorid") !== null) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid"),
},
});
}
}
}}
/>
</Form.Item>
<Form.Item
label={t("bills.fields.vendor")}
name="vendorid"
// style={{ display: billEdit ? "none" : null }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
if (
value &&
!getFieldValue(["isinhouse"]) &&
value === bodyshop.inhousevendorid
) {
return Promise.reject(t("bills.validation.manualinhouse"));
}
return Promise.resolve();
},
}),
]}
>
<VendorSearchSelect
disabled={disabled}
options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect}
/>
</Form.Item>
</LayoutFormRow>
{job &&
job.ious &&
job.ious.length > 0 &&
job.ious.map((iou) => (
<Alert
key={iou.id}
type="warning"
message={
<Space>
{t("bills.labels.iouexists")}
<Link
target="_blank"
rel="noopener noreferrer"
to={`/manage/jobs/${iou.id}?tab=repairdata`}
>
<Space>
{iou.ro_number}
<Icon component={MdOpenInNew} />
</Space>
</Link>
</Space>
useEffect(() => {
const vendorId = form.getFieldValue("vendorid");
if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter(
(v) => v.id === vendorId
);
if (matchingVendors.length === 1) {
setDiscount(matchingVendors[0].discount);
}
/>
))}
<LayoutFormRow>
<Form.Item
label={t("bills.fields.invoice_number")}
name="invoice_number"
validateTrigger="onBlur"
hasFeedback
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
async validator(rule, value) {
const vendorid = getFieldValue("vendorid");
if (vendorid && value) {
const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER,
}
const jobId = form.getFieldValue("jobid");
if (jobId) {
loadLines({variables: {id: jobId}});
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({
variables: {
invoice_number: value,
vendorid: vendorid,
jobId: jobId,
vendorId: vendorId,
},
});
});
}
}
if (response.data.bills_aggregate.aggregate.count === 0) {
return Promise.resolve();
} else if (
response.data.bills_aggregate.nodes.length === 1 &&
response.data.bills_aggregate.nodes[0].id ===
form.getFieldValue("id")
) {
return Promise.resolve();
}
return Promise.reject(
t("bills.validation.unique_invoice_number")
);
} else {
return Promise.resolve();
}
},
}),
]}
>
<Input disabled={disabled || disableInvNumber} />
</Form.Item>
<Form.Item
label={t("bills.fields.date")}
name="date"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
if (
ClosingPeriod.treatment === "on" &&
bodyshop.accountingconfig.ClosingPeriod
) {
if (
moment(value)
.startOf("day")
.isSameOrAfter(
moment(
bodyshop.accountingconfig.ClosingPeriod[0]
).startOf("day")
) &&
moment(value)
.startOf("day")
.isSameOrBefore(
moment(
bodyshop.accountingconfig.ClosingPeriod[1]
).endOf("day")
)
) {
return Promise.resolve();
} else {
return Promise.reject(t("bills.validation.closingperiod"));
}
} else {
return Promise.resolve();
}
},
}),
]}
>
<FormDatePicker disabled={disabled} />
</Form.Item>
<Form.Item
label={t("bills.fields.is_credit_memo")}
name="is_credit_memo"
valuePropName="checked"
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (
value === true &&
getFieldValue("jobid") &&
getFieldValue("vendorid")
) {
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({
// variables: {
// jobId: form.getFieldValue("jobid"),
// vendorId: form.getFieldValue("vendorid"),
// },
// });
}
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
if (
!bodyshop.bill_allow_post_to_closed &&
job &&
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) &&
(value === false || !value)
) {
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
}
return (
<div>
<FormFieldsChanged form={form}/>
<Form.Item
style={{display: "none"}}
name="isinhouse"
valuePropName="checked"
>
<Switch/>
</Form.Item>
<LayoutFormRow grow>
<Form.Item
name="jobid"
label={t("bills.fields.ro_number")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelect
disabled={billEdit || disabled}
convertedOnly
notExported={false}
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({variables: {id: form.getFieldValue("jobid")}});
if (form.getFieldValue("vendorid") !== null) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid"),
},
});
}
}
}}
/>
</Form.Item>
<Form.Item
label={t("bills.fields.vendor")}
name="vendorid"
// style={{ display: billEdit ? "none" : null }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
validator(rule, value) {
if (
value &&
!getFieldValue(["isinhouse"]) &&
value === bodyshop.inhousevendorid
) {
return Promise.reject(t("bills.validation.manualinhouse"));
}
return Promise.resolve();
},
}),
]}
>
<VendorSearchSelect
disabled={disabled}
options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect}
/>
</Form.Item>
</LayoutFormRow>
{job &&
job.ious &&
job.ious.length > 0 &&
job.ious.map((iou) => (
<Alert
key={iou.id}
type="warning"
message={
<Space>
{t("bills.labels.iouexists")}
<Link
target="_blank"
rel="noopener noreferrer"
to={`/manage/jobs/${iou.id}?tab=repairdata`}
>
<Space>
{iou.ro_number}
<Icon component={MdOpenInNew}/>
</Space>
</Link>
</Space>
}
/>
))}
<LayoutFormRow>
<Form.Item
label={t("bills.fields.invoice_number")}
name="invoice_number"
validateTrigger="onBlur"
hasFeedback
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
async validator(rule, value) {
const vendorid = getFieldValue("vendorid");
if (vendorid && value) {
const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER,
variables: {
invoice_number: value,
vendorid: vendorid,
},
});
if (response.data.bills_aggregate.aggregate.count === 0) {
return Promise.resolve();
} else if (
response.data.bills_aggregate.nodes.length === 1 &&
response.data.bills_aggregate.nodes[0].id ===
form.getFieldValue("id")
) {
return Promise.resolve();
}
return Promise.reject(
t("bills.validation.unique_invoice_number")
);
} else {
return Promise.resolve();
}
},
}),
]}
>
<Input disabled={disabled || disableInvNumber}/>
</Form.Item>
<Form.Item
label={t("bills.fields.date")}
name="date"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
validator(rule, value) {
if (
ClosingPeriod.treatment === "on" &&
bodyshop.accountingconfig.ClosingPeriod
) {
if (
dayjs(value)
.startOf("day")
.isSameOrAfter(
dayjs(
bodyshop.accountingconfig.ClosingPeriod[0]
).startOf("day")
) &&
dayjs(value)
.startOf("day")
.isSameOrBefore(
dayjs(
bodyshop.accountingconfig.ClosingPeriod[1]
).endOf("day")
)
) {
return Promise.resolve();
} else {
return Promise.reject(t("bills.validation.closingperiod"));
}
} else {
return Promise.resolve();
}
},
}),
]}
>
<FormDatePicker disabled={disabled}/>
</Form.Item>
<Form.Item
label={t("bills.fields.is_credit_memo")}
name="is_credit_memo"
valuePropName="checked"
rules={[
({getFieldValue}) => ({
validator(rule, value) {
if (
value === true &&
getFieldValue("jobid") &&
getFieldValue("vendorid")
) {
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({
// variables: {
// jobId: form.getFieldValue("jobid"),
// vendorId: form.getFieldValue("vendorid"),
// },
// });
}
if (
!bodyshop.bill_allow_post_to_closed &&
job &&
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) &&
(value === false || !value)
) {
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
}
return Promise.resolve();
},
@@ -401,15 +375,15 @@ export function BillFormComponent({
>
<CurrencyInput min={0} />
</Form.Item>
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
<Form.Item
span={2}
label={t("bills.labels.federal_tax_exempt")}
name="federal_tax_exempt"
>
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
</Form.Item>
) : null}
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
<Form.Item
span={2}
label={t("bills.labels.federal_tax_exempt")}
name="federal_tax_exempt"
>
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
</Form.Item>
) : null}
<Form.Item shouldUpdate span={13}>
{() => {
const values = form.getFieldsValue([
@@ -486,55 +460,55 @@ export function BillFormComponent({
</LayoutFormRow>
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
{Extended_Bill_Posting.treatment === "on" ? (
<BillFormLinesExtended
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
) : (
<BillFormLines
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
billEdit={billEdit}
/>
)}
{Extended_Bill_Posting.treatment === "on" ? (
<BillFormLinesExtended
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
) : (
<BillFormLines
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
billEdit={billEdit}
/>
)}
<Form.Item
name="upload"
label="Upload"
style={{ display: billEdit ? "none" : null }}
valuePropName="fileList"
getValueFromEvent={(e) => {
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
}}
>
<Upload.Dragger
multiple={true}
name="logo"
beforeUpload={() => false}
listType="picture"
>
<>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
</Upload.Dragger>
</Form.Item>
</div>
);
<Form.Item
name="upload"
label="Upload"
style={{display: billEdit ? "none" : null}}
valuePropName="fileList"
getValueFromEvent={(e) => {
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
}}
>
<Upload.Dragger
multiple={true}
name="logo"
beforeUpload={() => false}
listType="picture"
>
<>
<p className="ant-upload-drag-icon">
<UploadOutlined/>
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
</Upload.Dragger>
</Form.Item>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent);

View File

@@ -1,5 +1,5 @@
import { useLazyQuery, useQuery } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -23,11 +23,11 @@ export function BillFormContainer({
disabled,
disableInvNumber,
}) {
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
const { treatments: {Simple_Inventory} } = useSplitTreatments({
attributes: {},
names: ["Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid,
});
const { data: VendorAutoCompleteData } = useQuery(
SEARCH_VENDOR_AUTOCOMPLETE,

View File

@@ -1,5 +1,5 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import {
Button,
Form,
@@ -41,11 +41,14 @@ export function BillEnterModalLinesComponent({
}) {
const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
const { treatments: {Simple_Inventory} } = useSplitTreatments({
attributes: {},
names: ["Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid,
});
const columns = (remove) => {
return [
{

View File

@@ -15,7 +15,8 @@ const BillLineSearchSelect = (
disabled={disabled}
ref={ref}
showSearch
dropdownMatchSelectWidth={false}
popupMatchSelectWidth={false}
optionLabelProp={"name"}
// optionFilterProp="line_desc"
filterOption={(inputValue, option) => {
return (
@@ -57,6 +58,9 @@ const BillLineSearchSelect = (
style={{
...(item.removed ? { textDecoration: "line-through" } : {}),
}}
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
>
<span>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${

View File

@@ -2,7 +2,7 @@ import { FileAddFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, notification, Tooltip } from "antd";
import { t } from "i18next";
import moment from "moment";
import dayjs from "./../../utils/day";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -36,7 +36,6 @@ export function BilllineAddInventory({
}) {
const [loading, setLoading] = useState(false);
const { billid } = queryString.parse(useLocation().search);
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
const addToInventory = async () => {
@@ -50,7 +49,7 @@ export function BilllineAddInventory({
jobid: jobid,
isinhouse: true,
is_credit_memo: true,
date: moment().format("YYYY-MM-DD"),
date: dayjs().format("YYYY-MM-DD"),
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
@@ -92,7 +91,7 @@ export function BilllineAddInventory({
pol: {
returnfrombill: billid,
vendorid: bodyshop.inhousevendorid,
deliver_by: moment().format("YYYY-MM-DD"),
deliver_by: dayjs().format("YYYY-MM-DD"),
parts_order_lines: {
data: [
{

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { useQuery } from "@apollo/client";
import queryString from "query-string";
import { useHistory, useLocation } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import { Table, Input } from "antd";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
@@ -10,7 +10,7 @@ import AlertComponent from "../alert/alert.component";
export default function BillsVendorsList() {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const history = useNavigate();
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, {
fetchPolicy: "network-only",

View File

@@ -1,54 +1,64 @@
import { HomeFilled } from "@ant-design/icons";
import { Breadcrumb, Row, Col } from "antd";
import {HomeFilled} from "@ant-design/icons";
import {Breadcrumb, Col, Row} from "antd";
import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import {connect} from "react-redux";
import {Link} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import {selectBreadcrumbs} from "../../redux/application/application.selectors";
import {selectBodyshop} from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component";
import GlobalSearchOs from "../global-search/global-search-os.component";
import "./breadcrumbs.styles.scss";
import { useTreatments } from "@splitsoftware/splitio-react";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
breadcrumbs: selectBreadcrumbs,
bodyshop: selectBodyshop,
breadcrumbs: selectBreadcrumbs,
bodyshop: selectBodyshop,
});
export function BreadCrumbs({ breadcrumbs, bodyshop }) {
const { OpenSearch } = useTreatments(
["OpenSearch"],
{},
bodyshop && bodyshop.imexshopid
);
export function BreadCrumbs({breadcrumbs, bodyshop}) {
return (
<Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}>
<Breadcrumb separator=">">
<Breadcrumb.Item>
<Link to={`/manage`}>
<HomeFilled />{" "}
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
""}
</Link>
</Breadcrumb.Item>
{breadcrumbs.map((item) =>
item.link ? (
<Breadcrumb.Item key={item.label}>
<Link to={item.link}>{item.label} </Link>
</Breadcrumb.Item>
) : (
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
)
)}
</Breadcrumb>
</Col>
<Col xs={24} sm={24} md={8}>
{OpenSearch.treatment === "on" ? <GlobalSearchOs /> : <GlobalSearch />}
</Col>
</Row>
);
const {treatments: {OpenSearch}} = useSplitTreatments({
attributes: {},
names: ["OpenSearch"],
splitKey: bodyshop && bodyshop.imexshopid,
});
// TODO - Client Update - Technically key is not doing anything here
return (
<Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}>
<Breadcrumb
separator=">"
items={[
{
key: "home",
title: (
<Link to={`/manage/`}>
<HomeFilled/>{" "}
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
""}
</Link>
),
},
...breadcrumbs.map((item) =>
item.link
? {
key: item.label,
title: <Link to={item.link}>{item.label}</Link>,
}
: {
key: item.label,
title: item.label,
}
),
]}
/>
</Col>
<Col xs={24} sm={24} md={8}>
{OpenSearch.treatment === "on" ? <GlobalSearchOs/> : <GlobalSearch/>}
</Col>
</Row>
);
}
export default connect(mapStateToProps, null)(BreadCrumbs);

View File

@@ -25,7 +25,7 @@ export function ContractsFindModalContainer({
}) {
const { t } = useTranslation();
const { visible } = caBcEtfTableModal;
const { open } = caBcEtfTableModal;
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const EtfTemplate = TemplateList("special").ca_bc_etf_table;
@@ -63,14 +63,14 @@ export function ContractsFindModalContainer({
};
useEffect(() => {
if (visible) {
if (open) {
form.resetFields();
}
}, [visible, form]);
}, [open, form]);
return (
<Modal
visible={visible}
open={open}
width="70%"
title={t("payments.labels.findermodal")}
onCancel={() => toggleModalVisible()}

View File

@@ -38,7 +38,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
<Popover
destroyTooltipOnHide
content={popContent}
visible={visibility}
open={visibility}
disabled={disabled}
>
<Button disabled={disabled} onClick={() => setVisibility(true)}>

View File

@@ -13,7 +13,7 @@ import {
notification,
} from "antd";
import axios from "axios";
import moment from "moment";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -117,7 +117,7 @@ const CardPaymentModalComponent = ({
payer: t("payments.labels.customer"),
type: values.paymentResponse.cardbrand,
jobid: payment.jobid,
date: moment(Date.now()),
date: dayjs(Date.now()),
payment_responses: {
data: [
{

View File

@@ -22,7 +22,7 @@ function CardPaymentModalContainer({
toggleModalVisible,
bodyshop,
}) {
const { visible } = cardPaymentModal;
const { open } = cardPaymentModal;
const { t } = useTranslation();
const handleCancel = () => {
@@ -35,7 +35,7 @@ function CardPaymentModalContainer({
return (
<Modal
open={visible}
open={open}
onOk={handleOK}
onCancel={handleCancel}
footer={[

View File

@@ -4,20 +4,11 @@ import { Button, notification, Space } from "antd";
import axios from "axios";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { messaging, requestForToken } from "../../firebase/firebase.utils";
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FcmHandler from "../../utils/fcm-handler";
import ChatPopupComponent from "../chat-popup/chat-popup.component";
import "./chat-affix.styles.scss";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
chatVisible: selectChatVisible,
});
export function ChatAffixContainer({ bodyshop, chatVisible }) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -28,7 +19,7 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
try {
const r = await axios.post("/notifications/subscribe", {
fcm_tokens: await getToken(messaging, {
vapidKey: process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY,
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY,
}),
type: "messaging",
imexshopid: bodyshop.imexshopid,
@@ -36,35 +27,34 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
console.log("FCM Topic Subscription", r.data);
} catch (error) {
console.log(
"Error attempting to subscribe to messaging topic: ",
error
"Error attempting to subscribe to messaging topic: ",
error
);
notification.open({
type: "warning",
message: t("general.errors.fcm"),
btn: (
<Space>
<Button
onClick={async () => {
await requestForToken();
SubscribeToTopic();
}}
>
{t("general.actions.tryagain")}
</Button>
<Button
onClick={() => {
const win = window.open(
"https://help.imex.online/en/article/enabling-notifications-o978xi/",
"_blank"
);
win.focus();
}}
>
{t("general.labels.help")}
</Button>
</Space>
<Space>
<Button
onClick={async () => {
await requestForToken();
SubscribeToTopic();
}}
>
{t("general.actions.tryagain")}
</Button>
<Button
onClick={() => {
const win = window.open(
"https://help.imex.online/en/article/enabling-notifications-o978xi/",
"_blank"
);
win.focus();
}}
>
{t("general.labels.help")}
</Button>
</Space>
),
});
}
@@ -81,16 +71,16 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
payload: (payload && payload.data && payload.data.data) || payload.data,
});
}
let stopMessageListenr, channel;
let stopMessageListener, channel;
try {
stopMessageListenr = onMessage(messaging, handleMessage);
stopMessageListener = onMessage(messaging, handleMessage);
channel = new BroadcastChannel("imex-sw-messages");
channel.addEventListener("message", handleMessage);
} catch (error) {
console.log("Unable to set event listeners.");
}
return () => {
stopMessageListenr && stopMessageListenr();
stopMessageListener && stopMessageListener();
channel && channel.removeEventListener("message", handleMessage);
};
}, [client]);
@@ -98,9 +88,10 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
</div>
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
</div>
);
}
export default connect(mapStateToProps, null)(ChatAffixContainer);
export default ChatAffixContainer;

View File

@@ -1,120 +1,122 @@
import { Badge, List, Tag } from "antd";
import {Badge, Card, List, Space, Tag} from "antd";
import React from "react";
import { connect } from "react-redux";
import {
AutoSizer,
CellMeasurer,
CellMeasurerCache,
List as VirtualizedList,
} from "react-virtualized";
import { createStructuredSelector } from "reselect";
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
import { TimeAgoFormatter } from "../../utils/DateFormatter";
import {connect} from "react-redux";
import {AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList,} from "react-virtualized";
import {createStructuredSelector} from "reselect";
import {setSelectedConversation} from "../../redux/messaging/messaging.actions";
import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors";
import {TimeAgoFormatter} from "../../utils/DateFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
import _ from "lodash";
import "./chat-conversation-list.styles.scss";
const mapStateToProps = createStructuredSelector({
selectedConversation: selectSelectedConversation,
selectedConversation: selectSelectedConversation,
});
const mapDispatchToProps = (dispatch) => ({
setSelectedConversation: (conversationId) =>
dispatch(setSelectedConversation(conversationId)),
setSelectedConversation: (conversationId) =>
dispatch(setSelectedConversation(conversationId)),
});
function ChatConversationListComponent({
conversationList,
selectedConversation,
setSelectedConversation,
loadMoreConversations,
}) {
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 60,
});
conversationList,
selectedConversation,
setSelectedConversation,
loadMoreConversations,
}) {
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 60,
});
const rowRenderer = ({ index, key, style, parent }) => {
const item = conversationList[index];
const rowRenderer = ({index, key, style, parent}) => {
const item = conversationList[index];
const cardContentRight =
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
const cardContentLeft = item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => (
<Tag key={idx}>{j.job.ro_number}</Tag>
))
: null;
const names = <>{_.uniq(item.job_conversations.map((j, idx) =>
OwnerNameDisplayFunction(j.job)
))}</>
const cardTitle = <>
{item.label && <Tag color="blue">{item.label}</Tag>}
{item.job_conversations.length > 0 ? (
<Space direction="vertical">
{names}
</Space>
) : (
<Space>
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
</Space>
)}
</>
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0}/>
const getCardStyle = () =>
item.id === selectedConversation
? { backgroundColor: 'rgba(128, 128, 128, 0.2)' }
: { backgroundColor: index % 2 === 0 ? '#f0f2f5' : '#ffffff' };
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
<List.Item
onClick={() => setSelectedConversation(item.id)}
style={style}
className={`chat-list-item
${
item.id === selectedConversation
? "chat-list-selected-conversation"
: null
}`}
>
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
<div style={{display: 'inline-block', width: '70%', textAlign: 'left'}}>
{cardContentLeft}
</div>
<div
style={{display: 'inline-block', width: '30%', textAlign: 'right'}}>{cardContentRight}</div>
</Card>
</List.Item>
</CellMeasurer>
);
};
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
<List.Item
onClick={() => setSelectedConversation(item.id)}
className={`chat-list-item ${
item.id === selectedConversation
? "chat-list-selected-conversation"
: null
}`}
style={style}
>
<div
style={{
display: "inline-block",
}}
>
{item.label && <div className="chat-name">{item.label}</div>}
{item.job_conversations.length > 0 ? (
<div className="chat-name">
{item.job_conversations.map((j, idx) => (
<div key={idx}>
<OwnerNameDisplay ownerObject={j.job} />
</div>
))}
</div>
) : (
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
)}
</div>
<div style={{ display: "inline-block" }}>
<div>
{item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => (
<Tag key={idx} className="ro-number-tag">
{j.job.ro_number}
</Tag>
))
: null}
</div>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
</div>
<Badge count={item.messages_aggregate.aggregate.count || 0} />
</List.Item>
</CellMeasurer>
<div className="chat-list-container">
<AutoSizer>
{({height, width}) => (
<VirtualizedList
height={height}
width={width}
rowCount={conversationList.length}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
onScroll={({scrollTop, scrollHeight, clientHeight}) => {
if (scrollTop + clientHeight === scrollHeight) {
loadMoreConversations();
}
}}
/>
)}
</AutoSizer>
</div>
);
};
return (
<div className="chat-list-container">
<AutoSizer>
{({ height, width }) => (
<VirtualizedList
height={height}
width={width}
rowCount={conversationList.length}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
if (scrollTop + clientHeight === scrollHeight) {
loadMoreConversations();
}
}}
/>
)}
</AutoSizer>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(ChatConversationListComponent);

View File

@@ -1,27 +1,14 @@
.chat-list-selected-conversation {
background-color: rgba(128, 128, 128, 0.2);
}
.chat-list-container {
flex: 1;
overflow: hidden;
height: 100%;
border: 1px solid gainsboro;
}
.chat-list-item {
display: flex;
flex-direction: row;
.ant-card-head {
border: none;
}
&:hover {
cursor: pointer;
color: #ff7a00;
}
.chat-name {
flex: 1;
display: inline;
}
.ro-number-tag {
align-self: baseline;
}
padding: 12px 24px;
border-bottom: 1px solid gainsboro;
}

View File

@@ -27,7 +27,7 @@ export function ChatMediaSelector({
conversation,
}) {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [open, setOpen] = useState(false);
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
fetchPolicy: "network-only",
@@ -39,13 +39,13 @@ export function ChatMediaSelector({
},
skip:
!visible ||
!open ||
!conversation.job_conversations ||
conversation.job_conversations.length === 0,
});
const handleVisibleChange = (visible) => {
setVisible(visible);
const handleVisibleChange = (change) => {
setOpen(change);
};
useEffect(() => {
@@ -65,7 +65,7 @@ export function ChatMediaSelector({
externalMediaState={[selectedMedia, setSelectedMedia]}
/>
)}
{bodyshop.uselocalmediaserver && visible && (
{bodyshop.uselocalmediaserver && open && (
<JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={
@@ -88,8 +88,8 @@ export function ChatMediaSelector({
}
title={t("messaging.labels.selectmedia")}
trigger="click"
visible={visible}
onVisibleChange={handleVisibleChange}
open={open}
onOpenChange={handleVisibleChange}
>
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
<PictureFilled style={{ margin: "0 .5rem" }} />

View File

@@ -1,7 +1,7 @@
import Icon from "@ant-design/icons";
import { Tooltip } from "antd";
import i18n from "i18next";
import moment from "moment";
import dayjs from "../../utils/day";
import React, { useEffect, useRef } from "react";
import { MdDone, MdDoneAll } from "react-icons/md";
import {
@@ -52,7 +52,7 @@ export default function ChatMessageListComponent({ messages }) {
<div style={{ fontSize: 10 }}>
{i18n.t("messaging.labels.sentby", {
by: messages[index].userid,
time: moment(messages[index].created_at).format(
time: dayjs(messages[index].created_at).format(
"MM/DD/YYYY @ hh:mm a"
),
})}

View File

@@ -1,5 +1,5 @@
import { PlusCircleOutlined } from "@ant-design/icons";
import { Dropdown, Menu } from "antd";
import { Dropdown } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -16,19 +16,16 @@ const mapDispatchToProps = (dispatch) => ({
});
export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
const menu = (
<Menu>
{bodyshop.md_messaging_presets.map((i, idx) => (
<Menu.Item onClick={() => setMessage(i.text)} key={idx}>
{i.label}
</Menu.Item>
))}
</Menu>
);
const items = bodyshop.md_messaging_presets.map((i, idx) => ({
key: idx,
label: (i.label),
onClick: () => setMessage(i.text),
}));
return (
<div className={className}>
<Dropdown trigger={["click"]} overlay={menu}>
<Dropdown trigger={["click"]} menu={{items}}>
<PlusCircleOutlined />
</Dropdown>
</div>

View File

@@ -16,42 +16,28 @@ const mapDispatchToProps = (dispatch) => ({
export function ChatPrintButton({ conversation }) {
const [loading, setLoading] = useState(false);
const generateDocument = (type) => {
setLoading(true);
GenerateDocument(
{
name: TemplateList("messaging").conversation_list.key,
variables: { id: conversation.id },
},
{
subject: TemplateList("messaging").conversation_list.subject,
},
type,
conversation.id
).catch(e => {
console.warn('Something went wrong generating a document.');
});
setLoading(false);
}
return (
<Space wrap>
<PrinterOutlined
onClick={() => {
setLoading(true);
GenerateDocument(
{
name: TemplateList("messaging").conversation_list.key,
variables: { id: conversation.id },
},
{
subject: TemplateList("messaging").conversation_list.subject,
},
"p",
conversation.id
);
setLoading(false);
}}
/>
<MailOutlined
onClick={() => {
setLoading(true);
GenerateDocument(
{
name: TemplateList("messaging").conversation_list.key,
variables: { id: conversation.id },
},
{
subject: TemplateList("messaging").conversation_list.subject,
},
"e",
conversation.id
);
setLoading(false);
}}
/>
<PrinterOutlined onClick={() => generateDocument('p')}/>
<MailOutlined onClick={() => generateDocument('e')}/>
{loading && <Spin />}
</Space>
);

View File

@@ -9,17 +9,17 @@ export default function ChatTagRoComponent({
loading,
handleSearch,
handleInsertTag,
setVisible,
setOpen,
}) {
const { t } = useTranslation();
return (
<Space flex>
<Space>
<div style={{ width: "15rem" }}>
<Select
showSearch
autoFocus
dropdownMatchSelectWidth
popupMatchSelectWidth
placeholder={t("general.labels.search")}
filterOption={false}
onSearch={handleSearch}
@@ -38,7 +38,7 @@ export default function ChatTagRoComponent({
{loading ? (
<LoadingOutlined />
) : (
<CloseCircleOutlined onClick={() => setVisible(false)} />
<CloseCircleOutlined onClick={() => setOpen(false)} />
)}
</Space>
);

View File

@@ -11,7 +11,7 @@ import ChatTagRo from "./chat-tag-ro.component";
export default function ChatTagRoContainer({ conversation }) {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [open, setOpen] = useState(false);
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
@@ -33,7 +33,7 @@ export default function ChatTagRoContainer({ conversation }) {
const handleInsertTag = (value, option) => {
logImEXEvent("messaging_add_job_tag");
insertTag({ variables: { jobId: option.key } });
setVisible(false);
setOpen(false);
};
const existingJobTags =
@@ -47,16 +47,16 @@ export default function ChatTagRoContainer({ conversation }) {
return (
<div>
{visible ? (
{open ? (
<ChatTagRo
loading={loading}
roOptions={roOptions}
handleSearch={handleSearch}
handleInsertTag={handleInsertTag}
setVisible={setVisible}
setOpen={setOpen}
/>
) : (
<Tag onClick={() => setVisible(true)}>
<Tag onClick={() => setOpen(true)}>
<PlusOutlined />
{t("messaging.actions.link")}
</Tag>

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@apollo/client";
import moment from "moment";
import dayjs from "../../utils/day";
import React from "react";
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
import AlertComponent from "../alert/alert.component";
@@ -7,7 +7,7 @@ import ContractCarsComponent from "./contract-cars.component";
export default function ContractCarsContainer({ selectedCarState, form }) {
const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC, {
variables: { today: moment().format("YYYY-MM-DD") },
variables: { today: dayjs().format("YYYY-MM-DD") },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});

View File

@@ -10,11 +10,11 @@ import {
Space,
} from "antd";
import axios from "axios";
import moment from "moment";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
import {
@@ -38,17 +38,17 @@ export function ContractConvertToRo({
disabled,
}) {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [insertJob] = useMutation(INSERT_NEW_JOB);
const history = useHistory();
const history = useNavigate();
const handleFinish = async (values) => {
setLoading(true);
const contractLength = moment(contract.actualreturn).diff(
moment(contract.start),
"days"
const contractLength = dayjs(contract.actualreturn).diff(
dayjs(contract.start),
"day"
);
const billingLines = [];
if (contractLength > 0)
@@ -306,7 +306,7 @@ export function ContractConvertToRo({
});
}
setVisible(false);
setOpen(false);
setLoading(false);
};
@@ -380,7 +380,7 @@ export function ContractConvertToRo({
<Button type="primary" htmlType="submit" loading={loading}>
{t("contracts.actions.convertoro")}
</Button>
<Button onClick={() => setVisible(false)}>
<Button onClick={() => setOpen(false)}>
{t("general.actions.close")}
</Button>
</Space>
@@ -390,9 +390,9 @@ export function ContractConvertToRo({
return (
<div>
<Popover content={popContent} visible={visible}>
<Popover content={popContent} open={open}>
<Button
onClick={() => setVisible(true)}
onClick={() => setOpen(true)}
loading={loading}
disabled={!contract.dailyrate || !contract.actualreturn || disabled}
>

View File

@@ -1,6 +1,6 @@
import { WarningFilled } from "@ant-design/icons";
import { Form, Input, InputNumber, Space } from "antd";
import moment from "moment";
import dayjs from "../../utils/day";
import React from "react";
import { useTranslation } from "react-i18next";
import { DateFormatter } from "../../utils/DateFormatter";
@@ -96,8 +96,8 @@ export default function ContractFormComponent({
const dueForService =
selectedCar &&
selectedCar.nextservicedate &&
moment(selectedCar.nextservicedate).isBefore(
moment(form.getFieldValue("scheduledreturn"))
dayjs(selectedCar.nextservicedate).isBefore(
dayjs(form.getFieldValue("scheduledreturn"))
);
if (mileageOver || dueForService)
@@ -190,9 +190,9 @@ export default function ContractFormComponent({
}
>
{() => {
const dlExpiresBeforeReturn = moment(
const dlExpiresBeforeReturn = dayjs(
form.getFieldValue("driver_dlexpiry")
).isBefore(moment(form.getFieldValue("scheduledreturn")));
).isBefore(dayjs(form.getFieldValue("scheduledreturn")));
return (
<div>

View File

@@ -1,5 +1,5 @@
import { Button, Input, Modal, Typography } from "antd";
import moment from "moment";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import aamva from "../../utils/aamva";
@@ -26,8 +26,8 @@ export default function ContractLicenseDecodeButton({ form }) {
const values = {
driver_dlnumber: decodedBarcode.dl,
driver_dlexpiry: moment(
`20${decodedBarcode.expiration_date}${moment(
driver_dlexpiry: dayjs(
`20${decodedBarcode.expiration_date}${dayjs(
decodedBarcode.birthday
).format("DD")}`
),
@@ -38,7 +38,7 @@ export default function ContractLicenseDecodeButton({ form }) {
driver_city: decodedBarcode.city,
driver_state: decodedBarcode.state,
driver_zip: decodedBarcode.postal_code,
driver_dob: moment(decodedBarcode.birthday),
driver_dob: dayjs(decodedBarcode.birthday),
};
form.setFieldsValue(values);
@@ -55,7 +55,7 @@ export default function ContractLicenseDecodeButton({ form }) {
return (
<div>
<Modal
visible={modalVisible}
open={modalVisible}
okText={t("contracts.actions.senddltoform")}
onOk={handleInsertForm}
okButtonProps={{ disabled: !!!decodedBarcode }}
@@ -94,14 +94,14 @@ export default function ContractLicenseDecodeButton({ form }) {
{decodedBarcode.address}
</DataLabel>
<DataLabel label={t("contracts.fields.driver_dlexpiry")}>
{moment(
`20${decodedBarcode.expiration_date}${moment(
{dayjs(
`20${decodedBarcode.expiration_date}${dayjs(
decodedBarcode.birthday
).format("DD")}`
).format("MM/DD/YYYY")}
</DataLabel>
<DataLabel label={t("contracts.fields.driver_dob")}>
{moment(decodedBarcode.birthday).format("MM/DD/YYYY")}
{dayjs(decodedBarcode.birthday).format("MM/DD/YYYY")}
</DataLabel>
<div>
<Typography.Title level={4}>

View File

@@ -31,7 +31,7 @@ export function ContractsFindModalContainer({
}) {
const { t } = useTranslation();
const { visible } = contractFinderModal;
const { open } = contractFinderModal;
const [form] = Form.useForm();
@@ -52,14 +52,14 @@ export function ContractsFindModalContainer({
};
useEffect(() => {
if (visible) {
if (open) {
form.resetFields();
}
}, [visible, form]);
}, [open, form]);
return (
<Modal
visible={visible}
open={open}
width="70%"
title={t("contracts.labels.findermodal")}
onCancel={() => toggleModalVisible()}

View File

@@ -3,13 +3,13 @@ import { Button, Card, Input, Space, Table, Typography } from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import { Link, useNavigate, useLocation } from "react-router-dom";
import { setModalContext } from "../../redux/modals/modals.actions";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import ContractsFindModalContainer from "../contracts-find-modal/contracts-find-modal.container";
import moment from "moment";
import dayjs from "../../utils/day";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
@@ -39,7 +39,7 @@ export function ContractsList({
sortedInfo: {},
filteredInfo: { text: "" },
});
const history = useHistory();
const history = useNavigate();
const search = queryString.parse(useLocation().search);
const { page } = search;
@@ -152,8 +152,8 @@ export function ContractsList({
render: (text, record) =>
(record.actualreturn &&
record.start &&
`${moment(record.actualreturn)
.diff(moment(record.start), "days", true)
`${dayjs(record.actualreturn)
.diff(dayjs(record.start), "day", true)
.toFixed(1)} days`) ||
"",
},
@@ -164,7 +164,7 @@ export function ContractsList({
search.page = pagination.current;
search.sortcolumn = sorter.columnKey;
search.sortorder = sorter.order;
history.push({ search: queryString.stringify(search) });
history({ search: queryString.stringify(search) });
};
return (
@@ -179,7 +179,7 @@ export function ContractsList({
<Button
onClick={() => {
delete search.search;
history.push({ search: queryString.stringify(search) });
history({ search: queryString.stringify(search) });
}}
>
{t("general.actions.clear")}
@@ -196,7 +196,7 @@ export function ContractsList({
placeholder={search.searh || t("general.labels.search")}
onSearch={(value) => {
search.search = value;
history.push({ search: queryString.stringify(search) });
history({ search: queryString.stringify(search) });
}}
/>
</Space>

View File

@@ -1,5 +1,5 @@
import { DownOutlined } from "@ant-design/icons";
import { Dropdown, Menu } from "antd";
import { Dropdown } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -18,20 +18,16 @@ export function ContractsRatesChangeButton({ disabled, form, bodyshop }) {
form.setFieldsValue(rate);
};
const menu = (
<div>
<Menu onClick={handleClick}>
{bodyshop.md_ccc_rates.map((rate, idx) => (
<Menu.Item value={rate} key={idx}>
{rate.label}
</Menu.Item>
))}
</Menu>
</div>
);
const menuItems = bodyshop.md_ccc_rates.map((i, idx) => ({
key: idx,
label: i.label,
value: i,
}));
const menu = {items: menuItems, onClick: handleClick};
return (
<Dropdown overlay={menu} disabled={disabled}>
<Dropdown menu={menu} disabled={disabled}>
<a
className="ant-dropdown-link"
href=" #"

View File

@@ -2,7 +2,7 @@ import { Card, Table } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import {pageLimit} from "../../utils/config";
@@ -11,9 +11,9 @@ export default function CourtesyCarContractListComponent({
contracts,
totalContracts,
}) {
const search = queryString.parse(useLocation().search);
const search =queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder } = search;
const history = useHistory();
const history = useNavigate();
const { t } = useTranslation();
@@ -81,7 +81,7 @@ export default function CourtesyCarContractListComponent({
search.page = pagination.current;
search.sortcolumn = sorter.columnKey;
search.sortorder = sorter.order;
history.push({ search: queryString.stringify(search) });
history({ search: queryString.stringify(search) });
};
return (

View File

@@ -1,7 +1,8 @@
import { WarningFilled } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { Button, Form, Input, InputNumber, PageHeader, Space } from "antd";
import moment from "moment";
import { Button, Form, Input, InputNumber, Space } from "antd";
import {PageHeader} from "@ant-design/pro-layout";
import dayjs from "../../utils/day";
import React from "react";
import { useTranslation } from "react-i18next";
import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries";
@@ -263,7 +264,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
const nextservicedate = form.getFieldValue("nextservicedate");
const dueForService =
nextservicedate &&
moment(nextservicedate).endOf("day").isSameOrBefore(moment());
dayjs(nextservicedate).endOf("day").isSameOrBefore(dayjs());
if (dueForService)
return (
@@ -304,7 +305,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
const expires = form.getFieldValue("registrationexpires");
const dateover =
expires && moment(expires).endOf("day").isBefore(moment());
expires && dayjs(expires).endOf("day").isBefore(dayjs());
if (dateover)
return (
@@ -340,7 +341,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
const expires = form.getFieldValue("insuranceexpires");
const dateover =
expires && moment(expires).endOf("day").isBefore(moment());
expires && dayjs(expires).endOf("day").isBefore(dayjs());
if (dateover)
return (

View File

@@ -7,7 +7,7 @@ import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCourtesyCarReturn } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CourtesyCarReturnModalComponent from "./courtesy-car-return-modal.component";
import moment from "moment";
import dayjs from "../../utils/day";
import { RETURN_CONTRACT } from "../../graphql/cccontracts.queries";
import { useMutation } from "@apollo/client";
@@ -26,7 +26,7 @@ export function CCReturnModalContainer({
bodyshop,
}) {
const [loading, setLoading] = useState(false);
const { visible, context, actions } = courtesyCarReturnModal;
const { open, context, actions } = courtesyCarReturnModal;
const { t } = useTranslation();
const [form] = Form.useForm();
const [updateContract] = useMutation(RETURN_CONTRACT);
@@ -64,7 +64,7 @@ export function CCReturnModalContainer({
return (
<Modal
title={t("courtesycars.labels.return")}
visible={visible}
open={open}
onCancel={() => toggleModalVisible()}
width={"90%"}
okText={t("general.actions.save")}
@@ -74,7 +74,7 @@ export function CCReturnModalContainer({
<Form
form={form}
onFinish={handleFinish}
initialValues={{ fuel: 100, actualreturn: moment(new Date()) }}
initialValues={{ fuel: 100, actualreturn: dayjs(new Date()) }}
>
<CourtesyCarReturnModalComponent />
</Form>

View File

@@ -4,12 +4,11 @@ import {
Card,
Dropdown,
Input,
Menu,
Space,
Table,
Tooltip,
} from "antd";
import moment from "moment";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
@@ -77,8 +76,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
const dueForService =
nextservicedate &&
moment(nextservicedate).endOf("day").isSameOrBefore(moment());
nextservicedate && dayjs(nextservicedate).endOf('day').isSameOrBefore(dayjs());
return (
<Space>
@@ -229,6 +227,27 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
(t(c.status) || "").toLowerCase().includes(searchText.toLowerCase())
)
: courtesycars;
const items = [
{
key: "courtesycar_inventory",
label: t("printcenter.courtesycarcontract.courtesy_car_inventory"),
onClick: () =>
GenerateDocument(
{
name: TemplateList("courtesycar").courtesy_car_inventory.key,
variables: {
//id: contract.id
},
},
{},
"p"
),
},
];
const menu = { items };
return (
<Card
title={t("menus.header.courtesycars")}
@@ -237,30 +256,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Dropdown
trigger="click"
overlay={
<Menu>
<Menu.Item
onClick={() =>
GenerateDocument(
{
name: TemplateList("courtesycar").courtesy_car_inventory
.key,
variables: {
//id: contract.id
},
},
{},
"p"
)
}
>
{t("printcenter.courtesycarcontract.courtesy_car_inventory")}
</Menu.Item>
</Menu>
}
>
<Dropdown trigger="click" menu={menu}>
<Button>{t("general.labels.print")}</Button>
</Dropdown>
<Link to={`/manage/courtesycars/new`}>

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { Card } from "antd";
import _ from "lodash";
import moment from "moment";
import dayjs from "../../../utils/day";
import React from "react";
import { useTranslation } from "react-i18next";
import {
@@ -27,7 +27,7 @@ export default function DashboardMonthlyEmployeeEfficiency({
return <DashboardRefreshRequired {...cardProps} />;
const ticketsByDate = _.groupBy(data.monthly_employee_efficiency, (item) =>
moment(item.date).format("YYYY-MM-DD")
dayjs(item.date).format("YYYY-MM-DD")
);
const listOfDays = Utils.ListOfDaysInCurrentMonth();
@@ -53,7 +53,7 @@ export default function DashboardMonthlyEmployeeEfficiency({
((dailyHrs.productive - dailyHrs.actual) / dailyHrs.actual + 1) * 100;
const theValue = {
date: moment(val).format("DD"),
date: dayjs(val).format("DD"),
// ...dailyHrs,
actual: dailyHrs.actual.toFixed(1),
productive: dailyHrs.productive.toFixed(1),
@@ -159,9 +159,9 @@ export default function DashboardMonthlyEmployeeEfficiency({
}
export const DashboardMonthlyEmployeeEfficiencyGql = `
monthly_employee_efficiency: timetickets(where: {_and: [{date: {_gte: "${moment()
monthly_employee_efficiency: timetickets(where: {_and: [{date: {_gte: "${dayjs()
.startOf("month")
.format("YYYY-MM-DD")}"}},{date: {_lte: "${moment()
.format("YYYY-MM-DD")}"}},{date: {_lte: "${dayjs()
.endOf("month")
.format("YYYY-MM-DD")}"}} ]}) {
actualhrs

View File

@@ -21,6 +21,8 @@ export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
async function getCostingData() {
if (data && data.monthly_sales) {
setLoading(true);
console.log('defaults:')
console.dir(axios.defaults);
const response = await axios.post("/job/costingmulti", {
jobids: data.monthly_sales.map((x) => x.id),
});

View File

@@ -1,5 +1,5 @@
import { Card } from "antd";
import moment from "moment";
import dayjs from "../../../utils/day";
import React from "react";
import { useTranslation } from "react-i18next";
import _ from "lodash";
@@ -24,7 +24,7 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
const jobsByDate = _.groupBy(data.monthly_sales, (item) =>
moment(item.date_invoiced).format("YYYY-MM-DD")
dayjs(item.date_invoiced).format("YYYY-MM-DD")
);
const listOfDays = Utils.ListOfDaysInCurrentMonth();
@@ -43,7 +43,7 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
}
const theValue = {
date: moment(val).format("DD"),
date: dayjs(val).format("DD"),
dailySales: dailySales.getAmount() / 100,
accSales:
acc.length > 0

View File

@@ -1,6 +1,6 @@
import { Card, Statistic } from "antd";
import Dinero from "dinero.js";
import moment from "moment";
import dayjs from "../../../utils/day";
import React from "react";
import { useTranslation } from "react-i18next";
import DashboardRefreshRequired from "../refresh-required.component";
@@ -36,10 +36,10 @@ export const DashboardProjectedMonthlySalesGql = `
_or: [
{_and: [
{date_invoiced:{_is_null: false }},
{date_invoiced: {_gte: "${moment()
{date_invoiced: {_gte: "${dayjs()
.startOf("month")
.startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.toISOString()}"}}, {date_invoiced: {_lte: "${dayjs()
.endOf("month")
.endOf("day")
.toISOString()}"}}]},
@@ -47,10 +47,10 @@ export const DashboardProjectedMonthlySalesGql = `
_and:[
{date_invoiced:{_is_null: true }},
{actual_completion: {_gte: "${moment()
{actual_completion: {_gte: "${dayjs()
.startOf("month")
.startOf("day")
.toISOString()}"}}, {actual_completion: {_lte: "${moment()
.toISOString()}"}}, {actual_completion: {_lte: "${dayjs()
.endOf("month")
.endOf("day")
.toISOString()}"}}
@@ -61,10 +61,10 @@ _and:[
{_and: [
{date_invoiced: {_is_null: true}},
{actual_completion: {_is_null: true}}
{scheduled_completion: {_gte: "${moment()
{scheduled_completion: {_gte: "${dayjs()
.startOf("month")
.startOf("day")
.toISOString()}"}}, {scheduled_completion: {_lte: "${moment()
.toISOString()}"}}, {scheduled_completion: {_lte: "${dayjs()
.endOf("month")
.endOf("day")
.toISOString()}"}}

View File

@@ -4,7 +4,7 @@ import {
PauseCircleOutlined,
} from "@ant-design/icons";
import { Card, Space, Table, Tooltip } from "antd";
import moment from "moment";
import dayjs from "../../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
@@ -49,14 +49,14 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
start: moment(item.start).format("hh:mm a"),
start: dayjs(item.start).format("hh:mm a"),
title: item.title,
};
appt.push(i);
}
});
appt.sort(function (a, b) {
return new moment(a.start) - new moment(b.start);
return new dayjs(a.start) - new dayjs(b.start);
});
const columns = [
@@ -189,7 +189,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
return (
<Card
title={t("dashboard.titles.scheduledintoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
date: dayjs().startOf("day").format("MM/DD/YYYY"),
})}
{...cardProps}
>
@@ -209,9 +209,9 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
}
export const DashboardScheduledInTodayGql = `
scheduled_in_today: appointments(where: {start: {_gte: "${moment()
scheduled_in_today: appointments(where: {start: {_gte: "${dayjs()
.startOf("day")
.toISOString()}", _lte: "${moment()
.toISOString()}", _lte: "${dayjs()
.endOf("day")
.toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) {
canceled

View File

@@ -4,7 +4,7 @@ import {
PauseCircleOutlined,
} from "@ant-design/icons";
import { Card, Space, Table, Tooltip } from "antd";
import moment from "moment";
import dayjs from "../../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
@@ -22,12 +22,13 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
if (!data.scheduled_out_today)
return <DashboardRefreshRequired {...cardProps} />;
data.scheduled_out_today.forEach((item) => {
item.scheduled_completion= moment(item.scheduled_completion).format("hh:mm a")
});
data.scheduled_out_today.sort(function (a, b) {
return new Date(a.scheduled_completion) - new Date(b.scheduled_completion);
});
const filteredScheduledOutToday = data.scheduled_out_today.map((item) => {
return {
...item,
scheduled_completion: dayjs(item.scheduled_completion).format("hh:mm a"),
timestamp: dayjs(item.scheduled_completion).valueOf(),
}
}).sort((a, b) => a.timestamp - b.timestamp);
const columns = [
{
@@ -159,7 +160,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
return (
<Card
title={t("dashboard.titles.scheduledouttoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
date: dayjs().startOf("day").format("MM/DD/YYYY"),
})}
{...cardProps}
>
@@ -171,7 +172,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id"
style={{ height: "85%" }}
dataSource={data.scheduled_out_today}
dataSource={filteredScheduledOutToday}
/>
</div>
</Card>
@@ -183,8 +184,8 @@ export const DashboardScheduledOutTodayGql = `
date_invoiced: {_is_null: true},
ro_number: {_is_null: false},
voided: {_eq: false},
scheduled_completion: {_gte: "${moment().startOf("day").toISOString()}",
_lte: "${moment().endOf("day").toISOString()}"}}) {
scheduled_completion: {_gte: "${dayjs().startOf("day").toISOString()}",
_lte: "${dayjs().endOf("day").toISOString()}"}}) {
alt_transport
clm_no
jobid: id

View File

@@ -1,9 +1,10 @@
import Icon, { SyncOutlined } from "@ant-design/icons";
import { gql, useMutation, useQuery } from "@apollo/client";
import { Button, Dropdown, Menu, PageHeader, Space, notification } from "antd";
import { Button, Dropdown, Space, notification } from "antd";
import {PageHeader} from "@ant-design/pro-layout";
import i18next from "i18next";
import _ from "lodash";
import moment from "moment";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import { useTranslation } from "react-i18next";
@@ -122,19 +123,15 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
[data]
);
const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
{Object.keys(componentList).map((key) => (
<Menu.Item
key={key}
value={key}
disabled={existingLayoutKeys.includes(key)}
>
{componentList[key].label}
</Menu.Item>
))}
</Menu>
);
const menuItems = Object.keys(componentList).map((key) => ({
key: key,
label: componentList[key].label,
value: key,
disabled: existingLayoutKeys.includes(key),
}));
const menu = {items: menuItems, onClick: handleAddComponent};
if (error) return <AlertComponent message={error.message} type="error" />;
@@ -146,7 +143,7 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
<Dropdown menu={menu} trigger={["click"]}>
<Button>{t("dashboard.actions.addcomponent")}</Button>
</Dropdown>
</Space>
@@ -276,7 +273,7 @@ const componentList = {
},
ScheduleInToday: {
label: i18next.t("dashboard.titles.scheduledintoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
date: dayjs().startOf("day").format("MM/DD/YYYY"),
}),
component: DashboardScheduledInToday,
gqlFragment: DashboardScheduledInTodayGql,
@@ -287,7 +284,7 @@ const componentList = {
},
ScheduleOutToday: {
label: i18next.t("dashboard.titles.scheduledouttoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
date: dayjs().startOf("day").format("MM/DD/YYYY"),
}),
component: DashboardScheduledOutToday,
gqlFragment: DashboardScheduledOutTodayGql,
@@ -310,10 +307,10 @@ const createDashboardQuery = (state) => {
${componentBasedAdditions || ""}
monthly_sales: jobs(where: {_and: [
{ voided: {_eq: false}},
{date_invoiced: {_gte: "${moment()
{date_invoiced: {_gte: "${dayjs()
.startOf("month")
.startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.toISOString()}"}}, {date_invoiced: {_lte: "${dayjs()
.endOf("month")
.endOf("day")
.toISOString()}"}}]}) {

View File

@@ -6,13 +6,13 @@ export default function DataLabel({
hideIfNull,
children,
vertical,
visible = true,
open = true,
valueStyle = {},
valueClassName,
onValueClick,
...props
}) {
if (!visible || (hideIfNull && !!!children)) return null;
if (!open || (hideIfNull && !!!children)) return null;
return (
<div {...props} style={{ display: "flex" }}>

View File

@@ -18,7 +18,7 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkVehicles);
export function DmsCdkVehicles({ bodyshop, form, socket, job }) {
const [visible, setVisible] = useState(false);
const [open, setOpen] = useState(false);
const [selectedModel, setSelectedModel] = useState(null);
const { t } = useTranslation();
@@ -51,14 +51,14 @@ export function DmsCdkVehicles({ bodyshop, form, socket, job }) {
<>
<Modal
width={"90%"}
visible={visible}
onCancel={() => setVisible(false)}
open={open}
onCancel={() => setOpen(false)}
onOk={() => {
form.setFieldsValue({
dms_make: selectedModel.makecode,
dms_model: selectedModel.modelcode,
});
setVisible(false);
setOpen(false);
}}
>
{error && <AlertComponent error={error.message} />}
@@ -90,7 +90,7 @@ export function DmsCdkVehicles({ bodyshop, form, socket, job }) {
</Modal>
<Button
onClick={() => {
setVisible(true);
setOpen(true);
callSearch({
variables: {
search: job && job.v_model_desc && job.v_model_desc.substr(0, 3),

View File

@@ -21,29 +21,29 @@ export default connect(
export function DmsCustomerSelector({ bodyshop }) {
const { t } = useTranslation();
const [customerList, setcustomerList] = useState([]);
const [visible, setVisible] = useState(false);
const [open, setOpen] = useState(false);
const [selectedCustomer, setSelectedCustomer] = useState(null);
const [dmsType, setDmsType] = useState("cdk");
socket.on("cdk-select-customer", (customerList, callback) => {
setVisible(true);
setOpen(true);
setDmsType("cdk");
setcustomerList(customerList);
});
socket.on("pbs-select-customer", (customerList, callback) => {
setVisible(true);
setOpen(true);
setDmsType("pbs");
setcustomerList(customerList);
});
const onUseSelected = () => {
setVisible(false);
setOpen(false);
socket.emit(`${dmsType}-selected-customer`, selectedCustomer);
setSelectedCustomer(null);
};
const onUseGeneric = () => {
setVisible(false);
setOpen(false);
socket.emit(
`${dmsType}-selected-customer`,
bodyshop.cdk_configuration.generic_customer_number
@@ -52,7 +52,7 @@ export function DmsCustomerSelector({ bodyshop }) {
};
const onCreateNew = () => {
setVisible(false);
setOpen(false);
socket.emit(`${dmsType}-selected-customer`, null);
setSelectedCustomer(null);
};
@@ -114,7 +114,7 @@ export function DmsCustomerSelector({ bodyshop }) {
},
];
if (!visible) return null;
if (!open) return null;
return (
<Col span={24}>
<Table

View File

@@ -1,5 +1,5 @@
import { Divider, Space, Tag, Timeline } from "antd";
import moment from "moment";
import dayjs from "../../utils/day";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -22,20 +22,22 @@ export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents);
export function DmsLogEvents({ socket, logs, bodyshop }) {
return (
<Timeline pending
reverse={true}
>
{logs.map((log, idx) => (
<Timeline.Item key={idx} color={LogLevelHierarchy(log.level)}>
<Timeline
pending
reverse={true}
items={logs.map((log, idx) => ({
key: idx,
color: LogLevelHierarchy(log.level),
children: (
<Space wrap align="start" style={{}}>
<Tag color={LogLevelHierarchy(log.level)}>{log.level}</Tag>
<span>{moment(log.timestamp).format("MM/DD/YYYY HH:mm:ss")}</span>
<span>{dayjs(log.timestamp).format("MM/DD/YYYY HH:mm:ss")}</span>
<Divider type="vertical" />
<span>{log.message}</span>
</Space>
</Timeline.Item>
))}
</Timeline>
),
}))}
/>
);
}

View File

@@ -1,27 +1,26 @@
import { DeleteFilled, DownOutlined } from "@ant-design/icons";
import {DeleteFilled, DownOutlined} from "@ant-design/icons";
import {
Button,
Card,
Divider,
Dropdown,
Form,
Input,
InputNumber,
Menu,
Select,
Space,
Statistic,
Switch,
Typography,
Button,
Card,
Divider,
Dropdown,
Form,
Input,
InputNumber,
Select,
Space,
Statistic,
Switch,
Typography,
} from "antd";
import Dinero from "dinero.js";
import moment from "moment";
import dayjs from "../../utils/day";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { determineDmsType } from "../../pages/dms/dms.container";
import { selectBodyshop } from "../../redux/user/user.selectors";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {determineDmsType} from "../../pages/dms/dms.container";
import {selectBodyshop} from "../../redux/user/user.selectors";
import i18n from "../../translations/i18n";
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
@@ -30,440 +29,425 @@ import CurrencyInput from "../form-items-formatted/currency-form-item.component"
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
const [form] = Form.useForm();
const { t } = useTranslation();
export function DmsPostForm({bodyshop, socket, job, logsRef}) {
const [form] = Form.useForm();
const {t} = useTranslation();
const handlePayerSelect = (value, index) => {
form.setFieldsValue({
payers: form.getFieldValue("payers").map((payer, mapIndex) => {
if (index !== mapIndex) return payer;
const cdkPayer =
bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.find((i) => i.name === value);
const handlePayerSelect = (value, index) => {
form.setFieldsValue({
payers: form.getFieldValue("payers").map((payer, mapIndex) => {
if (index !== mapIndex) return payer;
const cdkPayer =
bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.find((i) => i.name === value);
if (!cdkPayer) return payer;
if (!cdkPayer) return payer;
return {
...cdkPayer,
dms_acctnumber: cdkPayer.dms_acctnumber,
controlnumber: job && job[cdkPayer.control_type],
};
}),
});
};
const handleFinish = (values) => {
socket.emit(`${determineDmsType(bodyshop)}-export-job`, {
jobid: job.id,
txEnvelope: values,
});
console.log(logsRef);
if (logsRef) {
console.log("executing", logsRef);
logsRef.curent &&
logsRef.current.scrollIntoView({
behavior: "smooth",
return {
...cdkPayer,
dms_acctnumber: cdkPayer.dms_acctnumber,
controlnumber: job && job[cdkPayer.control_type],
};
}),
});
}
};
};
return (
<Card title={t("jobs.labels.dms.postingform")}>
<Form
form={form}
layout="vertical"
onFinish={handleFinish}
initialValues={{
story: `${t("jobs.labels.dms.defaultstory", {
ro_number: job.ro_number,
ownr_nm: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
job.ownr_co_nm || ""
}`.trim(),
ins_co_nm: job.ins_co_nm || "N/A",
clm_po: `${job.clm_no ? `${job.clm_no} ` : ""}${
job.po_number || ""
}`,
}).trim()}.${
job.area_of_damage && job.area_of_damage.impact1
? " " +
t("jobs.labels.dms.damageto", {
area_of_damage:
(job.area_of_damage && job.area_of_damage.impact1) ||
"UNKNOWN",
})
: ""
}`.substr(0, 239),
inservicedate: moment("2019-01-01"),
}}
>
<LayoutFormRow grow>
<Form.Item
name="journal"
label={t("jobs.fields.dms.journal")}
initialValue={
bodyshop.cdk_configuration &&
bodyshop.cdk_configuration.default_journal
}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="kmin"
label={t("jobs.fields.kmin")}
initialValue={job && job.kmin}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber disabled />
</Form.Item>
<Form.Item
name="kmout"
label={t("jobs.fields.kmout")}
initialValue={job && job.kmout}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber disabled />
</Form.Item>
</LayoutFormRow>
const handleFinish = (values) => {
socket.emit(`${determineDmsType(bodyshop)}-export-job`, {
jobid: job.id,
txEnvelope: values,
});
console.log(logsRef);
if (logsRef) {
console.log("executing", logsRef);
logsRef.curent &&
logsRef.current.scrollIntoView({
behavior: "smooth",
});
}
};
{bodyshop.cdk_dealerid && (
<div>
<LayoutFormRow style={{ justifyContent: "center" }} grow>
<Form.Item
name="dms_make"
label={t("jobs.fields.dms.dms_make")}
rules={[
{
required: true,
},
]}
>
<Input disabled />
</Form.Item>
<Form.Item
name="dms_model"
label={t("jobs.fields.dms.dms_model")}
rules={[
{
required: true,
},
]}
>
<Input disabled />
</Form.Item>
<Form.Item
name="inservicedate"
label={t("jobs.fields.dms.inservicedate")}
>
<FormDatePicker />
</Form.Item>
</LayoutFormRow>
<Space>
<DmsCdkMakes form={form} socket={socket} job={job} />
<DmsCdkMakesRefetch />
<Form.Item
name="dms_unsold"
label={t("jobs.fields.dms.dms_unsold")}
initialValue={false}
>
<Switch />
</Form.Item>
<Form.Item
name="dms_model_override"
label={t("jobs.fields.dms.dms_model_override")}
initialValue={false}
>
<Switch />
</Form.Item>
</Space>
</div>
)}
<Form.Item
name="story"
label={t("jobs.fields.dms.story")}
rules={[
{
required: true,
},
]}
>
<Input.TextArea maxLength={240} />
</Form.Item>
<Divider />
<Space size="large" wrap align="center">
<Statistic
title={t("jobs.fields.ded_amt")}
value={Dinero(
job.job_totals.totals.custPayable.deductible
).toFormat()}
/>
<Statistic
title={t("jobs.labels.total_cust_payable")}
value={Dinero(job.job_totals.totals.custPayable.total).toFormat()}
/>
<Statistic
title={t("jobs.labels.net_repairs")}
value={Dinero(job.job_totals.totals.net_repairs).toFormat()}
/>
</Space>
<Form.List name={["payers"]}>
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Space wrap>
<Form.Item
label={t("jobs.fields.dms.payer.name")}
key={`${index}name`}
name={[field.name, "name"]}
rules={[
{
required: true,
},
]}
>
<Select
style={{ minWidth: "15rem" }}
onSelect={(value) => handlePayerSelect(value, index)}
>
{bodyshop.cdk_configuration &&
bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.map((payer) => (
<Select.Option key={payer.name}>
{payer.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.dms_acctnumber")}
key={`${index}dms_acctnumber`}
name={[field.name, "dms_acctnumber"]}
rules={[
{
required: true,
},
]}
>
<Input disabled />
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.amount")}
key={`${index}amount`}
name={[field.name, "amount"]}
rules={[
{
required: true,
},
]}
>
<CurrencyInput min={0} />
</Form.Item>
<Form.Item
label={
<div>
{t("jobs.fields.dms.payer.controlnumber")}{" "}
<Dropdown
overlay={
<Menu>
{bodyshop.cdk_configuration.controllist &&
bodyshop.cdk_configuration.controllist.map(
(key, idx) => (
<Menu.Item
key={idx}
onClick={() => {
form.setFieldsValue({
payers: form
.getFieldValue("payers")
.map((row, mapIndex) => {
if (index !== mapIndex)
return row;
return {
...row,
controlnumber:
key.controlnumber,
};
}),
});
}}
>
{key.name}
</Menu.Item>
)
)}
</Menu>
}
>
<a href=" #" onClick={(e) => e.preventDefault()}>
<DownOutlined />
</a>
</Dropdown>
</div>
return (
<Card title={t("jobs.labels.dms.postingform")}>
<Form
form={form}
layout="vertical"
onFinish={handleFinish}
initialValues={{
story: `${t("jobs.labels.dms.defaultstory", {
ro_number: job.ro_number,
ownr_nm: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
job.ownr_co_nm || ""
}`.trim(),
ins_co_nm: job.ins_co_nm || "N/A",
clm_po: `${job.clm_no ? `${job.clm_no} ` : ""}${
job.po_number || ""
}`,
}).trim()}.${
job.area_of_damage && job.area_of_damage.impact1
? " " +
t("jobs.labels.dms.damageto", {
area_of_damage:
(job.area_of_damage && job.area_of_damage.impact1) ||
"UNKNOWN",
})
: ""
}`.slice(0, 239),
inservicedate: dayjs("2019-01-01"),
}}
>
<LayoutFormRow grow>
<Form.Item
name="journal"
label={t("jobs.fields.dms.journal")}
initialValue={
bodyshop.cdk_configuration &&
bodyshop.cdk_configuration.default_journal
}
key={`${index}controlnumber`}
name={[field.name, "controlnumber"]}
rules={[
{
required: true,
},
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
>
<Input/>
</Form.Item>
<Form.Item
name="kmin"
label={t("jobs.fields.kmin")}
initialValue={job && job.kmin}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber disabled/>
</Form.Item>
<Form.Item
name="kmout"
label={t("jobs.fields.kmout")}
initialValue={job && job.kmout}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber disabled/>
</Form.Item>
</LayoutFormRow>
<Form.Item shouldUpdate>
{() => {
const payers = form.getFieldValue("payers");
const row = payers && payers[index];
const cdkPayer =
bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.find(
(i) => i && row && i.name === row.name
);
if (
i18n.exists(`jobs.fields.${cdkPayer?.control_type}`)
)
return (
<div>
{cdkPayer &&
t(`jobs.fields.${cdkPayer?.control_type}`)}
</div>
);
else if (
i18n.exists(
`jobs.fields.dms.control_type.${cdkPayer?.control_type}`
)
) {
return (
<div>
{cdkPayer &&
t(
`jobs.fields.dms.control_type.${cdkPayer?.control_type}`
)}
</div>
);
} else {
return null;
}
}}
</Form.Item>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
</Space>
</Form.Item>
))}
<Form.Item>
<Button
disabled={!(fields.length < 3)}
onClick={() => {
if (fields.length < 3) add();
}}
style={{ width: "100%" }}
>
{t("jobs.actions.dms.addpayer")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Form.Item shouldUpdate>
{() => {
//Perform Calculation to determine discrepancy.
let totalAllocated = Dinero();
const payers = form.getFieldValue("payers");
payers &&
payers.forEach((payer) => {
totalAllocated = totalAllocated.add(
Dinero({ amount: Math.round((payer?.amount || 0) * 100) })
);
});
const totals =
socket.allocationsSummary &&
socket.allocationsSummary.reduce(
(acc, val) => {
return {
totalSale: acc.totalSale.add(Dinero(val.sale)),
totalCost: acc.totalCost.add(Dinero(val.cost)),
};
},
{
totalSale: Dinero(),
totalCost: Dinero(),
}
);
const discrep = totals
? totals.totalSale.subtract(totalAllocated)
: Dinero();
return (
<Space size="large" wrap align="center">
<Statistic
title={t("jobs.labels.subtotal")}
value={(totals ? totals.totalSale : Dinero()).toFormat()}
/>
<Typography.Title>-</Typography.Title>
<Statistic
title={t("jobs.labels.dms.totalallocated")}
value={totalAllocated.toFormat()}
/>
<Typography.Title>=</Typography.Title>
<Statistic
title={t("jobs.labels.dms.notallocated")}
valueStyle={{
color: discrep.getAmount() === 0 ? "green" : "red",
}}
value={discrep.toFormat()}
/>
<Button
disabled={
!socket.allocationsSummary || discrep.getAmount() !== 0
}
htmlType="submit"
{bodyshop.cdk_dealerid && (
<div>
<LayoutFormRow style={{justifyContent: "center"}} grow>
<Form.Item
name="dms_make"
label={t("jobs.fields.dms.dms_make")}
rules={[
{
required: true,
},
]}
>
<Input disabled/>
</Form.Item>
<Form.Item
name="dms_model"
label={t("jobs.fields.dms.dms_model")}
rules={[
{
required: true,
},
]}
>
<Input disabled/>
</Form.Item>
<Form.Item
name="inservicedate"
label={t("jobs.fields.dms.inservicedate")}
>
<FormDatePicker/>
</Form.Item>
</LayoutFormRow>
<Space>
<DmsCdkMakes form={form} socket={socket} job={job}/>
<DmsCdkMakesRefetch/>
<Form.Item
name="dms_unsold"
label={t("jobs.fields.dms.dms_unsold")}
initialValue={false}
>
<Switch/>
</Form.Item>
<Form.Item
name="dms_model_override"
label={t("jobs.fields.dms.dms_model_override")}
initialValue={false}
>
<Switch/>
</Form.Item>
</Space>
</div>
)}
<Form.Item
name="story"
label={t("jobs.fields.dms.story")}
rules={[
{
required: true,
},
]}
>
{t("jobs.actions.dms.post")}
</Button>
</Space>
);
}}
</Form.Item>
</Form>
</Card>
);
<Input.TextArea maxLength={240}/>
</Form.Item>
<Divider/>
<Space size="large" wrap align="center">
<Statistic
title={t("jobs.fields.ded_amt")}
value={Dinero(
job.job_totals.totals.custPayable.deductible
).toFormat()}
/>
<Statistic
title={t("jobs.labels.total_cust_payable")}
value={Dinero(job.job_totals.totals.custPayable.total).toFormat()}
/>
<Statistic
title={t("jobs.labels.net_repairs")}
value={Dinero(job.job_totals.totals.net_repairs).toFormat()}
/>
</Space>
<Form.List name={["payers"]}>
{(fields, {add, remove}) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Space wrap>
<Form.Item
label={t("jobs.fields.dms.payer.name")}
key={`${index}name`}
name={[field.name, "name"]}
rules={[
{
required: true,
},
]}
>
<Select
style={{minWidth: "15rem"}}
onSelect={(value) => handlePayerSelect(value, index)}
>
{bodyshop.cdk_configuration &&
bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.map((payer) => (
<Select.Option key={payer.name}>
{payer.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.dms_acctnumber")}
key={`${index}dms_acctnumber`}
name={[field.name, "dms_acctnumber"]}
rules={[
{
required: true,
},
]}
>
<Input disabled/>
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.amount")}
key={`${index}amount`}
name={[field.name, "amount"]}
rules={[
{
required: true,
},
]}
>
<CurrencyInput min={0}/>
</Form.Item>
<Form.Item
label={
<div>
{t("jobs.fields.dms.payer.controlnumber")}{" "}
<Dropdown menu={{
items: bodyshop.cdk_configuration.controllist?.map((key, idx) => ({
key: idx,
label: key.name,
onClick: () => {
form.setFieldsValue({
payers: form.getFieldValue("payers").map((row, mapIndex) => {
if (index !== mapIndex) return row;
return {
...row,
controlnumber: key.controlnumber,
};
}),
});
},
}))
}}>
<a href=" #" onClick={(e) => e.preventDefault()}>
<DownOutlined/>
</a>
</Dropdown>
</div>
}
key={`${index}controlnumber`}
name={[field.name, "controlnumber"]}
rules={[
{
required: true,
},
]}
>
<Input/>
</Form.Item>
<Form.Item shouldUpdate>
{() => {
const payers = form.getFieldValue("payers");
const row = payers && payers[index];
const cdkPayer =
bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.find(
(i) => i && row && i.name === row.name
);
if (
i18n.exists(`jobs.fields.${cdkPayer?.control_type}`)
)
return (
<div>
{cdkPayer &&
t(`jobs.fields.${cdkPayer?.control_type}`)}
</div>
);
else if (
i18n.exists(
`jobs.fields.dms.control_type.${cdkPayer?.control_type}`
)
) {
return (
<div>
{cdkPayer &&
t(
`jobs.fields.dms.control_type.${cdkPayer?.control_type}`
)}
</div>
);
} else {
return null;
}
}}
</Form.Item>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
</Space>
</Form.Item>
))}
<Form.Item>
<Button
disabled={!(fields.length < 3)}
onClick={() => {
if (fields.length < 3) add();
}}
style={{width: "100%"}}
>
{t("jobs.actions.dms.addpayer")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Form.Item shouldUpdate>
{() => {
//Perform Calculation to determine discrepancy.
let totalAllocated = Dinero();
const payers = form.getFieldValue("payers");
payers &&
payers.forEach((payer) => {
totalAllocated = totalAllocated.add(
Dinero({amount: Math.round((payer?.amount || 0) * 100)})
);
});
const totals =
socket.allocationsSummary &&
socket.allocationsSummary.reduce(
(acc, val) => {
return {
totalSale: acc.totalSale.add(Dinero(val.sale)),
totalCost: acc.totalCost.add(Dinero(val.cost)),
};
},
{
totalSale: Dinero(),
totalCost: Dinero(),
}
);
const discrep = totals
? totals.totalSale.subtract(totalAllocated)
: Dinero();
return (
<Space size="large" wrap align="center">
<Statistic
title={t("jobs.labels.subtotal")}
value={(totals ? totals.totalSale : Dinero()).toFormat()}
/>
<Typography.Title>-</Typography.Title>
<Statistic
title={t("jobs.labels.dms.totalallocated")}
value={totalAllocated.toFormat()}
/>
<Typography.Title>=</Typography.Title>
<Statistic
title={t("jobs.labels.dms.notallocated")}
valueStyle={{
color: discrep.getAmount() === 0 ? "green" : "red",
}}
value={discrep.toFormat()}
/>
<Button
disabled={
!socket.allocationsSummary || discrep.getAmount() !== 0
}
htmlType="submit"
>
{t("jobs.actions.dms.post")}
</Button>
</Space>
);
}}
</Form.Item>
</Form>
</Card>
);
}

View File

@@ -60,9 +60,10 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
markerArea.current = new markerjs2.MarkerArea(imgRef.current);
// attach an event handler to assign annotated image back to our image element
markerArea.current.addCloseEventListener((closeEvent) => {});
markerArea.current.addRenderEventListener((dataUrl) => {
markerArea.current.addEventListener("close", (closeEvent) => {});
markerArea.current.addEventListener("render", (event) => {
const dataUrl = event.dataUrl;
imgRef.current.src = dataUrl;
markerArea.current.close();
triggerUpload(dataUrl);

View File

@@ -4,7 +4,7 @@ import queryString from "query-string";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router";
import { useLocation } from "react-router-dom";
import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries";
import { GET_DOCUMENT_BY_PK } from "../../graphql/documents.queries";
import { setBodyshop } from "../../redux/user/user.actions";

View File

@@ -57,7 +57,7 @@ export const uploadToCloudinary = async (
let tags = `${bodyshop.imexshopid},${
tagsArray ? tagsArray.map((tag) => `${tag},`) : ""
}`;
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
// let eager = import.meta.env.VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
//Get the signed url.
@@ -96,14 +96,14 @@ export const uploadToCloudinary = async (
formData.append("upload_preset", upload_preset);
formData.append("api_key", process.env.REACT_APP_CLOUDINARY_API_KEY);
formData.append("api_key", import.meta.env.VITE_APP_CLOUDINARY_API_KEY);
formData.append("public_id", public_id);
formData.append("tags", tags);
formData.append("timestamp", timestamp);
formData.append("signature", signature);
const cloudinaryUploadResponse = await cleanAxios.post(
`${process.env.REACT_APP_CLOUDINARY_ENDPOINT_API}/${DetermineFileType(
`${import.meta.env.VITE_APP_CLOUDINARY_ENDPOINT_API}/${DetermineFileType(
fileType
)}/upload`,
formData,

View File

@@ -1,267 +1,262 @@
import { UploadOutlined, UserAddOutlined } from "@ant-design/icons";
import {
Button,
Divider,
Dropdown,
Form,
Input,
Menu,
Select,
Space,
Tabs,
Upload,
} from "antd";
import {UploadOutlined, UserAddOutlined} from "@ant-design/icons";
import {Button, Divider, Dropdown, Form, Input, Select, Space, Tabs, Upload,} from "antd";
import _ from "lodash";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectEmailConfig } from "../../redux/email/email.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { CreateExplorerLinkForJob } from "../../utils/localmedia";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectEmailConfig} from "../../redux/email/email.selectors";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
import {CreateExplorerLinkForJob} from "../../utils/localmedia";
import EmailDocumentsComponent from "../email-documents/email-documents.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
emailConfig: selectEmailConfig,
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
emailConfig: selectEmailConfig,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(EmailOverlayComponent);
export function EmailOverlayComponent({
emailConfig,
form,
selectedMediaState,
bodyshop,
currentUser,
}) {
const { t } = useTranslation();
const handleClick = ({ item, key, keyPath }) => {
const email = item.props.value;
form.setFieldsValue({
to: _.uniq([
...form.getFieldValue("to"),
...(typeof email === "string" ? [email] : email),
]),
});
};
const handle_CC_Click = ({ item, key, keyPath }) => {
const email = item.props.value;
form.setFieldsValue({
cc: _.uniq([
...(form.getFieldValue("cc") || ""),
...(typeof email === "string" ? [email] : email),
]),
});
};
emailConfig,
form,
selectedMediaState,
bodyshop,
currentUser,
}) {
const {t} = useTranslation();
const handleClick = ({item, key, keyPath}) => {
const email = item.props.value;
form.setFieldsValue({
to: _.uniq([
...form.getFieldValue("to"),
...(typeof email === "string" ? [email] : email),
]),
});
};
const handle_CC_Click = ({item, key, keyPath}) => {
const email = item.props.value;
form.setFieldsValue({
cc: _.uniq([
...(form.getFieldValue("cc") || ""),
...(typeof email === "string" ? [email] : email),
]),
});
};
const menu = (
<div>
<Menu onClick={handleClick}>
{bodyshop.employees
.filter((e) => e.user_email)
.map((e, idx) => (
<Menu.Item value={e.user_email} key={idx}>
{`${e.first_name} ${e.last_name}`}
</Menu.Item>
))}
{bodyshop.md_to_emails.map((e, idx) => (
<Menu.Item value={e.emails} key={idx + "group"}>
{e.label}
</Menu.Item>
))}
</Menu>
</div>
);
const emailsToMenu = {
items: [
...bodyshop.employees
.filter((e) => e.user_email)
.map((e, idx) => ({
key: idx,
label: `${e.first_name} ${e.last_name}`,
value: e.user_email,
})),
...bodyshop.md_to_emails.map((e, idx) => ({
key: idx + "group",
label: e.label,
value: e.emails,
})),
],
onClick: handleClick,
};
const menuCC = {
items: [
...bodyshop.employees
.filter((e) => e.user_email)
.map((e, idx) => ({
key: idx,
label: `${e.first_name} ${e.last_name}`,
value: e.user_email,
})),
...bodyshop.md_to_emails.map((e, idx) => ({
key: idx + "group",
label: e.label,
value: e.emails,
})),
],
onClick: handle_CC_Click,
};
const menuCC = (
<div>
<Menu onClick={handle_CC_Click}>
{bodyshop.employees
.filter((e) => e.user_email)
.map((e, idx) => (
<Menu.Item value={e.user_email} key={idx}>
{`${e.first_name} ${e.last_name}`}
</Menu.Item>
))}
{bodyshop.md_to_emails.map((e, idx) => (
<Menu.Item value={e.emails} key={idx + "group"}>
{e.label}
</Menu.Item>
))}
</Menu>
</div>
);
return (
<div>
<Form.Item
label={t("emails.fields.from")}
name="from"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select>
<Select.Option key={currentUser.email}>
{currentUser.email}
</Select.Option>
<Select.Option key={bodyshop.email}>{bodyshop.email}</Select.Option>
{bodyshop.md_from_emails &&
bodyshop.md_from_emails.map((e) => (
<Select.Option key={e}>{e}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={
<Space>
{t("emails.fields.to")}
<Dropdown overlay={menu}>
<a
className="ant-dropdown-link"
href=" #"
onClick={(e) => e.preventDefault()}
>
<UserAddOutlined />
</a>
</Dropdown>
</Space>
}
name="to"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select mode="tags" tokenSeparators={[",", ";"]} />
</Form.Item>
<Form.Item
label={
<Space>
{t("emails.fields.cc")}
<Dropdown overlay={menuCC}>
<a
className="ant-dropdown-link"
href=" #"
onClick={(e) => e.preventDefault()}
>
<UserAddOutlined />
</a>
</Dropdown>
</Space>
}
name="cc"
>
<Select mode="tags" tokenSeparators={[",", ";"]} />
</Form.Item>
<Form.Item
label={t("emails.fields.subject")}
name="subject"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Divider>{t("emails.labels.preview")}</Divider>
{bodyshop.attach_pdf_to_email && (
<strong>{t("emails.labels.pdfcopywillbeattached")}</strong>
)}
<Form.Item shouldUpdate>
{() => {
return (
<div
style={{
padding: "1rem",
backgroundColor: "lightgray",
borderLeft: "6px solid #2196F3",
}}
dangerouslySetInnerHTML={{ __html: form.getFieldValue("html") }}
/>
);
}}
</Form.Item>
<Tabs>
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
<EmailDocumentsComponent
selectedMediaState={selectedMediaState}
form={form}
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments">
{bodyshop.uselocalmediaserver && emailConfig.jobid && (
<a href={CreateExplorerLinkForJob({ jobid: emailConfig.jobid })}>
<Button>{t("documents.labels.openinexplorer")}</Button>
</a>
)}
<Form.Item
name="fileList"
valuePropName="fileList"
getValueFromEvent={(e) => {
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
}}
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
const totalSize = value.reduce(
(acc, val) => (acc = acc + val.size),
0
);
const limit =
10485760 - new Blob([form.getFieldValue("html")]).size;
if (totalSize > limit) {
return Promise.reject(t("general.errors.sizelimit"));
}
return Promise.resolve();
},
}),
]}
>
<Upload.Dragger
beforeUpload={Upload.LIST_IGNORE}
multiple
listType="picture-card"
return (
<div>
<Form.Item
label={t("emails.fields.from")}
name="from"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
</Upload.Dragger>
</Form.Item>
</Tabs.TabPane>
</Tabs>
</div>
);
<Select>
<Select.Option key={currentUser.email}>
{currentUser.email}
</Select.Option>
<Select.Option key={bodyshop.email}>{bodyshop.email}</Select.Option>
{bodyshop.md_from_emails &&
bodyshop.md_from_emails.map((e) => (
<Select.Option key={e}>{e}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={
<Space>
{t("emails.fields.to")}
<Dropdown menu={emailsToMenu}>
<a
className="ant-dropdown-link"
href=" #"
onClick={(e) => e.preventDefault()}
>
<UserAddOutlined/>
</a>
</Dropdown>
</Space>
}
name="to"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select mode="tags" tokenSeparators={[",", ";"]}/>
</Form.Item>
<Form.Item
label={
<Space>
{t("emails.fields.cc")}
<Dropdown menu={menuCC}>
<a
className="ant-dropdown-link"
href=" #"
onClick={(e) => e.preventDefault()}
>
<UserAddOutlined/>
</a>
</Dropdown>
</Space>
}
name="cc"
>
<Select mode="tags" tokenSeparators={[",", ";"]}/>
</Form.Item>
<Form.Item
label={t("emails.fields.subject")}
name="subject"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input/>
</Form.Item>
<Divider>{t("emails.labels.preview")}</Divider>
{bodyshop.attach_pdf_to_email && (
<strong>{t("emails.labels.pdfcopywillbeattached")}</strong>
)}
<Form.Item shouldUpdate>
{() => {
return (
<div
style={{
padding: "1rem",
backgroundColor: "lightgray",
borderLeft: "6px solid #2196F3",
}}
dangerouslySetInnerHTML={{__html: form.getFieldValue("html")}}
/>
);
}}
</Form.Item>
<Tabs
defaultActiveKey="documents"
items={[
{
key: "documents",
label: t("emails.labels.documents"),
children: (
<EmailDocumentsComponent
selectedMediaState={selectedMediaState}
form={form}
/>
),
},
{
key: "attachments",
label: t("emails.labels.attachments"),
children: (
<>
{bodyshop.uselocalmediaserver && emailConfig.jobid && (
<a href={CreateExplorerLinkForJob({jobid: emailConfig.jobid})}>
<Button>{t("documents.labels.openinexplorer")}</Button>
</a>
)}
<Form.Item
name="fileList"
valuePropName="fileList"
getValueFromEvent={(e) => {
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
}}
rules={[
({getFieldValue}) => ({
validator(rule, value) {
const totalSize = value.reduce(
(acc, val) => (acc = acc + val.size),
0
);
const limit =
10485760 - new Blob([form.getFieldValue("html")]).size;
if (totalSize > limit) {
return Promise.reject(t("general.errors.sizelimit"));
}
return Promise.resolve();
},
}),
]}
>
<Upload.Dragger
beforeUpload={Upload.LIST_IGNORE}
multiple
listType="picture-card"
>
<>
<p className="ant-upload-drag-icon">
<UploadOutlined/>
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
</Upload.Dragger>
</Form.Item>
</>
),
},
]}
/>
</div>
);
}

View File

@@ -174,7 +174,7 @@ export function EmailOverlayContainer({
return (
<Modal
destroyOnClose={true}
visible={modalVisible}
open={modalVisible}
maskClosable={false}
width={"80%"}
onOk={() => form.submit()}
@@ -186,7 +186,6 @@ export function EmailOverlayContainer({
okText={t("general.actions.send")}
okButtonProps={{
loading: sending,
disabled:
selectedMedia &&
(selectedMedia

View File

@@ -7,7 +7,7 @@ import { selectCurrentUser } from "../../redux/user/user.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import moment from "moment";
import dayjs from "../../utils/day";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -27,16 +27,16 @@ export function EmailTestComponent({ currentUser, setEmailOptions }) {
variables: {
...(values.start
? {
start: moment(values.start).startOf("day").format("YYYY-MM-DD"),
start: dayjs(values.start).startOf("day").format("YYYY-MM-DD"),
}
: {}),
...(values.end
? { end: moment(values.end).endOf("day").format("YYYY-MM-DD") }
? { end: dayjs(values.end).endOf("day").format("YYYY-MM-DD") }
: {}),
...(values.start
? { starttz: moment(values.start).startOf("day") }
? { starttz: dayjs(values.start).startOf("day") }
: {}),
...(values.end ? { endtz: moment(values.end).endOf("day") } : {}),
...(values.end ? { endtz: dayjs(values.end).endOf("day") } : {}),
...(values.id ? { id: values.id } : {}),
},

View File

@@ -0,0 +1,250 @@
import React, {useCallback, useEffect, useRef, useState} from "react";
import {Button, Card, Checkbox, Col, Form, Input, Modal, notification, Row} from "antd";
import Markdown from "react-markdown";
import {createStructuredSelector} from "reselect";
import {selectCurrentEula, selectCurrentUser} from "../../redux/user/user.selectors";
import {connect} from "react-redux";
import {FormDatePicker} from "../form-date-picker/form-date-picker.component";
import {INSERT_EULA_ACCEPTANCE} from "../../graphql/user.queries";
import {useMutation} from "@apollo/client";
import {acceptEula} from "../../redux/user/user.actions";
import {useTranslation} from "react-i18next";
import day from '../../utils/day';
import './eula.styles.scss';
const Eula = ({currentEula, currentUser, acceptEula}) => {
const [formReady, setFormReady] = useState(false);
const [hasEverScrolledToBottom, setHasEverScrolledToBottom] = useState(false);
const [insertEulaAcceptance] = useMutation(INSERT_EULA_ACCEPTANCE);
const [form] = Form.useForm();
const markdownCardRef = useRef(null);
const {t} = useTranslation();
const [api, contextHolder] = notification.useNotification();
const handleScroll = useCallback((e) => {
const bottom = e.target.scrollHeight - 100 <= e.target.scrollTop + e.target.clientHeight;
if (bottom && !hasEverScrolledToBottom) {
setHasEverScrolledToBottom(true);
} else if (e.target.scrollHeight <= e.target.clientHeight && !hasEverScrolledToBottom) {
setHasEverScrolledToBottom(true);
}
}, [hasEverScrolledToBottom, setHasEverScrolledToBottom]);
useEffect(() => {
handleScroll({target: markdownCardRef.current});
}, [handleScroll]);
const handleChange = useCallback(() => {
form.validateFields({validateOnly: true})
.then(() => setFormReady(hasEverScrolledToBottom))
.catch(() => setFormReady(false));
}, [form, hasEverScrolledToBottom]);
useEffect(() => {
handleChange();
}, [handleChange, hasEverScrolledToBottom, form]);
const onFinish = async ({acceptTerms, ...formValues}) => {
const eulaId = currentEula.id;
const useremail = currentUser.email;
try {
const {accepted_terms, ...otherFormValues} = formValues;
// Trim the values of the fields before submitting
const trimmedFormValues = Object.entries(otherFormValues).reduce((acc, [key, value]) => {
acc[key] = typeof value === 'string' ? value.trim() : value;
return acc;
}, {});
await insertEulaAcceptance({
variables: {
eulaAcceptance: {
eulaid: eulaId,
useremail,
...otherFormValues,
...trimmedFormValues,
date_accepted: new Date(),
}
}
});
acceptEula();
} catch (err) {
api.error({
message: t('eula.errors.acceptance.message'),
description: t('eula.errors.acceptance.description'),
placement: 'bottomRight',
duration: 5000,
});
console.log(`${t('eula.errors.acceptance.message')}`);
console.dir({
message: err.message,
stack: err.stack,
});
}
};
return (
<>
{contextHolder}
<Modal
title={t('eula.titles.modal')}
className='eula-modal'
width={'100vh'}
open={currentEula}
footer={() => (
<Button
className='eula-accept-button'
form='tosForm'
type="primary"
size='large'
htmlType="submit"
disabled={!formReady}
children={t('eula.buttons.accept')}
/>
)}
closable={false}
>
<Card type='inner' className='eula-markdown-card' onScroll={handleScroll} ref={markdownCardRef}>
<div id='markdowndiv' className='eula-markdown-div'>
<Markdown children={currentEula?.content?.replace(/\\n|\\r|\\n\\r|\\r\\n/g, '\n')}/>
</div>
</Card>
<EulaFormComponent form={form} handleChange={handleChange} onFinish={onFinish} t={t}/>
{!hasEverScrolledToBottom && (
<Card className='eula-never-scrolled' type='inner'>
<h3>{t('eula.content.never_scrolled')}</h3>
</Card>
)}
</Modal>
</>
)
}
const EulaFormComponent = ({form, handleChange, onFinish, t}) => (
<Card type='inner' title={t('eula.titles.upper_card')} style={{marginTop: '10px'}}>
<Form id='tosForm' onChange={handleChange} onFinish={onFinish} form={form}>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t('eula.labels.first_name')}
name="first_name"
rules={[{
required: true,
validator: (_, value) =>
value.trim() !== '' ? Promise.resolve() : Promise.reject(new Error(t('eula.messages.first_name'))),
},]}
>
<Input placeholder={t('eula.labels.first_name')}
aria-label={t('eula.labels.first_name')}/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t('eula.labels.last_name')}
name="last_name"
rules={[{
required: true,
validator: (_, value) =>
value.trim() !== '' ? Promise.resolve() : Promise.reject(new Error(t('eula.messages.last_name'))),
}]}
>
<Input placeholder={t('eula.labels.last_name')}
aria-label={t('eula.labels.last_name')}/>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t('eula.labels.business_name')}
name="business_name"
rules={[{
required: true,
validator: (_, value) =>
value.trim() !== '' ? Promise.resolve() : Promise.reject(new Error(t('eula.messages.business_name'))),
}]}
>
<Input placeholder={t('eula.labels.business_name')} aria-label={t('eula.labels.business_name')}/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t('eula.labels.phone_number')}
name="phone_number"
rules={[
{
pattern: /^(\+\d{1,2}\s?)?1?-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/,
message: t('eula.messages.phone_number'),
}
]}
>
<Input placeholder={t('eula.labels.phone_number')}
aria-label={t('eula.labels.phone_number')}/>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t('eula.labels.address')}
name="address"
>
<Input placeholder={t('eula.labels.address')} aria-label={t('eula.labels.address')}/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t('eula.labels.date_accepted')}
name="date_accepted"
rules={[
{
required: true,
validator: (_, value) => {
if (day(value).isSame(day(), 'day')) {
return Promise.resolve();
}
return Promise.reject(new Error(t('eula.messages.date_accepted')));
}
},
]}
>
<FormDatePicker onChange={handleChange} onlyToday aria-label={t('eula.labels.date_accepted')}/>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={24}>
<Form.Item
name="accepted_terms"
valuePropName="checked"
rules={[
{
required: true,
validator: (_, value) =>
value ? Promise.resolve() : Promise.reject(new Error(t('eula.messages.accepted_terms'))),
},
]}
>
<Checkbox aria-label={t('eula.labels.accepted_terms')}>{t('eula.labels.accepted_terms')}</Checkbox>
</Form.Item>
</Col>
</Row>
</Form>
</Card>
);
const mapStateToProps = createStructuredSelector({
currentEula: selectCurrentEula,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
acceptEula: () => dispatch(acceptEula()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Eula);

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