Compare commits

..

170 Commits
master ... vite

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
444 changed files with 27629 additions and 56751 deletions

View File

@@ -2,9 +2,8 @@ version: 2.1
orbs: orbs:
#snyk: snyk/snyk@0.0.8 #snyk: snyk/snyk@0.0.8
#cypress: cypress-io/cypress@1.23.0 #cypress: cypress-io/cypress@1.23.0
aws-s3: circleci/aws-s3@2.0.0 aws-s3: circleci/aws-s3@4.0.0
eb: circleci/aws-elastic-beanstalk@1.0.2 eb: circleci/aws-elastic-beanstalk@2.0.1
jira: circleci/jira@1.3.1
jobs: jobs:
api-deploy: api-deploy:
docker: docker:
@@ -18,7 +17,6 @@ jobs:
eb status --verbose eb status --verbose
eb deploy eb deploy
eb status eb status
- jira/notify
hasura-migrate: hasura-migrate:
docker: docker:
@@ -42,7 +40,7 @@ jobs:
app-build: app-build:
docker: docker:
- image: cimg/node:16.15.0 - image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client working_directory: ~/repo/client
steps: steps:
@@ -58,7 +56,26 @@ jobs:
from: build from: build
to: "s3://imex-online-production/" to: "s3://imex-online-production/"
arguments: "--exclude '*.map'" 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: test-hasura-migrate:
docker: docker:
@@ -83,7 +100,7 @@ jobs:
test-app-build: test-app-build:
docker: docker:
- image: cimg/node:16.15.0 - image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client working_directory: ~/repo/client
steps: steps:
@@ -99,7 +116,27 @@ jobs:
from: build from: build
to: "s3://imex-online-test/" to: "s3://imex-online-test/"
arguments: "--exclude '*.map'" 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: admin-app-build:
docker: docker:
@@ -134,10 +171,18 @@ jobs:
workflows: workflows:
deploy_and_build: deploy_and_build:
jobs: jobs:
- api-deploy:
filters:
branches:
only: master
- app-build: - app-build:
filters: filters:
branches: branches:
only: master only: master
- app-beta-build:
filters:
branches:
only: master-beta
- hasura-migrate: - hasura-migrate:
secret: ${HASURA_PROD_SECRET} secret: ${HASURA_PROD_SECRET}
filters: filters:
@@ -147,6 +192,10 @@ workflows:
filters: filters:
branches: branches:
only: test only: test
- test-app-beta-build:
filters:
branches:
only: test-beta
- test-hasura-migrate: - test-hasura-migrate:
secret: ${HASURA_TEST_SECRET} secret: ${HASURA_TEST_SECRET}
filters: filters:

View File

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

View File

@@ -1,189 +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.
For filters and sorters, valid types include (`type` key in the schema):
- string (default)
- number
- bool or boolean
- date
## Special Notes
- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected
## High level Schema Overview
```javascript
const schema = {
"filters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more filters
],
"sorters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more sorters
],
"dates": {
// This is not yet implemented and will be added in a future release
}
}
```
## Filters
Filters effect the where clause of the graphQL query. They are used to filter the data returned from the server.
A note on special notation used in the `name` field.
## Reflection
Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file.
```json
{
"name": "jobs.status",
"translation": "jobs.fields.status",
"label": "Status",
"type": "string",
"reflector": {
"type": "internal",
"name": "special.job_statuses"
}
}
```
in this example, a reflector with the type 'internal' (all types at the moment require this, and it is used for future functionality), with a name of `special.job_statuses`
The following cases are available
- `special.job_statuses` - This will reflect the statuses of the jobs table `bodyshop.md_ro_statuses.statuses'`
- `special.cost_centers` - This will reflect the cost centers `bodyshop.md_responsibility_centers.costs`
- `special.categories` - This will reflect the categories `bodyshop.md_categories`
- `special.insurance_companies` - This will reflect the insurance companies `bodyshop.md_ins_cos`'
- `special.employee_teams` - This will reflect the employee teams `bodyshop.employee_teams`
- `special.employees` - This will reflect the employees `bodyshop.employees`
- `special.first_names` - This will reflect the first names `bodyshop.employees`
- `special.last_names` - This will reflect the last names `bodyshop.employees`
- `special.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources`
- `special.class`- This will reflect the class `bodyshop.md_classes`
- `special.lost_sale_reasons` - This will reflect the lost sale reasons `bodyshop.md_lost_sale_reasons`
- `special.alt_transports` - This will reflect the alternative transports `bodyshop.appt_alt_transport`
- `special.payment_types` - This will reflect the payment types `bodyshop.md_payment_types`
- `special.payment_payers` - This is a special case with a key value set of [Customer, Insurance]
### Path without brackets, multi level
`"name": "jobs.joblines.mod_lb_hrs",`
This will produce a where clause at the `joblines` level of the graphQL query,
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}, mod_lb_hrs: {_lt: 3}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
### Path with brackets,top level
`"name": "[jobs].joblines.mod_lb_hrs",`
This will produce a where clause at the `jobs` level of the graphQL query.
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}, joblines: {mod_lb_hrs: {_gt: 4}}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
## Known Caveats
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs`
is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
- The type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' and is case-sensitive.
- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used.
- Do not add the ability to filter things that are already filtered as part of the original query, this would be
redundant and could cause issues.
- Do not add the ability to filter on things like FK constraints, must like the above example.
- *INHERITANCE CAVEAT* If you have a filters file on a parent report that has a child that you do not want the filters inherited from, you must place a blank filters file (valid json so {}) on the child report level. This will than fetch the child filters, which are empty and move along, versus inheriting the parent filters.
## Sorters
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting,
a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs`
would be added at the `joblines` level.
- Most of the reports currently do sorting on a template level, this will need to change to actually see the results
using the sorters.
### Default Sorters
- A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves.
- The `order` key is the order in which the sorters are applied, and the `direction` key is the direction of the sort, either `asc` or `desc`.
```json
{
"name": "jobs.joblines.mod_lb_hrs",
"translation": "jobs.joblines.mod_lb_hrs_1",
"label": "mod_lb_hrs_1",
"type": "number",
"default": {
"order": 1,
"direction": "asc"
}
}
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
REACT_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
REACT_APP_GA_CODE=231099835 VITE_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"} 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"}
REACT_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
REACT_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
REACT_APP_CLOUDINARY_API_KEY=957865933348715 VITE_APP_CLOUDINARY_API_KEY=957865933348715
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250 VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4' VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
REACT_APP_AXIOS_BASE_API_URL=http://localhost:4000 VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
REACT_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc 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 // craco.config.js
const TerserPlugin = require("terser-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin");
const CracoLessPlugin = require("craco-less"); 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 = { module.exports = {
plugins: [ plugins: [
// { // {
// plugin: SentryWebpackPlugin, // plugin: SentryWebpackPlugin,
// options: { // options: {
// // sentry-cli configuration // // sentry-cli configuration
// authToken: // authToken:
// "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260", // "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
// org: "snapt-software", // org: "snapt-software",
// project: "imexonline", // project: "imexonline",
// release: process.env.REACT_APP_GIT_SHA, // release: process.env.REACT_APP_GIT_SHA,
//
// // webpack-specific configuration // // webpack-specific configuration
// include: ".", // include: ".",
// ignore: ["node_modules", "webpack.config.js"], // ignore: ["node_modules", "webpack.config.js"],
// }, // },
// }, // },
{ {
plugin: CracoLessPlugin, plugin: CracoLessPlugin,
options: { options: {
lessLoaderOptions: { lessLoaderOptions: {
lessOptions: { lessOptions: {
modifyVars: { modifyVars: {...v4Token},
...(process.env.NODE_ENV === "development" javascriptEnabled: true,
? { "@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 }",
}, },
javascriptEnabled: true,
},
}, },
}, ],
}, webpack: {
], configure: (webpackConfig) => {
webpack: { return {
configure: (webpackConfig) => ({ ...webpackConfig,
...webpackConfig, // Required for Dev Server
optimization: { devServer: {
...webpackConfig.optimization, ...webpackConfig.devServer,
// Workaround for CircleCI bug caused by the number of CPUs shown allowedHosts: 'all',
// https://github.com/facebook/create-react-app/issues/8320 },
minimizer: webpackConfig.optimization.minimizer.map((item) => { optimization: {
if (item instanceof TerserPlugin) { ...webpackConfig.optimization,
item.options.parallel = 2; // 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; return item;
}), }),
}, },
}), };
}, },
devtool: "source-map", },
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"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <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="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#002366" /> <meta name="theme-color" content="#002366" />
<meta name="description" content="ImEX Online" /> <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="logo192.png" /> <link rel="apple-touch-icon" href="public/logo192.png" />
<script type="text/javascript"> <script type="text/javascript">
window.$crisp = []; window.$crisp = [];
window.CRISP_WEBSITE_ID = "36724f62-2eb0-4b29-9cdd-9905fb99913e"; 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 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/ 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. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.
@@ -82,5 +82,7 @@
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<script type="module" src="src/index.js"></script>
</body> </body>
</html> </html>

View File

@@ -8872,13 +8872,13 @@
│ ├─ email: luis@luisrudge.net │ ├─ email: luis@luisrudge.net
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes │ ├─ 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 │ └─ 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 │ ├─ licenses: CC0-1.0
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-visible │ ├─ repository: https://github.com/jonathantneal/postcss-focus-open
│ ├─ publisher: Jonathan Neal │ ├─ publisher: Jonathan Neal
│ ├─ email: jonathantneal@hotmail.com │ ├─ email: jonathantneal@hotmail.com
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible │ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible/LICENSE.md │ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open/LICENSE.md
├─ postcss-focus-within@3.0.0 ├─ postcss-focus-within@3.0.0
│ ├─ licenses: CC0-1.0 │ ├─ licenses: CC0-1.0
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-within │ ├─ 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", "name": "bodyshop",
"version": "0.2.1", "version": "0.2.1",
"engines": {
"node": "18.18.2"
},
"type": "commonjs",
"private": true, "private": true,
"proxy": "http://localhost:4000", "proxy": "http://localhost:4000",
"dependencies": { "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", "@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^7.0.0", "@craco/craco": "^7.1.0",
"@fingerprintjs/fingerprintjs": "^3.4.2", "@fingerprintjs/fingerprintjs": "^4.2.2",
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.1.0",
"@sentry/cli": "^2.27.0", "@sentry/cli": "^2.27.0",
"@sentry/react": "^7.99.0", "@sentry/react": "^7.99.0",
"@sentry/tracing": "^7.40.0", "@sentry/tracing": "^7.99.0",
"@splitsoftware/splitio-react": "^1.8.1", "@splitsoftware/splitio-react": "^1.11.0",
"@tanem/react-nprogress": "^5.0.8", "@tanem/react-nprogress": "^5.0.51",
"antd": "^4.24.8", "@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-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0", "apollo-link-sentry": "^3.3.0",
"axios": "^1.3.4", "axios": "^1.6.7",
"craco-less": "^2.0.0", "consola": "^3.2.3",
"dayjs": "^1.11.10",
"dayjs-business-days2": "^1.2.2",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^16.0.1", "dotenv": "^16.4.1",
"enquire-js": "^0.2.1", "enquire-js": "^0.2.1",
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"esbuild": "^0.20.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"firebase": "^9.17.1", "firebase": "^10.7.2",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"i18next": "^22.4.10", "i18next": "^23.8.1",
"i18next-browser-languagedetector": "^7.0.1", "i18next-browser-languagedetector": "^7.0.2",
"jsoneditor": "^9.9.0", "jsoneditor": "^10.0.0",
"jsreport-browser-client-dist": "^1.3.0", "jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.21", "libphonenumber-js": "^1.10.54",
"logrocket": "^3.0.1", "logrocket": "^7.0.0",
"markerjs2": "^2.28.1", "markerjs2": "^2.32.0",
"moment-business-days": "^1.2.0",
"moment-timezone": "^0.5.41",
"normalize-url": "^8.0.0", "normalize-url": "^8.0.0",
"phone": "^3.1.35", "phone": "^3.1.42",
"preval.macro": "^5.0.0", "preval.macro": "^5.0.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^7.1.3", "query-string": "^8.1.0",
"rc-queue-anim": "^2.0.0", "rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6", "rc-scroll-anim": "^2.7.6",
"react": "^17.0.2", "react": "^18.2.0",
"react-big-calendar": "^1.6.8", "react-big-calendar": "^1.8.7",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^4.1.1", "react-cookie": "^7.0.2",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-drag-listview": "^0.2.1", "react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.0", "react-grid-gallery": "^1.0.0",
"react-grid-layout": "^1.3.4", "react-grid-layout": "1.3.4",
"react-i18next": "^12.2.0", "react-i18next": "^14.0.1",
"react-icons": "^4.7.1", "react-icons": "^5.0.1",
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.4.3", "react-intersection-observer": "^9.5.3",
"react-number-format": "^5.1.3", "react-markdown": "^9.0.1",
"react-redux": "^8.0.5", "react-number-format": "^5.1.4",
"react-resizable": "^3.0.4", "react-redux": "^9.1.0",
"react-router-dom": "^5.3.0", "react-resizable": "^3.0.5",
"react-scripts": "^5.0.1", "react-router-dom": "^6.21.2",
"react-sticky": "^6.0.3", "react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5", "react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.3", "react-virtualized": "^9.22.5",
"recharts": "^2.4.3", "recharts": "^2.11.0",
"redux": "^4.2.1", "redux": "^5.0.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-saga": "^1.2.2", "redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4", "redux-state-sync": "^3.1.4",
"reselect": "^4.1.7", "reselect": "^5.1.0",
"sass": "^1.58.3", "sass": "^1.70.0",
"socket.io-client": "^4.6.1", "socket.io-client": "^4.7.4",
"styled-components": "^5.3.6", "styled-components": "^6.1.8",
"subscriptions-transport-ws": "^0.11.0", "subscriptions-transport-ws": "^0.11.0",
"web-vitals": "^2.1.4", "terser-webpack-plugin": "^5.3.10",
"workbox-background-sync": "^6.5.3", "vite-plugin-svgr": "^4.2.0",
"workbox-broadcast-update": "^6.5.3", "web-vitals": "^3.5.2",
"workbox-cacheable-response": "^6.5.3", "workbox-core": "^7.0.0",
"workbox-core": "^6.5.3", "workbox-expiration": "^7.0.0",
"workbox-expiration": "^6.5.3", "workbox-navigation-preload": "^7.0.0",
"workbox-google-analytics": "^6.5.3", "workbox-precaching": "^7.0.0",
"workbox-navigation-preload": "^6.5.3", "workbox-routing": "^7.0.0",
"workbox-precaching": "^6.5.3", "workbox-strategies": "^7.0.0",
"workbox-range-requests": "^6.5.3",
"workbox-routing": "^6.5.3",
"workbox-strategies": "^6.5.3",
"workbox-streams": "^6.5.3",
"yauzl": "^2.10.0" "yauzl": "^2.10.0"
}, },
"scripts": { "scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'", "analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "craco start", "start": "vite",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build && npm run sentry:sourcemaps", "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: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!'", "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", "test": "cypress open",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .", "madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
"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" "sentry:sourcemaps": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build"
}, },
"eslintConfig": { "eslintConfig": {
@@ -122,12 +131,29 @@
"react-error-overlay": "6.0.9" "react-error-overlay": "6.0.9"
}, },
"devDependencies": { "devDependencies": {
"@sentry/webpack-plugin": "^1.20.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@testing-library/cypress": "^8.0.3", "@babel/preset-react": "^7.23.3",
"cypress": "^10.3.1", "@emotion/babel-plugin": "^11.11.0",
"eslint-plugin-cypress": "^2.12.1", "@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", "react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6", "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 # @firebase/logger
This package serves as the base of all logging in the JS SDK. Any logging that 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. module.
## Basic Usage ## 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. a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices" 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) feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey 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-dir-pseudo-class@5.0.0
- postcss-double-position-gradients@1.0.0 - postcss-double-position-gradients@1.0.0
- postcss-env-function@2.0.2 - 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-focus-within@3.0.0
- postcss-gap-properties@2.0.0 - postcss-gap-properties@2.0.0
- postcss-image-set-function@3.0.1 - postcss-image-set-function@3.0.1
@@ -1699,7 +1699,7 @@ This package contains the following license and notice below:
# @firebase/logger # @firebase/logger
This package serves as the base of all logging in the JS SDK. Any logging that 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. module.
## Basic Usage ## 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. a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices" 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) feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the extent that warranties are provided), that licensees may convey the

View File

@@ -1,47 +1,53 @@
import { ApolloProvider } from "@apollo/client"; import {ApolloProvider} from "@apollo/client";
import { SplitFactory, SplitSdk } from "@splitsoftware/splitio-react"; import {SplitFactoryProvider, SplitSdk,} from '@splitsoftware/splitio-react';
import { ConfigProvider } from "antd"; import {ConfigProvider} from "antd";
import enLocale from "antd/es/locale/en_US"; 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 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 GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import client from "../utils/GraphQLClient"; import client from "../utils/GraphQLClient";
import App from "./App"; import App from "./App";
import * as Sentry from "@sentry/react"; 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() { function AppContainer() {
const { t } = useTranslation(); const {t} = useTranslation();
return ( return (
<ApolloProvider client={client}> <ApolloProvider client={client}>
<ConfigProvider <ConfigProvider
//componentSize="small" //componentSize="small"
input={{ autoComplete: "new-password" }} input={{autoComplete: "new-password"}}
locale={enLocale} locale={enLocale}
form={{ theme={themeProvider}
validateMessages: { form={{
// eslint-disable-next-line no-template-curly-in-string validateMessages: {
required: t("general.validation.required", { label: "${label}" }), // eslint-disable-next-line no-template-curly-in-string
}, required: t("general.validation.required", {label: "${label}"}),
}} },
> }}
<GlobalLoadingBar /> >
<SplitFactory factory={factory}> <GlobalLoadingBar/>
<App /> <SplitFactoryProvider factory={factory}>
</SplitFactory> <App/>
</ConfigProvider> </SplitFactoryProvider>
</ApolloProvider> </ConfigProvider>
); </ApolloProvider>
);
} }
export default Sentry.withProfiler(AppContainer); export default Sentry.withProfiler(AppContainer);

View File

@@ -1,132 +1,156 @@
import { useClient } from "@splitsoftware/splitio-react"; import {useSplitClient} from "@splitsoftware/splitio-react";
import { Button, Result } from "antd"; import {Button, Result} from "antd";
import LogRocket from "logrocket"; import LogRocket from "logrocket";
import React, { lazy, Suspense, useEffect } from "react"; import React, {lazy, Suspense, useEffect, useState} from "react";
import { useTranslation } from "react-i18next"; import {useTranslation} from "react-i18next";
import { connect } from "react-redux"; import {connect} from "react-redux";
import { Route, Switch } from "react-router-dom"; import {Route, Routes} from "react-router-dom";
import { createStructuredSelector } from "reselect"; import {createStructuredSelector} from "reselect";
import DocumentEditorContainer from "../components/document-editor/document-editor.container"; import DocumentEditorContainer from "../components/document-editor/document-editor.container";
import ErrorBoundary from "../components/error-boundary/error-boundary.component"; import ErrorBoundary from "../components/error-boundary/error-boundary.component";
//Component Imports //Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component"; import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page"; import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page"; import LandingPage from "../pages/landing/landing.page";
import TechPageContainer from "../pages/tech/tech.page.container"; import TechPageContainer from "../pages/tech/tech.page.container";
import { setOnline } from "../redux/application/application.actions"; import {setOnline} from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors"; import {selectOnline} from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions"; import {checkUserSession} from "../redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "../redux/user/user.selectors"; import {selectBodyshop, selectCurrentEula, selectCurrentUser,} from "../redux/user/user.selectors";
import PrivateRoute from "../utils/private-route"; import PrivateRoute from "../components/PrivateRoute";
import "./App.styles.scss"; import "./App.styles.scss";
import handleBeta from "../utils/betaHandler";
import Eula from "../components/eula/eula.component";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component")); const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component")
);
const ManagePage = lazy(() => import("../pages/manage/manage.page.container")); const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page")); const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page")); const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container")); const MobilePaymentContainer = lazy(() =>
import("../pages/mobile-payment/mobile-payment.container")
);
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
online: selectOnline, online: selectOnline,
bodyshop: selectBodyshop bodyshop: selectBodyshop,
currentEula: selectCurrentEula
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()), checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)) setOnline: (isOnline) => dispatch(setOnline(isOnline)),
}); });
export function App({ bodyshop, checkUserSession, currentUser, online, setOnline }) { export function App({bodyshop, checkUserSession, currentUser, online, setOnline, currentEula}) {
const client = useClient(); 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(); useEffect(() => {
// console.log("Breakpoints:", b); if (!navigator.onLine) {
setOnline(false);
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>
} }
/>
);
return ( checkUserSession();
<Switch> }, [checkUserSession, setOnline]);
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}>
<ErrorBoundary> //const b = Grid.useBreakpoint();
<Route exact path="/" component={LandingPage} /> // console.log("Breakpoints:", b);
</ErrorBoundary>
<ErrorBoundary> // Associate event listeners, memoize to prevent multiple listeners being added
<Route exact path="/signin" component={SignInPage} /> useEffect(() => {
</ErrorBoundary> const offlineListener = (e) => {
<ErrorBoundary> setOnline(false);
<Route exact path="/resetpassword" component={ResetPassword} /> }
</ErrorBoundary>
<ErrorBoundary> const onlineListener = (e) => {
<Route exact path="/csi/:surveyId" component={CsiPage} /> setOnline(true);
</ErrorBoundary> }
<ErrorBoundary>
<Route exact path="/disclaimer" component={DisclaimerPage} /> if (!listenersAdded) {
</ErrorBoundary> console.log('Added events for offline and online');
<ErrorBoundary> window.addEventListener("offline", offlineListener);
<Route exact path="/mp/:paymentIs" component={MobilePaymentContainer} /> window.addEventListener("online", onlineListener);
</ErrorBoundary> setListenersAdded(true);
<ErrorBoundary> }
<PrivateRoute isAuthorized={currentUser.authorized} path="/manage" component={ManagePage} />
</ErrorBoundary> return () => {
<ErrorBoundary> window.removeEventListener("offline", offlineListener);
<PrivateRoute isAuthorized={currentUser.authorized} path="/tech" component={TechPageContainer} /> window.removeEventListener("online", onlineListener);
</ErrorBoundary> }
<ErrorBoundary> }, [setOnline, listenersAdded]);
<PrivateRoute isAuthorized={currentUser.authorized} path="/edit" component={DocumentEditorContainer} />
</ErrorBoundary> useEffect(() => {
</Suspense> if (currentUser.authorized && bodyshop) {
</Switch> 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>
);
} }
export default connect(mapStateToProps, mapDispatchToProps)(App); export default connect(mapStateToProps, mapDispatchToProps)(App);

View File

@@ -1,6 +1,10 @@
//Global Styles. //Global Styles.
@import "react-big-calendar/lib/sass/styles"; @import "react-big-calendar/lib/sass/styles";
.ant-menu-item-divider {
border-bottom: 1px solid #74695c !important;
}
.imex-table-header { .imex-table-header {
display: flex; display: flex;
flex-wrap: wrap; 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 { .job-line-manual {
color: tomato; color: tomato;
font-style: italic; font-style: italic;
} }
td.ant-table-column-sort {
background-color: transparent;
}
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td { .ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
background-color: #f4f4f4; 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

@@ -1,21 +1,21 @@
import { Card, Checkbox, Input, Space, Table } from "antd"; import { Input, Table, Checkbox, Card, Space } from "antd";
import queryString from "query-string";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import PayableExportButton from "../payable-export-button/payable-export-button.component"; import PayableExportButton from "../payable-export-button/payable-export-button.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component"; import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import { DateFormatter } from "../../utils/DateFormatter";
import queryString from "query-string";
import { logImEXEvent } from "../../firebase/firebase.utils";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -138,6 +138,7 @@ export function AccountingPayablesTableComponent({
title: t("exportlogs.labels.attempts"), title: t("exportlogs.labels.attempts"),
dataIndex: "attempts", dataIndex: "attempts",
key: "attempts", key: "attempts",
render: (text, record) => ( render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} /> <ExportLogsCountDisplay logs={record.exportlogs} />
), ),
@@ -146,6 +147,8 @@ export function AccountingPayablesTableComponent({
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => ( render: (text, record) => (
<PayableExportButton <PayableExportButton
billId={record.id} billId={record.id}

View File

@@ -8,16 +8,14 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, { import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import PaymentExportButton from "../payment-export-button/payment-export-button.component"; import PaymentExportButton from "../payment-export-button/payment-export-button.component";
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component"; import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component"; import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -77,11 +75,7 @@ export function AccountingPayablesTableComponent({
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
sorter: (a, b) => sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
alphaSort(
OwnerNameDisplayFunction(a.job),
OwnerNameDisplayFunction(b.job)
),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
@@ -100,9 +94,6 @@ export function AccountingPayablesTableComponent({
title: t("payments.fields.amount"), title: t("payments.fields.amount"),
dataIndex: "amount", dataIndex: "amount",
key: "amount", key: "amount",
sorter: (a, b) => a.amount - b.amount,
sortOrder:
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<CurrencyFormatter>{record.amount}</CurrencyFormatter> <CurrencyFormatter>{record.amount}</CurrencyFormatter>
), ),
@@ -121,21 +112,18 @@ export function AccountingPayablesTableComponent({
title: t("payments.fields.created_at"), title: t("payments.fields.created_at"),
dataIndex: "created_at", dataIndex: "created_at",
key: "created_at", key: "created_at",
sorter: (a, b) => dateSort(a.created_at, b.created_at),
sortOrder:
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<DateTimeFormatter>{record.created_at}</DateTimeFormatter> <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
), ),
}, },
// { {
// title: t("payments.fields.exportedat"), title: t("payments.fields.exportedat"),
// dataIndex: "exportedat", dataIndex: "exportedat",
// key: "exportedat", key: "exportedat",
// render: (text, record) => ( render: (text, record) => (
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter> <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
// ), ),
// }, },
{ {
title: t("exportlogs.labels.attempts"), title: t("exportlogs.labels.attempts"),
dataIndex: "attempts", dataIndex: "attempts",
@@ -149,6 +137,8 @@ export function AccountingPayablesTableComponent({
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => ( render: (text, record) => (
<PaymentExportButton <PaymentExportButton
paymentId={record.id} paymentId={record.id}

View File

@@ -4,19 +4,17 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, dateSort, statusSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component"; import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component"; import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { DateFormatter } from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -65,7 +63,7 @@ export function AccountingReceivablesTableComponent({
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses), sorter: (a, b) => a.status - b.status,
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
}, },
@@ -85,8 +83,7 @@ export function AccountingReceivablesTableComponent({
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
sorter: (a, b) => sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
@@ -106,15 +103,6 @@ export function AccountingReceivablesTableComponent({
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}> <Link to={"/manage/vehicles/" + record.vehicleid}>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { useMutation, useQuery } from "@apollo/client"; import {useMutation, useQuery} from "@apollo/client";
import { Button, Divider, Form, PageHeader, Popconfirm, Space } from "antd"; import {Button, Form, Popconfirm, Space} from "antd";
import moment from "moment"; import dayjs from "../../utils/day";
import queryString from "query-string"; import queryString from "query-string";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect";
import { import {
DELETE_BILL_LINE, DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES, INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE, UPDATE_BILL_LINE
} from "../../graphql/bill-lines.queries"; } from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries"; import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
@@ -26,145 +26,142 @@ import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-galler
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container"; import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillDetailEditReturn from "./bill-detail-edit-return.component"; import BillDetailEditReturn from "./bill-detail-edit-return.component";
import {PageHeader} from "@ant-design/pro-layout";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })), dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({jobid, operation})),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(BillDetailEditcontainer); )(BillDetailEditcontainer);
export function BillDetailEditcontainer({ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) {
setPartsOrderContext, const search = queryString.parse(useLocation().search);
insertAuditTrail,
bodyshop,
}) {
const search = queryString.parse(useLocation().search);
const { t } = useTranslation(); const {t} = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [visible, setVisible] = useState(false); const [open, setOpen] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false); const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL); const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES); const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE); const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE); const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, { const {loading, error, data, refetch} = useQuery(QUERY_BILL_BY_PK, {
variables: { billid: search.billid }, variables: {billid: search.billid},
skip: !!!search.billid, skip: !!!search.billid,
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "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;
}); });
//Find bill lines that were deleted. // ... rest of the code remains the same
const deletedJobLines = [];
data.bills_by_pk.billlines.forEach((a) => { const handleSave = () => {
const matchingRecord = billlines.find((b) => b.id === a.id); //It's got a previously deducted bill line!
if (!matchingRecord) { if (
deletedJobLines.push(a); 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) => { const handleFinish = async (values) => {
updates.push(deleteBillLine({ variables: { id: d.id } })); setUpdateLoading(true);
}); //let adjustmentsToInsert = {};
billlines.forEach((billline) => { const {billlines, upload, ...bill} = values;
const { deductedfromlbr, inventories, jobline, ...il } = billline; const updates = [];
delete il.__typename;
if (il.id) {
updates.push( updates.push(
updateBillLine({ update_bill({
variables: { variables: {billId: search.billid, bill: bill},
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); billlines.forEach((l) => {
delete l.selected;
});
insertAuditTrail({ //Find bill lines that were deleted.
jobid: bill.jobid, const deletedJobLines = [];
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
type: "billupdated",
});
await refetch(); data.bills_by_pk.billlines.forEach((a) => {
form.setFieldsValue(transformData(data)); const matchingRecord = billlines.find((b) => b.id === a.id);
form.resetFields(); if (!matchingRecord) {
setVisible(false); deletedJobLines.push(a);
setUpdateLoading(false); }
}; });
if (error) return <AlertComponent message={error.message} type="error" />; deletedJobLines.forEach((d) => {
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>; updates.push(deleteBillLine({variables: {id: d.id}}));
});
const exported = data && data.bills_by_pk && data.bills_by_pk.exported; billlines.forEach((billline) => {
const isinhouse = data && data.bills_by_pk && data.bills_by_pk.isinhouse; 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 ( return (
<> <>
@@ -181,9 +178,9 @@ export function BillDetailEditcontainer({
<BillDetailEditReturn data={data} /> <BillDetailEditReturn data={data} />
<BillPrintButton billid={search.billid} /> <BillPrintButton billid={search.billid} />
<Popconfirm <Popconfirm
visible={visible} open={open}
onConfirm={() => form.submit()} onConfirm={() => form.submit()}
onCancel={() => setVisible(false)} onCancel={() => setOpen(false)}
okButtonProps={{ loading: updateLoading }} okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")} title={t("bills.labels.editadjwarning")}
> >
@@ -208,47 +205,47 @@ export function BillDetailEditcontainer({
initialValues={transformData(data)} initialValues={transformData(data)}
layout="vertical" layout="vertical"
> >
<BillFormContainer form={form} billEdit disabled={exported} disableInHouse={isinhouse}/> <BillFormContainer form={form} billEdit disabled={exported} />
<Divider orientation="left">{t("general.labels.media")}</Divider>
{bodyshop.uselocalmediaserver ? ( {bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery <JobsDocumentsLocalGallery
job={{ id: data ? data.bills_by_pk.jobid : null }} job={{id: data ? data.bills_by_pk.jobid : null}}
invoice_number={data ? data.bills_by_pk.invoice_number : null} invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null} vendorid={data ? data.bills_by_pk.vendorid : null}
/> />
) : ( ) : (
<JobDocumentsGallery <JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null} jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid} billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []} documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch} billsCallback={refetch}
/> />
)}
</Form>
</>
)} )}
</Form>
</> </>
)} );
</>
);
} }
const transformData = (data) => { const transformData = (data) => {
return data return data
? { ? {
...data.bills_by_pk, ...data.bills_by_pk,
billlines: data.bills_by_pk.billlines.map((i) => { billlines: data.bills_by_pk.billlines.map((i) => {
return { return {
...i, ...i,
joblineid: !!i.joblineid ? i.joblineid : "noline", joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: { applicable_taxes: {
federal: federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false, (i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false, state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false, local: (i.applicable_taxes && i.applicable_taxes.local) || false,
}, },
}; };
}), }),
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null, 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 React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })), dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
@@ -33,10 +33,10 @@ export function BillDetailEditReturn({
disabled, disabled,
}) { }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useHistory(); const history = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [visible, setVisible] = useState(false); const [open, setOpen] = useState(false);
const handleFinish = ({ billlines }) => { const handleFinish = ({ billlines }) => {
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id); const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
@@ -45,7 +45,6 @@ export function BillDetailEditReturn({
actions: {}, actions: {},
context: { context: {
jobId: data.bills_by_pk.jobid, jobId: data.bills_by_pk.jobid,
job: data.bills_by_pk.job,
vendorId: data.bills_by_pk.vendorid, vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id, returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number, invoiceNumber: data.bills_by_pk.invoice_number,
@@ -68,18 +67,18 @@ export function BillDetailEditReturn({
}); });
delete search.billid; delete search.billid;
history.push({ search: queryString.stringify(search) }); history({ search: queryString.stringify(search) });
setVisible(false); setOpen(false);
}; };
useEffect(() => { useEffect(() => {
if (visible === false) form.resetFields(); if (open === false) form.resetFields();
}, [visible, form]); }, [open, form]);
return ( return (
<> <>
<Modal <Modal
visible={visible} open={open}
onCancel={() => setVisible(false)} onCancel={() => setOpen(false)}
destroyOnClose destroyOnClose
title={t("bills.actions.return")} title={t("bills.actions.return")}
onOk={() => form.submit()} onOk={() => form.submit()}
@@ -174,13 +173,9 @@ export function BillDetailEditReturn({
</Form> </Form>
</Modal> </Modal>
<Button <Button
disabled={ disabled={data.bills_by_pk.is_credit_memo || disabled}
data.bills_by_pk.is_credit_memo ||
data.bills_by_pk.isinhouse ||
disabled
}
onClick={() => { onClick={() => {
setVisible(true); setOpen(true);
}} }}
> >
{t("bills.actions.return")} {t("bills.actions.return")}

View File

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

View File

@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")), toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, billid, operation, type }) => insertAuditTrail: ({ jobid, billid, operation }) =>
dispatch(insertAuditTrail({ jobid, billid, operation, type })), dispatch(insertAuditTrail({ jobid, billid, operation })),
}); });
const Templates = TemplateList("job_special"); const Templates = TemplateList("job_special");
@@ -94,7 +94,6 @@ function BillEnterModalContainer({
location, location,
outstanding_returns, outstanding_returns,
inventory, inventory,
federal_tax_exempt,
...remainingValues ...remainingValues
} = values; } = values;
@@ -171,7 +170,6 @@ function BillEnterModalContainer({
mod_lbr_ty: key, mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1), hours: adjustmentsToInsert[key].toFixed(1),
}), }),
type: "jobmodifylbradj",
}); });
}); });
@@ -321,7 +319,6 @@ function BillEnterModalContainer({
operation: AuditTrailMapping.billposted( operation: AuditTrailMapping.billposted(
r1.data.insert_bills.returning[0].invoice_number r1.data.insert_bills.returning[0].invoice_number
), ),
type: "billposted",
}); });
if (enterAgain) { if (enterAgain) {
@@ -349,18 +346,18 @@ function BillEnterModalContainer({
}, [enterAgain, form]); }, [enterAgain, form]);
useEffect(() => { useEffect(() => {
if (billEnterModal.visible) { if (billEnterModal.open) {
form.setFieldsValue(formValues); form.setFieldsValue(formValues);
} else { } else {
form.resetFields(); form.resetFields();
} }
}, [billEnterModal.visible, form, formValues]); }, [billEnterModal.open, form, formValues]);
return ( return (
<Modal <Modal
title={t("bills.labels.new")} title={t("bills.labels.new")}
width={"98%"} width={"98%"}
visible={billEnterModal.visible} open={billEnterModal.open}
okText={t("general.actions.save")} okText={t("general.actions.save")}
keyboard="false" keyboard="false"
onOk={() => form.submit()} onOk={() => form.submit()}

View File

@@ -1,26 +1,16 @@
import Icon, { UploadOutlined } from "@ant-design/icons"; import Icon, {UploadOutlined} from "@ant-design/icons";
import { useApolloClient } from "@apollo/client"; import {useApolloClient} from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react"; import {useSplitTreatments} from "@splitsoftware/splitio-react";
import { import {Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload,} from "antd";
Alert, import dayjs from "../../utils/day";
Divider, import React, {useEffect, useState} from "react";
Form, import {useTranslation} from "react-i18next";
Input, import {MdOpenInNew} from "react-icons/md";
Select, import {connect} from "react-redux";
Space, import {Link} from "react-router-dom";
Statistic, import {createStructuredSelector} from "reselect";
Switch, import {CHECK_BILL_INVOICE_NUMBER} from "../../graphql/bills.queries";
Upload, import {selectBodyshop} from "../../redux/user/user.selectors";
} 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 AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component"; import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
@@ -30,55 +20,38 @@ import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.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({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({}); const mapDispatchToProps = (dispatch) => ({});
export function BillFormComponent({ export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteOptions, lineData, responsibilityCenters, loadLines, billEdit, disableInvNumber, job, loadOutstandingReturns, loadInventory, preferredMake}) {
bodyshop,
disabled,
form,
vendorAutoCompleteOptions,
lineData,
responsibilityCenters,
loadLines,
billEdit,
disableInvNumber,
disableInHouse,
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
);
const handleVendorSelect = (props, opt) => { const {t} = useTranslation();
setDiscount(opt.discount); const client = useApolloClient();
const [discount, setDiscount] = useState(0);
opt && const { treatments: {Extended_Bill_Posting, ClosingPeriod} } = useSplitTreatments({
!billEdit && attributes: {},
loadOutstandingReturns({ names: ["Extended_Bill_Posting", "ClosingPeriod"],
variables: { splitKey: bodyshop.imexshopid,
jobId: form.getFieldValue("jobid"), });
vendorId: opt.value,
},
}); const handleVendorSelect = (props, opt) => {
}; setDiscount(opt.discount);
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
});
};
const handleFederalTaxExemptSwitchToggle = (checked) => { const handleFederalTaxExemptSwitchToggle = (checked) => {
// Early gate // Early gate
@@ -98,256 +71,256 @@ export function BillFormComponent({
if (job) form.validateFields(["is_credit_memo"]); if (job) form.validateFields(["is_credit_memo"]);
}, [job, form]); }, [job, form]);
useEffect(() => { useEffect(() => {
const vendorId = form.getFieldValue("vendorid"); const vendorId = form.getFieldValue("vendorid");
if (vendorId && vendorAutoCompleteOptions) { if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter( const matchingVendors = vendorAutoCompleteOptions.filter(
(v) => v.id === vendorId (v) => v.id === vendorId
); );
if (matchingVendors.length === 1) { if (matchingVendors.length === 1) {
setDiscount(matchingVendors[0].discount); 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 || disableInHouse}
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>
} }
/> }
))} const jobId = form.getFieldValue("jobid");
<LayoutFormRow> if (jobId) {
<Form.Item loadLines({variables: {id: jobId}});
label={t("bills.fields.invoice_number")} if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
name="invoice_number" loadOutstandingReturns({
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: { variables: {
invoice_number: value, jobId: jobId,
vendorid: vendorid, vendorId: vendorId,
}, },
}); });
}
}
if (response.data.bills_aggregate.aggregate.count === 0) { if (vendorId === bodyshop.inhousevendorid && !billEdit) {
return Promise.resolve(); loadInventory();
} else if ( }
response.data.bills_aggregate.nodes.length === 1 && }, [
response.data.bills_aggregate.nodes[0].id === form,
form.getFieldValue("id") billEdit,
) { loadOutstandingReturns,
return Promise.resolve(); loadInventory,
} setDiscount,
return Promise.reject( vendorAutoCompleteOptions,
t("bills.validation.unique_invoice_number") loadLines,
); bodyshop.inhousevendorid,
} else { ]);
return Promise.resolve();
}
},
}),
]}
>
<Input disabled={disabled || disableInvNumber || disableInHouse} />
</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 ( return (
!bodyshop.bill_allow_post_to_closed && <div>
job && <FormFieldsChanged form={form}/>
(job.status === bodyshop.md_ro_statuses.default_invoiced || <Form.Item
job.status === bodyshop.md_ro_statuses.default_exported || style={{display: "none"}}
job.status === bodyshop.md_ro_statuses.default_void) && name="isinhouse"
(value === false || !value) valuePropName="checked"
) { >
return Promise.reject(t("bills.labels.onlycmforinvoiced")); <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(); return Promise.resolve();
}, },
@@ -402,15 +375,15 @@ export function BillFormComponent({
> >
<CurrencyInput min={0} /> <CurrencyInput min={0} />
</Form.Item> </Form.Item>
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( {bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
<Form.Item <Form.Item
span={2} span={2}
label={t("bills.labels.federal_tax_exempt")} label={t("bills.labels.federal_tax_exempt")}
name="federal_tax_exempt" name="federal_tax_exempt"
> >
<Switch onChange={handleFederalTaxExemptSwitchToggle} /> <Switch onChange={handleFederalTaxExemptSwitchToggle} />
</Form.Item> </Form.Item>
) : null} ) : null}
<Form.Item shouldUpdate span={13}> <Form.Item shouldUpdate span={13}>
{() => { {() => {
const values = form.getFieldsValue([ const values = form.getFieldsValue([
@@ -487,56 +460,55 @@ export function BillFormComponent({
</LayoutFormRow> </LayoutFormRow>
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider> <Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
{Extended_Bill_Posting.treatment === "on" ? ( {Extended_Bill_Posting.treatment === "on" ? (
<BillFormLinesExtended <BillFormLinesExtended
lineData={lineData} lineData={lineData}
discount={discount} discount={discount}
form={form} form={form}
responsibilityCenters={responsibilityCenters} responsibilityCenters={responsibilityCenters}
disabled={disabled} disabled={disabled}
/> />
) : ( ) : (
<BillFormLines <BillFormLines
lineData={lineData} lineData={lineData}
discount={discount} discount={discount}
form={form} form={form}
responsibilityCenters={responsibilityCenters} responsibilityCenters={responsibilityCenters}
disabled={disabled} disabled={disabled}
billEdit={billEdit} billEdit={billEdit}
/> />
)} )}
<Divider orientation="left" style={{ display: billEdit ? "none" : null }}>
{t("documents.labels.upload")} <Form.Item
</Divider> name="upload"
<Form.Item label="Upload"
name="upload" style={{display: billEdit ? "none" : null}}
style={{ display: billEdit ? "none" : null }} valuePropName="fileList"
valuePropName="fileList" getValueFromEvent={(e) => {
getValueFromEvent={(e) => { if (Array.isArray(e)) {
if (Array.isArray(e)) { return e;
return e; }
} return e && e.fileList;
return e && e.fileList; }}
}} >
> <Upload.Dragger
<Upload.Dragger multiple={true}
multiple={true} name="logo"
name="logo" beforeUpload={() => false}
beforeUpload={() => false} listType="picture"
listType="picture" >
> <>
<> <p className="ant-upload-drag-icon">
<p className="ant-upload-drag-icon"> <UploadOutlined/>
<UploadOutlined /> </p>
</p> <p className="ant-upload-text">
<p className="ant-upload-text"> Click or drag files to this area to upload.
Click or drag files to this area to upload. </p>
</p> </>
</> </Upload.Dragger>
</Upload.Dragger> </Form.Item>
</Form.Item> </div>
</div> );
);
} }
export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent); export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent);

View File

@@ -1,5 +1,5 @@
import { useLazyQuery, useQuery } from "@apollo/client"; import { useLazyQuery, useQuery } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react"; import {useSplitTreatments} from "@splitsoftware/splitio-react";
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -22,13 +22,12 @@ export function BillFormContainer({
billEdit, billEdit,
disabled, disabled,
disableInvNumber, disableInvNumber,
disableInHouse
}) { }) {
const { Simple_Inventory } = useTreatments( const { treatments: {Simple_Inventory} } = useSplitTreatments({
["Simple_Inventory"], attributes: {},
{}, names: ["Simple_Inventory"],
bodyshop && bodyshop.imexshopid splitKey: bodyshop && bodyshop.imexshopid,
); });
const { data: VendorAutoCompleteData } = useQuery( const { data: VendorAutoCompleteData } = useQuery(
SEARCH_VENDOR_AUTOCOMPLETE, SEARCH_VENDOR_AUTOCOMPLETE,
@@ -58,7 +57,6 @@ export function BillFormContainer({
job={lineData ? lineData.jobs_by_pk : null} job={lineData ? lineData.jobs_by_pk : null}
responsibilityCenters={bodyshop.md_responsibility_centers || null} responsibilityCenters={bodyshop.md_responsibility_centers || null}
disableInvNumber={disableInvNumber} disableInvNumber={disableInvNumber}
disableInHouse={disableInHouse}
loadOutstandingReturns={loadOutstandingReturns} loadOutstandingReturns={loadOutstandingReturns}
loadInventory={loadInventory} loadInventory={loadInventory}
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null} preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,8 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component"; import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component"; import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
@@ -58,9 +58,9 @@ export function BillsListTableComponent({
<EditFilled /> <EditFilled />
</Button> </Button>
)} )}
<BillDeleteButton bill={record} jobid={job.id} /> <BillDeleteButton bill={record} />
<BillDetailEditReturnComponent <BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id, job: job } }} data={{ bills_by_pk: { ...record, jobid: job.id } }}
disabled={ disabled={
record.is_credit_memo || record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid || record.vendorid === bodyshop.inhousevendorid ||

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import { CalculatorFilled } from "@ant-design/icons";
import { Button, Form, InputNumber, Popover, Space } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { Button, Form, InputNumber, Popover } from "antd";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { useTranslation } from "react-i18next";
import { CalculatorFilled } from "@ant-design/icons";
export default function CABCpvrtCalculator({ disabled, form }) { export default function CABCpvrtCalculator({ disabled, form }) {
const [visibility, setVisibility] = useState(false); const [visibility, setVisibility] = useState(false);
@@ -26,14 +26,10 @@ export default function CABCpvrtCalculator({ disabled, form }) {
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}> <Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
<InputNumber precision={0} min={0} /> <InputNumber precision={0} min={0} />
</Form.Item> </Form.Item>
<div style={{ display: "flex", justifyContent: "flex-end" }}> <Button type="primary" htmlType="submit">
<Space> {t("general.actions.calculate")}
<Button type="primary" htmlType="submit"> </Button>
{t("general.actions.calculate")} <Button onClick={() => setVisibility(false)}>Close</Button>
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</Space>
</div>
</Form> </Form>
</div> </div>
); );
@@ -42,7 +38,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
<Popover <Popover
destroyTooltipOnHide destroyTooltipOnHide
content={popContent} content={popContent}
visible={visibility} open={visibility}
disabled={disabled} disabled={disabled}
> >
<Button disabled={disabled} onClick={() => setVisibility(true)}> <Button disabled={disabled} onClick={() => setVisibility(true)}>

View File

@@ -13,6 +13,7 @@ import {
notification, notification,
} from "antd"; } from "antd";
import axios from "axios"; import axios from "axios";
import dayjs from "../../utils/day";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -21,6 +22,7 @@ import {
INSERT_PAYMENT_RESPONSE, INSERT_PAYMENT_RESPONSE,
QUERY_RO_AND_OWNER_BY_JOB_PKS, QUERY_RO_AND_OWNER_BY_JOB_PKS,
} from "../../graphql/payment_response.queries"; } from "../../graphql/payment_response.queries";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCardPayment } from "../../redux/modals/modals.selectors"; import { selectCardPayment } from "../../redux/modals/modals.selectors";
@@ -35,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")), toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
}); });
@@ -46,12 +48,12 @@ const CardPaymentModalComponent = ({
toggleModalVisible, toggleModalVisible,
insertAuditTrail, insertAuditTrail,
}) => { }) => {
const { context, actions } = cardPaymentModal; const { context } = cardPaymentModal;
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT); const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE); const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -63,6 +65,7 @@ const CardPaymentModalComponent = ({
} }
); );
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
//Initialize the intellipay window. //Initialize the intellipay window.
const SetIntellipayCallbackFunctions = () => { const SetIntellipayCallbackFunctions = () => {
console.log("*** Set IntelliPay callback functions."); console.log("*** Set IntelliPay callback functions.");
@@ -71,20 +74,16 @@ const CardPaymentModalComponent = ({
}); });
window.intellipay.runOnApproval(async function (response) { window.intellipay.runOnApproval(async function (response) {
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback. console.warn("*** Running On Approval Script ***");
//Add a slight delay to allow the refetch to properly get the data. form.setFieldValue("paymentResponse", response);
setTimeout(() => { form.submit();
if (actions && actions.refetch && typeof actions.refetch === "function")
actions.refetch();
setLoading(false);
toggleModalVisible();
}, 750);
}); });
window.intellipay.runOnNonApproval(async function (response) { window.intellipay.runOnNonApproval(async function (response) {
// Mutate unsuccessful payment // Mutate unsuccessful payment
const { payments } = form.getFieldsValue(); const { payments } = form.getFieldsValue();
await insertPaymentResponse({ await insertPaymentResponse({
variables: { variables: {
paymentResponse: payments.map((payment) => ({ paymentResponse: payments.map((payment) => ({
@@ -103,15 +102,55 @@ const CardPaymentModalComponent = ({
insertAuditTrail({ insertAuditTrail({
jobid: payment.jobid, jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(), operation: AuditTrailMapping.failedpayment(),
type: "failedpayment",
}) })
); );
}); });
}; };
const handleFinish = async (values) => {
try {
await insertPayment({
variables: {
paymentInput: values.payments.map((payment) => ({
amount: payment.amount,
transactionid: (values.paymentResponse.paymentid || "").toString(),
payer: t("payments.labels.customer"),
type: values.paymentResponse.cardbrand,
jobid: payment.jobid,
date: dayjs(Date.now()),
payment_responses: {
data: [
{
amount: payment.amount,
bodyshopid: bodyshop.id,
jobid: payment.jobid,
declinereason: values.paymentResponse.declinereason,
ext_paymentid: values.paymentResponse.paymentid.toString(),
successful: true,
response: values.paymentResponse,
},
],
},
})),
},
refetchQueries: ["GET_JOB_BY_PK"],
});
toggleModalVisible();
} catch (error) {
console.error(error);
notification.open({
type: "error",
message: t("payments.errors.inserting", { error: error.message }),
});
} finally {
setLoading(false);
}
};
const handleIntelliPayCharge = async () => { const handleIntelliPayCharge = async () => {
setLoading(true); setLoading(true);
//Validate //Validate
try { try {
await form.validateFields(); await form.validateFields();
@@ -124,7 +163,6 @@ const CardPaymentModalComponent = ({
const response = await axios.post("/intellipay/lightbox_credentials", { const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop, bodyshop,
refresh: !!window.intellipay, refresh: !!window.intellipay,
paymentSplitMeta: form.getFieldsValue(),
}); });
if (window.intellipay) { if (window.intellipay) {
@@ -153,6 +191,7 @@ const CardPaymentModalComponent = ({
<Card title="Card Payment"> <Card title="Card Payment">
<Spin spinning={loading}> <Spin spinning={loading}>
<Form <Form
onFinish={handleFinish}
form={form} form={form}
layout="vertical" layout="vertical"
initialValues={{ initialValues={{
@@ -233,14 +272,23 @@ const CardPaymentModalComponent = ({
} }
> >
{() => { {() => {
console.log("Updating the owner info section.");
//If all of the job ids have been fileld in, then query and update the IP field. //If all of the job ids have been fileld in, then query and update the IP field.
const { payments } = form.getFieldsValue(); const { payments } = form.getFieldsValue();
if ( if (
payments?.length > 0 && payments?.length > 0 &&
payments?.filter((p) => p?.jobid).length === payments?.length payments?.filter((p) => p?.jobid).length === payments?.length
) { ) {
console.log("**Calling refetch.");
refetch({ jobids: payments.map((p) => p.jobid) }); refetch({ jobids: payments.map((p) => p.jobid) });
} }
console.log(
"Acc info",
data,
payments && data && data.jobs.length > 0
? data.jobs.map((j) => j.ro_number).join(", ")
: null
);
return ( return (
<> <>
<Input <Input
@@ -295,13 +343,6 @@ const CardPaymentModalComponent = ({
value={totalAmountToCharge?.toFixed(2)} value={totalAmountToCharge?.toFixed(2)}
hidden hidden
/> />
<Input
className="ipayfield"
data-ipayname="comment"
//type="hidden"
value={btoa(JSON.stringify(payments))}
hidden
/>
<Button <Button
type="primary" type="primary"
// data-ipayname="submit" // data-ipayname="submit"
@@ -316,6 +357,11 @@ const CardPaymentModalComponent = ({
); );
}} }}
</Form.Item> </Form.Item>
{/* Lightbox payment response when it is completed */}
<Form.Item name="paymentResponse" hidden>
<Input type="hidden" />
</Form.Item>
</Form> </Form>
</Spin> </Spin>
</Card> </Card>

View File

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

View File

@@ -4,20 +4,11 @@ import { Button, notification, Space } from "antd";
import axios from "axios"; import axios from "axios";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { messaging, requestForToken } from "../../firebase/firebase.utils"; 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 FcmHandler from "../../utils/fcm-handler";
import ChatPopupComponent from "../chat-popup/chat-popup.component"; import ChatPopupComponent from "../chat-popup/chat-popup.component";
import "./chat-affix.styles.scss"; import "./chat-affix.styles.scss";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
chatVisible: selectChatVisible,
});
export function ChatAffixContainer({ bodyshop, chatVisible }) { export function ChatAffixContainer({ bodyshop, chatVisible }) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
@@ -28,7 +19,7 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
try { try {
const r = await axios.post("/notifications/subscribe", { const r = await axios.post("/notifications/subscribe", {
fcm_tokens: await getToken(messaging, { 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", type: "messaging",
imexshopid: bodyshop.imexshopid, imexshopid: bodyshop.imexshopid,
@@ -36,35 +27,34 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
console.log("FCM Topic Subscription", r.data); console.log("FCM Topic Subscription", r.data);
} catch (error) { } catch (error) {
console.log( console.log(
"Error attempting to subscribe to messaging topic: ", "Error attempting to subscribe to messaging topic: ",
error error
); );
notification.open({ notification.open({
type: "warning", type: "warning",
message: t("general.errors.fcm"), message: t("general.errors.fcm"),
btn: ( btn: (
<Space> <Space>
<Button <Button
onClick={async () => { onClick={async () => {
await requestForToken(); await requestForToken();
SubscribeToTopic();
SubscribeToTopic(); }}
}} >
> {t("general.actions.tryagain")}
{t("general.actions.tryagain")} </Button>
</Button> <Button
<Button onClick={() => {
onClick={() => { const win = window.open(
const win = window.open( "https://help.imex.online/en/article/enabling-notifications-o978xi/",
"https://help.imex.online/en/article/enabling-notifications-o978xi/", "_blank"
"_blank" );
); win.focus();
win.focus(); }}
}} >
> {t("general.labels.help")}
{t("general.labels.help")} </Button>
</Button> </Space>
</Space>
), ),
}); });
} }
@@ -81,16 +71,16 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
payload: (payload && payload.data && payload.data.data) || payload.data, payload: (payload && payload.data && payload.data.data) || payload.data,
}); });
} }
let stopMessageListenr, channel; let stopMessageListener, channel;
try { try {
stopMessageListenr = onMessage(messaging, handleMessage); stopMessageListener = onMessage(messaging, handleMessage);
channel = new BroadcastChannel("imex-sw-messages"); channel = new BroadcastChannel("imex-sw-messages");
channel.addEventListener("message", handleMessage); channel.addEventListener("message", handleMessage);
} catch (error) { } catch (error) {
console.log("Unable to set event listeners."); console.log("Unable to set event listeners.");
} }
return () => { return () => {
stopMessageListenr && stopMessageListenr(); stopMessageListener && stopMessageListener();
channel && channel.removeEventListener("message", handleMessage); channel && channel.removeEventListener("message", handleMessage);
}; };
}, [client]); }, [client]);
@@ -98,9 +88,10 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
if (!bodyshop || !bodyshop.messagingservicesid) return <></>; if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
return ( return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}> <div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null} {bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
</div> </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 React from "react";
import { connect } from "react-redux"; import {connect} from "react-redux";
import { import {AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList,} from "react-virtualized";
AutoSizer, import {createStructuredSelector} from "reselect";
CellMeasurer, import {setSelectedConversation} from "../../redux/messaging/messaging.actions";
CellMeasurerCache, import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors";
List as VirtualizedList, import {TimeAgoFormatter} from "../../utils/DateFormatter";
} 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 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"; import "./chat-conversation-list.styles.scss";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
selectedConversation: selectSelectedConversation, selectedConversation: selectSelectedConversation,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setSelectedConversation: (conversationId) => setSelectedConversation: (conversationId) =>
dispatch(setSelectedConversation(conversationId)), dispatch(setSelectedConversation(conversationId)),
}); });
function ChatConversationListComponent({ function ChatConversationListComponent({
conversationList, conversationList,
selectedConversation, selectedConversation,
setSelectedConversation, setSelectedConversation,
loadMoreConversations, loadMoreConversations,
}) { }) {
const cache = new CellMeasurerCache({ const cache = new CellMeasurerCache({
fixedWidth: true, fixedWidth: true,
defaultHeight: 60, defaultHeight: 60,
}); });
const rowRenderer = ({ index, key, style, parent }) => { const rowRenderer = ({index, key, style, parent}) => {
const item = conversationList[index]; 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 ( return (
<CellMeasurer <div className="chat-list-container">
key={key} <AutoSizer>
cache={cache} {({height, width}) => (
parent={parent} <VirtualizedList
columnIndex={0} height={height}
rowIndex={index} width={width}
> rowCount={conversationList.length}
<List.Item rowHeight={cache.rowHeight}
onClick={() => setSelectedConversation(item.id)} rowRenderer={rowRenderer}
className={`chat-list-item ${ onScroll={({scrollTop, scrollHeight, clientHeight}) => {
item.id === selectedConversation if (scrollTop + clientHeight === scrollHeight) {
? "chat-list-selected-conversation" loadMoreConversations();
: null }
}`} }}
style={style} />
> )}
<div </AutoSizer>
style={{ </div>
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>
); );
};
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( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(ChatConversationListComponent); )(ChatConversationListComponent);

View File

@@ -1,27 +1,14 @@
.chat-list-selected-conversation {
background-color: rgba(128, 128, 128, 0.2);
}
.chat-list-container { .chat-list-container {
flex: 1;
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
border: 1px solid gainsboro; border: 1px solid gainsboro;
} }
.chat-list-item { .chat-list-item {
display: flex; .ant-card-head {
flex-direction: row; border: none;
}
&:hover { &:hover {
cursor: pointer; cursor: pointer;
color: #ff7a00; 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, conversation,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [visible, setVisible] = useState(false); const [open, setOpen] = useState(false);
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, { const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
@@ -39,13 +39,13 @@ export function ChatMediaSelector({
}, },
skip: skip:
!visible || !open ||
!conversation.job_conversations || !conversation.job_conversations ||
conversation.job_conversations.length === 0, conversation.job_conversations.length === 0,
}); });
const handleVisibleChange = (visible) => { const handleVisibleChange = (change) => {
setVisible(visible); setOpen(change);
}; };
useEffect(() => { useEffect(() => {
@@ -65,7 +65,7 @@ export function ChatMediaSelector({
externalMediaState={[selectedMedia, setSelectedMedia]} externalMediaState={[selectedMedia, setSelectedMedia]}
/> />
)} )}
{bodyshop.uselocalmediaserver && visible && ( {bodyshop.uselocalmediaserver && open && (
<JobDocumentsLocalGalleryExternal <JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]} externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={ jobId={
@@ -88,8 +88,8 @@ export function ChatMediaSelector({
} }
title={t("messaging.labels.selectmedia")} title={t("messaging.labels.selectmedia")}
trigger="click" trigger="click"
visible={visible} open={open}
onVisibleChange={handleVisibleChange} onOpenChange={handleVisibleChange}
> >
<Badge count={selectedMedia.filter((s) => s.isSelected).length}> <Badge count={selectedMedia.filter((s) => s.isSelected).length}>
<PictureFilled style={{ margin: "0 .5rem" }} /> <PictureFilled style={{ margin: "0 .5rem" }} />

View File

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

View File

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

View File

@@ -16,42 +16,28 @@ const mapDispatchToProps = (dispatch) => ({
export function ChatPrintButton({ conversation }) { export function ChatPrintButton({ conversation }) {
const [loading, setLoading] = useState(false); 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 ( return (
<Space wrap> <Space wrap>
<PrinterOutlined <PrinterOutlined onClick={() => generateDocument('p')}/>
onClick={() => { <MailOutlined onClick={() => generateDocument('e')}/>
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);
}}
/>
{loading && <Spin />} {loading && <Spin />}
</Space> </Space>
); );

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { WarningFilled } from "@ant-design/icons"; import { WarningFilled } from "@ant-design/icons";
import { Form, Input, InputNumber, Space } from "antd"; import { Form, Input, InputNumber, Space } from "antd";
import moment from "moment"; import dayjs from "../../utils/day";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
@@ -68,30 +68,6 @@ export default function ContractFormComponent({
<FormDateTimePicker /> <FormDateTimePicker />
</Form.Item> </Form.Item>
)} )}
{create && (
<Form.Item
shouldUpdate={(p, c) => p.scheduledreturn !== c.scheduledreturn}
>
{() => {
const insuranceOver =
selectedCar &&
selectedCar.insuranceexpires &&
moment(selectedCar.insuranceexpires)
.endOf("day")
.isBefore(moment(form.getFieldValue("scheduledreturn")));
if (insuranceOver)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.insuranceexpired")}
</span>
</Space>
);
return <></>;
}}
</Form.Item>
)}
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow> <LayoutFormRow grow>
<Form.Item <Form.Item
@@ -114,17 +90,16 @@ export default function ContractFormComponent({
> >
{() => { {() => {
const mileageOver = const mileageOver =
selectedCar && selectedCar.nextservicekm selectedCar &&
? selectedCar.nextservicekm <= form.getFieldValue("kmstart") selectedCar.nextservicekm <= form.getFieldValue("kmstart");
: false;
const dueForService = const dueForService =
selectedCar && selectedCar &&
selectedCar.nextservicedate && selectedCar.nextservicedate &&
moment(selectedCar.nextservicedate) dayjs(selectedCar.nextservicedate).isBefore(
.endOf("day") dayjs(form.getFieldValue("scheduledreturn"))
.isSameOrBefore( );
moment(form.getFieldValue("scheduledreturn"))
);
if (mileageOver || dueForService) if (mileageOver || dueForService)
return ( return (
<Space direction="vertical" style={{ color: "tomato" }}> <Space direction="vertical" style={{ color: "tomato" }}>
@@ -142,6 +117,7 @@ export default function ContractFormComponent({
</span> </span>
</Space> </Space>
); );
return <></>; return <></>;
}} }}
</Form.Item> </Form.Item>
@@ -214,9 +190,9 @@ export default function ContractFormComponent({
} }
> >
{() => { {() => {
const dlExpiresBeforeReturn = moment( const dlExpiresBeforeReturn = dayjs(
form.getFieldValue("driver_dlexpiry") form.getFieldValue("driver_dlexpiry")
).isBefore(moment(form.getFieldValue("scheduledreturn"))); ).isBefore(dayjs(form.getFieldValue("scheduledreturn")));
return ( return (
<div> <div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
import { WarningFilled } from "@ant-design/icons"; import { WarningFilled } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import { Button, Form, Input, InputNumber, PageHeader, Space } from "antd"; import { Button, Form, Input, InputNumber, Space } from "antd";
import moment from "moment"; import {PageHeader} from "@ant-design/pro-layout";
import dayjs from "../../utils/day";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries"; import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries";
@@ -10,15 +11,11 @@ import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel
import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component"; import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component";
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component"; import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; //import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
export default function CourtesyCarCreateFormComponent({ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
form,
saveLoading,
newCC,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
@@ -37,7 +34,7 @@ export default function CourtesyCarCreateFormComponent({
} }
/> />
{newCC ? null : <FormFieldsChanged form={form} />} {/* <FormFieldsChanged form={form} /> */}
<LayoutFormRow header={t("courtesycars.labels.vehicle")}> <LayoutFormRow header={t("courtesycars.labels.vehicle")}>
<Form.Item <Form.Item
label={t("courtesycars.fields.year")} label={t("courtesycars.fields.year")}
@@ -267,7 +264,7 @@ export default function CourtesyCarCreateFormComponent({
const nextservicedate = form.getFieldValue("nextservicedate"); const nextservicedate = form.getFieldValue("nextservicedate");
const dueForService = const dueForService =
nextservicedate && nextservicedate &&
moment(nextservicedate).endOf("day").isSameOrBefore(moment()); dayjs(nextservicedate).endOf("day").isSameOrBefore(dayjs());
if (dueForService) if (dueForService)
return ( return (
@@ -308,7 +305,7 @@ export default function CourtesyCarCreateFormComponent({
const expires = form.getFieldValue("registrationexpires"); const expires = form.getFieldValue("registrationexpires");
const dateover = const dateover =
expires && moment(expires).endOf("day").isBefore(moment()); expires && dayjs(expires).endOf("day").isBefore(dayjs());
if (dateover) if (dateover)
return ( return (
@@ -344,7 +341,7 @@ export default function CourtesyCarCreateFormComponent({
const expires = form.getFieldValue("insuranceexpires"); const expires = form.getFieldValue("insuranceexpires");
const dateover = const dateover =
expires && moment(expires).endOf("day").isBefore(moment()); expires && dayjs(expires).endOf("day").isBefore(dayjs());
if (dateover) if (dateover)
return ( return (

View File

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

View File

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

View File

@@ -4,12 +4,11 @@ import {
Card, Card,
Dropdown, Dropdown,
Input, Input,
Menu,
Space, Space,
Table, Table,
Tooltip, Tooltip,
} from "antd"; } from "antd";
import moment from "moment"; import dayjs from "../../utils/day";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -17,18 +16,13 @@ import { DateTimeFormatter } from "../../utils/DateFormatter";
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export default function CourtesyCarsList({ loading, courtesycars, refetch }) { export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: { text: "" },
}); });
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [filter, setFilter] = useLocalStorage(
"filter_courtesy_cars_list",
null
);
const { t } = useTranslation(); const { t } = useTranslation();
const columns = [ const columns = [
@@ -55,16 +49,11 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => alphaSort(a.status, b.status), sorter: (a, b) => alphaSort(a.status, b.status),
filteredValue: filter?.status || null,
filters: [ filters: [
{ {
text: t("courtesycars.status.in"), text: t("courtesycars.status.in"),
value: "courtesycars.status.in", value: "courtesycars.status.in",
}, },
{
text: t("courtesycars.status.inservice"),
value: "courtesycars.status.inservice",
},
{ {
text: t("courtesycars.status.out"), text: t("courtesycars.status.out"),
value: "courtesycars.status.out", value: "courtesycars.status.out",
@@ -77,43 +66,23 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
text: t("courtesycars.status.leasereturn"), text: t("courtesycars.status.leasereturn"),
value: "courtesycars.status.leasereturn", value: "courtesycars.status.leasereturn",
}, },
{
text: t("courtesycars.status.unavailable"),
value: "courtesycars.status.unavailable",
},
], ],
onFilter: (value, record) => record.status === value, onFilter: (value, record) => value.includes(record.status),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
const { nextservicedate, nextservicekm, mileage, insuranceexpires } = const { nextservicedate, nextservicekm, mileage } = record;
record;
const mileageOver = nextservicekm ? nextservicekm <= mileage : false; const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
const dueForService = const dueForService =
nextservicedate && nextservicedate && dayjs(nextservicedate).endOf('day').isSameOrBefore(dayjs());
moment(nextservicedate).endOf("day").isSameOrBefore(moment());
const insuranceOver =
insuranceexpires &&
moment(insuranceexpires).endOf("day").isBefore(moment());
return ( return (
<Space> <Space>
{t(record.status)} {t(record.status)}
{(mileageOver || dueForService || insuranceOver) && ( {(mileageOver || dueForService) && (
<Tooltip <Tooltip title={t("contracts.labels.cardueforservice")}>
title={
(mileageOver || dueForService) && insuranceOver
? t("contracts.labels.insuranceexpired") +
" / " +
t("contracts.labels.cardueforservice")
: insuranceOver
? t("contracts.labels.insuranceexpired")
: t("contracts.labels.cardueforservice")
}
>
<WarningFilled style={{ color: "tomato" }} /> <WarningFilled style={{ color: "tomato" }} />
</Tooltip> </Tooltip>
)} )}
@@ -126,7 +95,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
dataIndex: "readiness", dataIndex: "readiness",
key: "readiness", key: "readiness",
sorter: (a, b) => alphaSort(a.readiness, b.readiness), sorter: (a, b) => alphaSort(a.readiness, b.readiness),
filteredValue: filter?.readiness || null,
filters: [ filters: [
{ {
text: t("courtesycars.readiness.ready"), text: t("courtesycars.readiness.ready"),
@@ -186,7 +154,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
title: t("courtesycars.fields.fuel"), title: t("courtesycars.fields.fuel"),
dataIndex: "fuel", dataIndex: "fuel",
key: "fuel", key: "fuel",
sorter: (a, b) => a.fuel - b.fuel, sorter: (a, b) => alphaSort(a.fuel, b.fuel),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "fuel" && state.sortedInfo.order, state.sortedInfo.columnKey === "fuel" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
@@ -195,14 +163,12 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
return t("courtesycars.labels.fuel.full"); return t("courtesycars.labels.fuel.full");
case 88: case 88:
return t("courtesycars.labels.fuel.78"); return t("courtesycars.labels.fuel.78");
case 75:
return t("courtesycars.labels.fuel.34");
case 63: case 63:
return t("courtesycars.labels.fuel.58"); return t("courtesycars.labels.fuel.58");
case 50: case 50:
return t("courtesycars.labels.fuel.12"); return t("courtesycars.labels.fuel.12");
case 38: case 38:
return t("courtesycars.labels.fuel.38"); return t("courtesycars.labels.fuel.34");
case 25: case 25:
return t("courtesycars.labels.fuel.14"); return t("courtesycars.labels.fuel.14");
case 13: case 13:
@@ -244,8 +210,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
setFilter(filters);
}; };
const tableData = searchText const tableData = searchText
@@ -262,6 +227,27 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
(t(c.status) || "").toLowerCase().includes(searchText.toLowerCase()) (t(c.status) || "").toLowerCase().includes(searchText.toLowerCase())
) )
: courtesycars; : 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 ( return (
<Card <Card
title={t("menus.header.courtesycars")} title={t("menus.header.courtesycars")}
@@ -270,30 +256,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
<Button onClick={() => refetch()}> <Button onClick={() => refetch()}>
<SyncOutlined /> <SyncOutlined />
</Button> </Button>
<Dropdown <Dropdown trigger="click" menu={menu}>
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>
}
>
<Button>{t("general.labels.print")}</Button> <Button>{t("general.labels.print")}</Button>
</Dropdown> </Dropdown>
<Link to={`/manage/courtesycars/new`}> <Link to={`/manage/courtesycars/new`}>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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