Compare commits

...

305 Commits

Author SHA1 Message Date
Dave Richer
3d8f16bb71 Merged in test-beta (pull request #1355)
[DO NOT MERGE] - 3/15/2024 - Beta Release

Approved-by: Allan Carr
2024-03-15 18:51:22 +00:00
Dave Richer
4a87be6adb Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1359)
Feature/IO-1828 Front End Package Updates
2024-03-15 17:28:09 +00:00
Dave Richer
da3087da75 Merge remote-tracking branch 'origin/release/2024-03-15' into feature/IO-1828-Front-End-Package-Updates 2024-03-15 13:27:39 -04:00
Patrick Fic
28f2e8ad30 Merged in feature/IO-2679-interactivity-tracking (pull request #1358)
Feature/IO-2679 interactivity tracking
2024-03-15 17:25:59 +00:00
Patrick Fic
c27e206687 Add index to audit trail. 2024-03-15 10:24:00 -07:00
Patrick Fic
01fd253f1d Manual modification to hasura migration. 2024-03-15 10:23:13 -07:00
Dave Richer
0a56b75aec Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1357)
Add ioevent logging for events.
2024-03-15 17:08:36 +00:00
Dave Richer
00b5036083 - merge release and adjust
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-15 13:07:47 -04:00
Patrick Fic
e67bc0d953 Merged in feature/IO-2679-interactivity-tracking (pull request #1356)
Add ioevent logging for events.
2024-03-15 16:56:37 +00:00
Patrick Fic
3eab3e2fb6 Add ioevent logging for events. 2024-03-15 09:55:14 -07:00
Dave Richer
8e8b038fe5 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1353)
Feature/IO-1828 Front End Package Updates
2024-03-15 14:55:40 +00:00
Dave Richer
17b412c12d - merge release and adjust
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-15 10:55:00 -04:00
Dave Richer
3adf6b649b Merged in feature/IO-2678-Linkable-schedule (pull request #1351)
- Implement
2024-03-15 14:49:06 +00:00
Dave Richer
f8243aa2b3 - Implement
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-15 10:47:46 -04:00
Dave Richer
2ad715f8a5 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1350)
Feature/IO-1828 Front End Package Updates
2024-03-14 19:06:47 +00:00
Dave Richer
3c3f50d138 Merged in feature/IO-2650-Lifecycle-V2 (pull request #1348)
- Fix bug
2024-03-14 18:59:37 +00:00
Dave Richer
4f7e1b81ac - Fix bug
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 14:59:06 -04:00
Dave Richer
0cb0942119 - Merge release
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 14:57:59 -04:00
Dave Richer
806bdc4c70 Merged in feature/IO-2650-Lifecycle-V2 (pull request #1346)
- Missing translation
2024-03-14 16:38:12 +00:00
Dave Richer
a0572a0cec - Missing translation
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 12:37:38 -04:00
Dave Richer
04cdf13e86 Merged in feature/IO-2650-Lifecycle-V2 (pull request #1344)
Feature/IO-2650 Lifecycle V2
2024-03-14 16:33:14 +00:00
Dave Richer
50349e91dc - remove duplicated code
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 12:32:40 -04:00
Dave Richer
9998a8f154 - Fix bug on humanReadable field we have not consumed until now.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 12:28:07 -04:00
Allan Carr
9dcbcb2a43 Merged in feature/IO-2671-Vehicle-Detail-Table (pull request #1343)
IO-2671 Add in appropriate sorters at same time

Approved-by: Dave Richer
2024-03-14 15:58:46 +00:00
Allan Carr
5773f7a0f3 Merged in feature/IO-2650-Lifecycle-Report (pull request #1345)
IO-2650 Job Lifecycle Report Center Reports

Approved-by: Dave Richer
2024-03-14 15:58:17 +00:00
Allan Carr
8614d88e71 Merged in feature/IO-2630-Parts-Queue-Mods (pull request #1342)
IO-2630 Adjust for onRow selection

Approved-by: Dave Richer
2024-03-14 15:57:45 +00:00
Allan Carr
fa5e26c52a IO-2650 Job Lifecycle Report Center Reports
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 17:22:42 -07:00
Dave Richer
90e1cbd390 - Job Lifecycle Dashboard Component.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-13 19:20:10 -04:00
Allan Carr
947a3c6a88 IO-2671 Add in appropriate sorters at same time
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 12:31:51 -07:00
Allan Carr
a33662e6f0 IO-2630 Adjust for oRow selection
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 12:04:45 -07:00
Patrick Fic
6d1f04369e Merged in feature/IO-2674-reorder-resp-centers (pull request #1340)
IO-2674 Add reordering arrows for responsibility centers in IO.

Approved-by: Allan Carr
2024-03-13 17:07:09 +00:00
Patrick Fic
4a27726ef3 Adjusted label for payers & added reorder to payers. 2024-03-13 12:54:31 -04:00
Allan Carr
eedba97237 Merged in feature/IO-2625-BPT-Hrs-in-Employee-Assignment (pull request #1339)
IO-2625 B/P/T Hrs Display in Employee Assignment Block

Approved-by: Dave Richer
2024-03-13 16:50:47 +00:00
Allan Carr
bcf095ed4f Merged in feature/IO-2669-Next-Contact-Date-formating (pull request #1337)
IO-2669 Next Contact Formating

Approved-by: Dave Richer
2024-03-13 16:50:00 +00:00
Allan Carr
c3f9e268c7 Merged in feature/IO-2671-Vehicle-Detail-Table (pull request #1338)
IO-2671 Missing field in Vehicle query

Approved-by: Dave Richer
2024-03-13 16:49:36 +00:00
Allan Carr
c8442f0750 Merged in feature/IO-2570-Totals-Card-on-Draw-have-Customer-Owing (pull request #1341)
IO-2570 Change Ded to Customer Owing amount

Approved-by: Dave Richer
2024-03-13 16:49:13 +00:00
Allan Carr
da5b446c30 Merge branch 'master' into feature/IO-2630-Parts-Queue-Mods
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 09:49:02 -07:00
Allan Carr
e13b2bb969 Merged in feature/IO-2520-Kaizen-Data-Pump (pull request #1336)
IO-2520 Change where email notification occurs

Approved-by: Dave Richer
2024-03-13 16:48:01 +00:00
Allan Carr
e8969c4698 IO-2570 Change Ded to Customer Owing amount
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 09:47:33 -07:00
Patrick Fic
346d32a2bb IO-2674 Add reordering arrows for responsibility centers in IO. 2024-03-13 08:02:20 -04:00
Dave Richer
6ba00a90be Merge branch 'master' into feature/IO-2650-Lifecycle-V2 2024-03-12 18:11:08 -04:00
Dave Richer
4293d20313 - Backend Changes for Lifecycle Data
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-12 18:10:09 -04:00
Allan Carr
706c70c509 IO-2625 B/P/T Hrs Display in Employee Assignment Block
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-12 14:49:12 -07:00
Allan Carr
e872b1bf0a IO-2671 Missing field in Vehicle query
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-12 11:55:07 -07:00
Allan Carr
379fa060d8 IO-2669 Next Contact Formating
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-12 11:24:57 -07:00
Allan Carr
a6258c6456 Merged in feature/IO-2650-Lifecycle-Report (pull request #1335)
IO-2650 Lifecycle Report for Print Center

Approved-by: Dave Richer
2024-03-12 17:01:19 +00:00
Allan Carr
d6bf0a225b IO-2520 Change where email notification occurs
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-11 17:46:19 -07:00
Allan Carr
ec2b914e5e Merge branch 'master' into feature/IO-2520-Kaizen-Data-Pump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-11 17:45:26 -07:00
Allan Carr
309a20148a IO-2650 Lifecycle Report for Print Center
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-11 17:41:37 -07:00
Dave Richer
386de19703 Merged in test-beta (pull request #1330)
Test Beta into Master Beta for 03-08 Release

Approved-by: Allan Carr
2024-03-11 17:48:21 +00:00
Dave Richer
013b56778b Merged in release/2024-03-08 (pull request #1331)
Release into Master for 03 08 Release

Approved-by: Allan Carr
2024-03-11 17:47:51 +00:00
Dave Richer
34f48f6c9e Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1334)
- Regression
2024-03-11 17:04:02 +00:00
Dave Richer
31dd526907 - Regression
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-11 13:02:37 -04:00
Dave Richer
59c4a7ffb2 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1333)
- Regression
2024-03-11 16:36:02 +00:00
Dave Richer
8fdee890dc - Regression
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-11 12:35:19 -04:00
Dave Richer
e429249e18 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1332)
- Regression
2024-03-11 16:20:48 +00:00
Dave Richer
2a6518d4f1 - Regression
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-11 12:19:20 -04:00
Dave Richer
ad0ccf0cff Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-03-11 11:26:23 -04:00
Dave Richer
8f8215111d - Regression
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-11 11:26:06 -04:00
Dave Richer
d81b8c65b8 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-03-11 09:50:45 -04:00
Dave Richer
2b36fd9037 Merge release / update 2024-03-11 09:50:20 -04:00
Allan Carr
9dec4a3a61 Merged in feature/IO-2663-Export-Pages-Sorters-and-Filters (pull request #1327)
Correct Sorters
2024-03-08 19:36:40 +00:00
Allan Carr
189b4db90f Merged in feature/IO-2575-Production-Special-Coverage (pull request #1328)
Bring in line with other components
2024-03-08 19:36:27 +00:00
Allan Carr
c49fa1c527 Bring in line with other components
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-08 11:32:48 -08:00
Allan Carr
53ef048f6f Correct Sorters
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-08 11:24:41 -08:00
Dave Richer
1cc7eed983 Merged in feature/IO-2662-New-Report-Reflectors (pull request #1325)
- Add new special filters
2024-03-08 17:47:43 +00:00
Dave Richer
1c8f377212 - Add new special filters
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-08 12:47:11 -05:00
Dave Richer
c050947276 Merged in feature/IO-2662-New-Report-Reflectors (pull request #1323)
- Add new special filters
2024-03-08 17:37:10 +00:00
Dave Richer
9bde1f820d - Add new special filters
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-08 12:36:29 -05:00
Allan Carr
393765640c Merged in feature/IO-2663-Export-Pages-Sorters-and-Filters (pull request #1321)
IO-2663 Export Pages Sorters and Filters

Approved-by: Dave Richer
2024-03-08 01:14:40 +00:00
Allan Carr
69c2836425 Merged in feature/IO-2665-Jobs-sorters-and-query (pull request #1322)
IO-2665 Jobs page sorters and query adjustment

Approved-by: Dave Richer
2024-03-08 01:14:07 +00:00
Allan Carr
ec9edf30eb Merged in feature/IO-2651-Audit-Log-Extension (pull request #1320)
IO-2651 Audit Log Extension

Approved-by: Dave Richer
2024-03-08 01:13:42 +00:00
Allan Carr
6a812f9ea7 IO-2665 Jobs page sorters and query adjustment
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-07 11:53:34 -08:00
Allan Carr
fa86254bfd Prettierr
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-06 15:30:56 -08:00
Allan Carr
58ab7afbb3 IO-2663 Export Pages Sorters and Filters
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-06 15:29:24 -08:00
Allan Carr
fa7d90d2a9 IO-2651 Audit Log Extension
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-05 15:28:22 -08:00
Allan Carr
fbf9047974 Merged in feature/IO-2660-Phonebook-Draw-Title-Bug (pull request #1317)
IO-2660 Phonebook Drawer Title

Approved-by: Dave Richer
2024-03-05 20:32:29 +00:00
Allan Carr
3e9b795052 Merged in feature/IO-2575-Production-Special-Coverage (pull request #1318)
IO-2575 Special Coverage and Sorters & Filters

Approved-by: Dave Richer
2024-03-05 20:31:53 +00:00
Dave Richer
c3a78e4f71 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1319)
- Fix for IO-2540
2024-03-05 20:30:50 +00:00
Dave Richer
bf9fc128cd - Fix for IO-2540
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-05 15:13:40 -05:00
Allan Carr
d29ffc21e5 IO-2575 Special Coverage and Sorters & Filters
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-05 11:49:57 -08:00
Allan Carr
959f7780e8 IO-2660 Phonebook Drawer Title
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-04 12:13:41 -08:00
Dave Richer
e47731702a Merged in release/2024-03-01 (pull request #1315)
Release/2024 03 01
2024-03-01 23:12:11 +00:00
Dave Richer
7bc9d18f5c Merged in test-beta (pull request #1314)
Test beta
2024-03-01 23:11:42 +00:00
Dave Richer
d68f1765d1 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-03-01 15:01:37 -05:00
Dave Richer
d557de7eec - Merge release
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-01 15:01:17 -05:00
Patrick Fic
85a3aeb335 Resolve refund payment logging. 2024-03-01 11:51:01 -08:00
Dave Richer
74d23010b8 Merge remote-tracking branch 'origin/feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-03-01 13:23:31 -05:00
Dave Richer
68fcd47315 - Merge release
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-01 13:21:33 -05:00
Dave Richer
cca7215f10 Merge remote-tracking branch 'origin/release/2024-03-01' into feature/IO-1828-Front-End-Package-Updates
# Conflicts:
#	_reference/reportFiltersAndSorters.md
#	client/src/components/bill-delete-button/bill-delete-button.component.jsx
#	client/src/components/bills-list-table/bills-list-table.component.jsx
#	client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx
#	client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx
#	client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx
#	client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx
#	client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx
#	client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx
#	client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx
#	client/src/pages/dms/dms.container.jsx
#	client/src/redux/application/application.sagas.js
#	client/src/translations/en_us/common.json
#	client/src/translations/es/common.json
#	client/src/translations/fr/common.json
#	client/src/utils/AuditTrailMappings.js
#	client/src/utils/graphQLmodifier.js
#	package-lock.json
#	package.json
2024-03-01 13:03:37 -05:00
Dave Richer
80b7ae0e54 Merge branch 'feautre/IO-2647-Reporting-V3-From-Master' into release/2024-03-01
# Conflicts:
#	_reference/reportFiltersAndSorters.md
2024-02-29 22:25:52 -05:00
Dave Richer
0529ac4478 - Reports V3 Targeted at Master
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-29 22:22:55 -05:00
Allan Carr
3cafbebbee Merged in feature/IO-1366-Audit-Logging (pull request #1311)
Feature/IO-1366 Audit Logging

Approved-by: Dave Richer
2024-03-01 02:44:05 +00:00
Allan Carr
6f248d864e Merged in feature/IO-2656-Job-Count-on-Scoreboard (pull request #1312)
IO-2656 Job Count on Scoreboard Jobs

Approved-by: Dave Richer
2024-03-01 02:43:23 +00:00
Allan Carr
a4a84572b7 IO-2656 Job Count on Scoreboard Jobs
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-29 15:36:58 -08:00
Allan Carr
a45d0bb9f4 IO-1366 Job Exported Audit Trail
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-29 14:13:45 -08:00
Allan Carr
a2e0f9fbe7 IO-1366 Audit Log for Bill Delete, Job Suspend, Job Void, Correct Saga
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-28 14:01:35 -08:00
Allan Carr
e37dc0a18f Merge branch 'master' into feature/IO-1366-Audit-Logging
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-28 13:56:25 -08:00
Allan Carr
0bc00d46cf Merged in feature/IO-2654-Courtesy-Car-List-Filter (pull request #1310)
IO-2654 Local Storage Filter State for Courtesy Car List

Approved-by: Dave Richer
2024-02-28 18:23:49 +00:00
Allan Carr
e80e40bb76 IO-2654 Local Storage Filter State for Courtesy Car List
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-27 16:37:07 -08:00
Allan Carr
a88c102b27 Prettier and Package Update
Azura Storage Blob and Trivago Prettier Sort Imports

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-27 16:04:41 -08:00
Dave Richer
a97197ccc7 Merged in test-beta (pull request #1306)
Test beta
2024-02-23 22:02:01 +00:00
Dave Richer
c691d44c44 Merged in release/2024-02-23 (pull request #1307)
Release/2024 02 23
2024-02-23 22:00:15 +00:00
Dave Richer
6eb48f92b3 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-23 16:59:43 -05:00
Dave Richer
00d5ab35e7 - Fix regression
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-23 16:59:22 -05:00
Dave Richer
dc09e47bf5 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-23 16:36:55 -05:00
Dave Richer
5f98c2286b - Ant 5 stuff
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-23 16:35:12 -05:00
Allan Carr
ebb3a13ff5 Merged in feature/IO-2640-TV-Mode-for-Scheduled-In-Out (pull request #1308)
IO-2640 Adjust Filters and Sorters for Table

Approved-by: Dave Richer
2024-02-23 21:14:18 +00:00
Allan Carr
3846b7c5fc IO-2640 Adjust Filters and Sorters for Table
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-23 13:01:46 -08:00
Dave Richer
7fba6cb5e6 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-23 11:47:11 -05:00
Dave Richer
6a26fb413c - refactor on scheduled-out-today
- routine package updates

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-23 11:46:53 -05:00
Dave Richer
07fa92f6d6 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1305)
Feature/IO-1828 Front End Package Updates
2024-02-22 23:45:38 +00:00
Dave Richer
16a43d998f - Merge release and adjust due to version differences
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-22 18:44:43 -05:00
Allan Carr
93d139f926 Merged in feature/IO-2640-TV-Mode-for-Scheduled-In-Out (pull request #1303)
IO-2640 TV Mode for Schedule In and Out Dashboard Components

Approved-by: Dave Richer
2024-02-22 21:50:18 +00:00
Allan Carr
4d1f40537c IO-2640 Change Variable Names and adjust CSS
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-22 12:57:48 -08:00
Dave Richer
896f1415f7 Merged in feature/IO-2636-Customized-Report-Filtering-Version-2 (pull request #1302)
Feature/IO-2636 Customized Report Filtering Version 2
2024-02-21 21:58:27 +00:00
Dave Richer
8a01cd9cb0 - call changes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-21 16:57:56 -05:00
Allan Carr
c2013d47e7 IO-2640 TV Mode for Schedule In and Out Dashboard Components
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-21 12:03:35 -08:00
Dave Richer
0f20807690 Merge remote-tracking branch 'origin/master' into feature/IO-2636-Customized-Report-Filtering-Version-2 2024-02-21 14:30:28 -05:00
Dave Richer
2a45be6a45 - fix on change set Field value issues
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-21 14:29:56 -05:00
Dave Richer
b63602143e - fix select box being weird on scroll / resize
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-21 14:10:50 -05:00
Dave Richer
2add712270 - add additional date picker presets in development
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-21 13:23:22 -05:00
Dave Richer
77c8f74bcb - Restore functionality
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-21 13:13:26 -05:00
Allan Carr
1d38102541 Merged in feature/IO-2578-Scoreboard-Entries (pull request #1300)
IO-2578 Scoreboard Entries Modal

Approved-by: Dave Richer
2024-02-21 17:36:28 +00:00
Allan Carr
6117b5ab64 Merged in feature/IO-2562-Job-Info-Block-CC-Info (pull request #1298)
IO-2562 CC Info in Job Block UI Correction

Approved-by: Dave Richer
2024-02-21 17:35:39 +00:00
Allan Carr
578f0a110e Merged in feature/IO-2556-CC-Sort-Order (pull request #1301)
IO-2556 CC Sort Order

Approved-by: Dave Richer
2024-02-21 17:34:56 +00:00
Allan Carr
b5c66274ca Merged in feature/IO-2557-New-CC-Contract-Warnings (pull request #1299)
IO-2557 New CC Contract Warnings

Approved-by: Dave Richer
2024-02-21 17:34:08 +00:00
Dave Richer
be46bdc57f - Default sorts!
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-20 19:11:37 -05:00
Dave Richer
6263e63a1d Merge branch 'feature/IO-2636-Customized-Report-Filtering-Version-2' of bitbucket.org:snaptsoft/bodyshop into feature/IO-2636-Customized-Report-Filtering-Version-2 2024-02-20 19:11:10 -05:00
Dave Richer
83d702f12b - Default sorts!
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-20 19:10:34 -05:00
Dave Richer
37708a0b59 - Default sorts!
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-20 19:07:23 -05:00
Allan Carr
6cfcab8156 IO-2557 / IO-1019 Update tooltip
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-20 14:52:40 -08:00
Allan Carr
33ec18986d IO-2556 CC Sort Order
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-20 13:10:57 -08:00
Dave Richer
6b7b34ae79 - Progress Commit, this fills agreed upon functionality
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-20 16:00:59 -05:00
Allan Carr
06ef2482ba IO-2562 CC Info in Job Block UI Correction
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-20 12:53:12 -08:00
Allan Carr
83bd485597 IO-2557 New CC Contract Warnings
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-20 09:19:30 -08:00
Dave Richer
6921f2fe68 Merge branch 'release/2024-02-16' into feature/IO-2636-Customized-Report-Filtering-Version-2 2024-02-16 20:49:04 -05:00
Allan Carr
a7e199932c IO-2578 Scoreboard Entries Modal
Correct OK button, add sorting to table, adjust date to only be a date, remove closeable on modal

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-16 17:14:11 -08:00
Allan Carr
2427c14b7b Merged in release/2024-02-16 (pull request #1296)
IO-2631 Correct Import Statement for moment

Approved-by: Dave Richer
2024-02-16 21:37:49 +00:00
Allan Carr
747cd52f54 Merged in test-beta (pull request #1297)
Test beta

Approved-by: Dave Richer
2024-02-16 21:36:57 +00:00
Dave Richer
c9fb40a9ff Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-16 15:36:53 -05:00
Dave Richer
08f7376961 - fix bug
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-16 15:36:16 -05:00
Dave Richer
d99eae6a7c Merge branch 'release/2024-02-16' into feature/IO-1828-Front-End-Package-Updates
# Conflicts:
#	client/src/components/jobs-available-table/jobs-available-table.container.jsx
2024-02-16 15:34:25 -05:00
Allan Carr
66655d449e Merged in feature/IO-2631-Update-Schedule-Completion-on-Import (pull request #1294)
IO-2631 Correct Import Statement for moment

Approved-by: Dave Richer
2024-02-16 20:33:05 +00:00
Allan Carr
845a84c4c8 IO-2631 Correct Import Statement for moment 2024-02-16 12:30:12 -08:00
Dave Richer
0ff5ea3d59 Merged in release/2024-02-16 (pull request #1291)
Release/2024 02 16

Approved-by: Patrick Fic
2024-02-16 20:21:35 +00:00
Dave Richer
3d70aa8b6c Merged in test-beta (pull request #1290)
Test beta

Approved-by: Patrick Fic
2024-02-16 20:21:30 +00:00
Dave Richer
b2ef0fb7d0 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-16 14:21:01 -05:00
Dave Richer
093089f977 Merge branch 'release/2024-02-16' into feature/IO-1828-Front-End-Package-Updates
# Conflicts:
#	client/src/components/jobs-available-table/jobs-available-table.container.jsx
2024-02-16 14:20:30 -05:00
Allan Carr
09492e647e Merged in feature/IO-2631-Update-Schedule-Completion-on-Import (pull request #1292)
IO-2631 Correct for Business Days

Approved-by: Dave Richer
2024-02-16 19:19:08 +00:00
Allan Carr
2d6594cc73 IO-2631 Correct for Business Days 2024-02-16 11:08:59 -08:00
Dave Richer
0a27c38b56 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1289)
Feature/IO-1828 Front End Package Updates
2024-02-16 17:53:11 +00:00
Dave Richer
e020f60d34 - clear stage
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-16 12:50:51 -05:00
Dave Richer
3b8e83d88a - clear stage
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-16 12:32:40 -05:00
Dave Richer
3ec4dbb5b8 - big progress commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-16 12:25:24 -05:00
Allan Carr
601fdbba39 Merged in feature/IO-2637-Add-Scoreboard-Date (pull request #1287)
IO-2637 Correct for Timezone offset

Approved-by: Dave Richer
2024-02-16 17:23:05 +00:00
Allan Carr
830adc4ef3 IO-2637 Correct for Timezone offset 2024-02-16 08:52:33 -08:00
Dave Richer
3f216195ca Merge branch 'feature/IO-2456-Report-Filters-From-Master' into release/2024-02-16 2024-02-15 21:03:30 -05:00
Dave Richer
bcb8de0937 - Fix issues with labels on sorters
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 21:03:04 -05:00
Dave Richer
9cc0d6175e - Progress commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 21:01:43 -05:00
Dave Richer
4c07632ee6 Merged in feature/IO-2456-Report-Filters-From-Master (pull request #1283)
- Report Center Filters Version 1 retargeted to Master

Approved-by: Patrick Fic
2024-02-15 17:32:38 +00:00
Dave Richer
767c219af8 - Remove additional console.log
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 11:59:56 -05:00
Dave Richer
cfc301570e - Remove redundant CSS
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 11:57:27 -05:00
Allan Carr
fb718f9f37 Merged in feature/IO-2631-Update-Schedule-Completion-on-Import (pull request #1284)
IO-2631 Update Scheduled Completion on Supp

Approved-by: Dave Richer
2024-02-15 16:53:41 +00:00
Dave Richer
cafc0e5628 - Update GUI and provide loading state
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 11:49:02 -05:00
Dave Richer
a635725839 - Remove console.log statements
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 10:58:19 -05:00
Allan Carr
eb8e9b10ef IO-2631 Update Scheduled Completion on Supp 2024-02-14 16:47:25 -08:00
Dave Richer
2584f7129c - Report Center Filters Version 1 retargeted to Master
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-14 14:31:35 -05:00
Dave Richer
5d68da846a Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-14 13:45:14 -05:00
Dave Richer
46be569b4a - package updates
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-14 13:44:55 -05:00
Dave Richer
3988deea19 Merge branch 'master' into feature/IO-1828-Front-End-Package-Updates 2024-02-14 13:34:12 -05:00
Dave Richer
17aaaf38d1 Merged in test-beta (pull request #1281)
- fix time input boxes with showSeconds deprecated prop
2024-02-12 21:29:01 +00:00
Dave Richer
e09081e9ef Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1280)
- fix time input boxes with showSeconds deprecated prop
2024-02-12 21:28:25 +00:00
Dave Richer
9883c39101 - fix time input boxes with showSeconds deprecated prop
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-12 16:27:10 -05:00
Dave Richer
735a38ead6 Merged in test-beta (pull request #1279)
- card payment modal component had unhidden fields.
2024-02-12 20:56:14 +00:00
Dave Richer
cca07229d2 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1278)
- card payment modal component had unhidden fields.
2024-02-12 20:55:47 +00:00
Dave Richer
f43b4b49ec - card payment modal component had unhidden fields.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-12 15:54:44 -05:00
Allan Carr
47a9bd1c2c Merged in test-beta (pull request #1277)
Test beta

Approved-by: Patrick Fic
2024-02-09 22:15:56 +00:00
Dave Richer
d03910315d Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-09 14:46:39 -05:00
Dave Richer
52ed56fe57 - fix query
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-09 14:46:22 -05:00
Dave Richer
960febfe00 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-09 14:33:04 -05:00
Dave Richer
b617ccdaf8 - fix query
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-09 14:32:48 -05:00
Dave Richer
44168de7a1 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-09 14:13:10 -05:00
Dave Richer
fe241be5d1 - missed a history push
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-09 14:12:36 -05:00
Dave Richer
ef30352ae3 Merged in test-beta (pull request #1276)
Test beta

Approved-by: Patrick Fic
2024-02-09 18:47:31 +00:00
Allan Carr
6c996037d6 Merged in release/2024-02-02 (pull request #1274)
Release/2024 02 02

Approved-by: Patrick Fic
2024-02-09 18:47:24 +00:00
Dave Richer
b69505206c Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1275)
Feature/IO-1828 Front End Package Updates
2024-02-09 18:39:03 +00:00
Dave Richer
e187bc3c0c - Fix deprecations brought in from merge release
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-09 13:35:50 -05:00
Dave Richer
4654e01cca - Merge release
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-09 13:29:51 -05:00
Allan Carr
f421b45222 Merged in feature/IO-2630-Parts-Queue-Mods (pull request #1272)
IO-2630 Adjust Query to match Tags
2024-02-09 17:54:46 +00:00
Allan Carr
30cf46a158 IO-2630 Adjust Query to match Tags 2024-02-09 09:54:22 -08:00
Allan Carr
8d37c54f89 Merged in feature/IO-2630-Parts-Queue-Mods (pull request #1269)
Feature/IO-2630 Parts Queue Mods

Approved-by: Dave Richer
2024-02-09 16:14:51 +00:00
Dave Richer
542ca4f1eb Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1270)
Feature/IO-1828 Front End Package Updates
2024-02-09 16:14:28 +00:00
Dave Richer
b090a0110a - Merge release
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-09 11:11:22 -05:00
Allan Carr
f9b9f39418 IO-2630 Add in Drawer for Parts Queue 2024-02-08 17:18:14 -08:00
Allan Carr
05a5df789b IO-2030 Change & Add Columns, Add Sorters and Filters 2024-02-08 13:22:00 -08:00
Allan Carr
bed87eda97 Merged in feature/IO-2626-CSI-Pages (pull request #1267)
IO-2626 Adjust Image Prop on customer page
2024-02-08 17:58:21 +00:00
Allan Carr
67008c35b8 IO-2626 Adjust Image Prop on customer page 2024-02-08 09:57:39 -08:00
Dave Richer
797610a364 Merge branch 'test-beta' into master-beta 2024-02-06 19:28:30 -05:00
Dave Richer
1e28bc5eef Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta
# Conflicts:
#	client/src/graphql/apollo-error-handling.js
#	client/src/index.js
2024-02-06 19:27:49 -05:00
Patrick Fic
9b95e40a13 Merge branch 'release/2024-02-02' of bitbucket.org:snaptsoft/bodyshop into release/2024-02-02 2024-02-06 10:05:26 -08:00
Patrick Fic
e3a998b6f8 Merge branch 'feature/IO-2626-CSI-Pages' into release/2024-02-02 2024-02-06 10:05:11 -08:00
Patrick Fic
3c7ede0155 IO-2626 Prevent crisp from loading on anon CSI page. 2024-02-06 10:04:59 -08:00
Allan Carr
82c7aa1347 Merged in feature/IO-2626-CSI-Pages (pull request #1265)
IO-2626 Resource Class for Test
2024-02-06 17:37:46 +00:00
Allan Carr
4ccd912363 Merge branch 'feature/IO-2626-CSI-Pages' of bitbucket.org:snaptsoft/bodyshop into feature/IO-2626-CSI-Pages 2024-02-06 09:36:16 -08:00
Patrick Fic
c0525842fd Merge branch 'feature/IO-2626-CSI-Pages' into release/2024-02-02 2024-02-06 09:29:39 -08:00
Patrick Fic
dd5ca5d233 Add resource class to test build as well. 2024-02-06 09:29:22 -08:00
Allan Carr
616a4b04a0 IO-2626 Resource Class for Test 2024-02-06 09:10:32 -08:00
Allan Carr
9a34640c88 Merged in feature/IO-2626-CSI-Pages (pull request #1263)
IO-2626 CICD Resource Size Change
2024-02-06 16:55:55 +00:00
Allan Carr
3110be4703 IO-2626 CICD Resource Size Change 2024-02-06 08:52:51 -08:00
Dave Richer
ed5ec61e7c Merged in test-beta (pull request #1262)
- Add additional date time autocomplete formats
2024-02-06 16:42:12 +00:00
Dave Richer
5fd71cf25e Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1261)
- Add additional date time autocomplete formats
2024-02-06 16:40:39 +00:00
Allan Carr
971c8d41ff Merged in feature/IO-2626-CSI-Pages (pull request #1253)
IO-2626 CSI Pages
2024-02-06 16:17:50 +00:00
Allan Carr
7c303a5154 IO-2626 Change Server Variable Name for response 2024-02-05 20:06:28 -08:00
Allan Carr
9383b37a41 IO-2626 Modify seachparm and fix linking 2024-02-05 20:03:17 -08:00
Dave Richer
9f41b9d9fa Merged in test-beta (pull request #1259)
- Add additional date time autocomplete formats
2024-02-05 21:52:56 +00:00
Dave Richer
942fff68a2 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1258)
- Add additional date time autocomplete formats
2024-02-05 21:52:27 +00:00
Dave Richer
397f9bf587 Merged in test-beta (pull request #1257)
- fix date picker
2024-02-05 21:13:57 +00:00
Dave Richer
0d7e54364a Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1256)
- fix date picker
2024-02-05 21:13:28 +00:00
Dave Richer
bfc2ecea1c Merged in test-beta (pull request #1255)
- Fix for job line null check
2024-02-05 16:37:58 +00:00
Dave Richer
4d1a583939 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1254)
- Fix for job line null check
2024-02-05 16:37:29 +00:00
Allan Carr
205d507097 IO-2626 Update Translations 2024-02-02 22:50:40 -08:00
Allan Carr
97a1bd66d1 IO-2626 Correct Sorting, Linking, Pagination and Update Response Container 2024-02-02 22:45:06 -08:00
Allan Carr
0d1ff6390c IO-2626 Correct Error Page footer 2024-02-02 12:35:18 -08:00
Allan Carr
830d2c87d2 IO-2626 CSI Pages
Move to Server side initial commit
2024-02-02 11:55:57 -08:00
Allan Carr
da98e7b886 Merged in feature/IO-2624-Federal-Tax-Exempt-Destructure (pull request #1251)
IO-2624 federal_tax_exempt destructure

Approved-by: Dave Richer
2024-02-02 17:18:24 +00:00
Patrick Fic
eb7e2d83af Merge branch 'test-beta' into master-beta - sentry error tracking. 2024-02-01 11:00:23 -08:00
Patrick Fic
ce3caef085 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into master-beta 2024-01-31 13:14:38 -08:00
Patrick Fic
a39dcfba10 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-31 13:14:13 -08:00
Patrick Fic
ce3dbbfbc6 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-31 13:04:34 -08:00
Patrick Fic
d8cb1b4a68 Merge branch 'feature/Sentry-Improvements' into test-beta 2024-01-31 10:45:17 -08:00
Patrick Fic
0455e03bfd Remove apollo sentry error. 2024-01-31 10:44:52 -08:00
Patrick Fic
c155d340b5 CI update. 2024-01-31 10:44:22 -08:00
Patrick Fic
434f346688 Merge branch 'feature/Sentry-Improvements' into test-beta 2024-01-31 10:39:07 -08:00
Patrick Fic
4719c2d3a3 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-31 10:37:03 -08:00
Patrick Fic
ae596b9df4 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-31 10:21:20 -08:00
Allan Carr
a74a9ba5a1 IO-2624 federal_tax_exempt destructure 2024-01-31 09:59:56 -08:00
Dave Richer
f463d3d6aa Merged in test-beta (pull request #1249)
Test beta
2024-01-31 17:01:06 +00:00
Dave Richer
b9bca31b57 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1248)
- replace require with import
2024-01-31 17:00:28 +00:00
Patrick Fic
5ad5cca2ed Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-30 17:40:00 -08:00
Patrick Fic
fd720f2a27 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-30 17:38:25 -08:00
Patrick Fic
02957fa132 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-30 17:33:29 -08:00
Patrick Fic
2310f99787 Merge branch 'test-beta' of bitbucket.org:snaptsoft/bodyshop into test-beta 2024-01-30 17:17:39 -08:00
Patrick Fic
fea67e752b Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-30 17:17:32 -08:00
Patrick Fic
b8a3081488 Merged in feature/Sentry-Improvements (pull request #1247)
Change tracing targets.
2024-01-31 01:09:03 +00:00
Patrick Fic
8c6ca5a4bd Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1245)
Customer runner.
2024-01-30 22:45:22 +00:00
Patrick Fic
9d1dc4e0bb Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1244)
Use self hosted runner.
2024-01-30 22:16:22 +00:00
Patrick Fic
caff9af1a3 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1243)
Increase CI allocation for beta builds.
2024-01-30 22:02:00 +00:00
Patrick Fic
b810dec766 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1241)
Update S3 sync in CI for beta builds.
2024-01-30 21:45:06 +00:00
Patrick Fic
929086061d Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1240)
Update S3 sync in CI for beta builds.
2024-01-30 21:44:38 +00:00
Patrick Fic
9b61da5c62 Merged in feature/Sentry-Improvements (pull request #1239)
Feature/Sentry Improvements
2024-01-30 21:42:33 +00:00
Dave Richer
7cb70978d6 Merged in test-beta (pull request #1238)
Test beta
2024-01-30 21:34:56 +00:00
Dave Richer
1c716546d5 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1237)
Feature/IO-1828 Front End Package Updates
2024-01-30 21:34:30 +00:00
Dave Richer
4f9acb68a6 Merged in test-beta (pull request #1236)
- fix EULA
2024-01-30 18:31:41 +00:00
Dave Richer
1e669e100a Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1235)
- fix EULA
2024-01-30 18:30:47 +00:00
Dave Richer
13e342f64f Merged in test-beta (pull request #1231)
IO-1532 resolve update logic issue for status timings.
2024-01-29 17:13:57 +00:00
Dave Richer
1bda15e353 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-29 12:12:55 -05:00
Dave Richer
7ec0963e32 Merged in test-beta (pull request #1226)
Test beta
2024-01-27 02:26:07 +00:00
Dave Richer
a928d72aac Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1225)
Feature/IO-1828 Front End Package Updates
2024-01-26 23:16:29 +00:00
Dave Richer
4475bf038b Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-26 11:50:07 -05:00
Dave Richer
53044d2790 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1214)
- Fix CC Cart stuff
2024-01-25 21:04:44 +00:00
Dave Richer
7a36e48401 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1213)
Feature/IO-1828 Front End Package Updates
2024-01-25 20:27:01 +00:00
Dave Richer
ebc75e71d3 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1211)
- Fix empty strings passing validation.
2024-01-25 18:09:51 +00:00
Dave Richer
1c779b05c3 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-24 17:29:16 -05:00
Dave Richer
9e6ee505ca Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-24 12:50:49 -05:00
Dave Richer
ed15620884 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-24 12:20:49 -05:00
Dave Richer
680583addf Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1208)
Feature/IO-1828 Front End Package Updates
2024-01-24 15:10:54 +00:00
Dave Richer
545a9b2544 Merge branch 'test-beta' into master-beta 2024-01-20 21:23:16 -05:00
Dave Richer
cac4f800ed Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-20 21:22:37 -05:00
Dave Richer
1516633aea Merge branch 'test-beta' into master-beta 2024-01-20 19:25:17 -05:00
Dave Richer
b25df2c7a8 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-20 19:24:53 -05:00
Dave Richer
60acb8e744 Merged in test-beta (pull request #1203)
Test beta
2024-01-19 17:13:01 +00:00
Dave Richer
1cf47b91e2 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1202)
Feature/IO-1828 Front End Package Updates
2024-01-19 17:12:21 +00:00
Dave Richer
a159bdbff1 Merged in test-beta (pull request #1200)
- optimize schedule out today in dashboard
2024-01-19 01:56:36 +00:00
Dave Richer
95c445c40f Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1199)
- optimize schedule out today in dashboard
2024-01-19 01:56:00 +00:00
Dave Richer
703fad7d39 Merged in test-beta (pull request #1196)
Test beta
2024-01-18 21:51:43 +00:00
Dave Richer
913c81409a Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1195)
Feature/IO-1828 Front End Package Updates
2024-01-18 21:51:08 +00:00
Dave Richer
ff0c0de926 Merged in test-beta (pull request #1188)
Test beta
2024-01-18 20:58:07 +00:00
Dave Richer
f6799c0436 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1187)
Feature/IO-1828 Front End Package Updates
2024-01-18 20:57:37 +00:00
Dave Richer
73bb7ffdab Merged in test-beta (pull request #1186)
Test beta
2024-01-18 20:34:37 +00:00
Dave Richer
39a69e60c9 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1185)
Feature/IO-1828 Front End Package Updates
2024-01-18 20:33:04 +00:00
Dave Richer
ef5a3701a0 Merged in test-beta (pull request #1182)
Update CI for test.
2024-01-18 18:49:18 +00:00
Dave Richer
3c0780e410 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1181)
Update CI for test.
2024-01-18 18:48:41 +00:00
Dave Richer
3d5112f545 Merged in test-beta (pull request #1179)
- remove source maps from prod
2024-01-18 18:16:15 +00:00
Dave Richer
4d20ac07e9 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1178)
- remove source maps from prod
2024-01-18 18:15:50 +00:00
Dave Richer
9145149015 Merge branch 'test-beta' into master-beta 2024-01-18 13:07:07 -05:00
Dave Richer
08bbfc6276 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-01-18 13:06:31 -05:00
Dave Richer
664ec1803f Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1176)
- Update build resource
2024-01-18 01:25:35 +00:00
Dave Richer
47f588e003 Merged in test-beta (pull request #1175)
- Generate sourcemaps
2024-01-18 01:22:46 +00:00
Dave Richer
380d7d7170 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1174)
- Generate sourcemaps
2024-01-18 01:22:20 +00:00
Dave Richer
bae5393b60 - fix issue
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-17 20:11:44 -05:00
Dave Richer
47db3fdbcd Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1172)
Feature/IO-1828 Front End Package Updates
2024-01-18 01:08:25 +00:00
Dave Richer
3c7134002d Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1170)
- adjust circle ci
2024-01-18 01:03:21 +00:00
Dave Richer
1c021368d1 Merge branch 'test-beta' into master-beta 2024-01-17 19:57:59 -05:00
Dave Richer
482f48ed6d Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1169)
Feature/IO-1828 Front End Package Updates
2024-01-18 00:54:58 +00:00
Dave Richer
6c1bcfd5cb - progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-17 19:53:04 -05:00
Patrick Fic
3847c44994 Update CI resource class. 2024-01-17 15:08:09 -08:00
Patrick Fic
a994d0dc16 Set CI for beta branch. 2024-01-17 14:43:09 -08:00
Dave Richer
c10a136110 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1163)
- Scoreboard fixes
2024-01-15 23:24:10 +00:00
Dave Richer
b09a1701cd Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1162)
Feature/IO-1828 Front End Package Updates
2024-01-15 22:29:45 +00:00
Dave Richer
bff174fdb6 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1155)
Feature/IO-1828 Front End Package Updates
2024-01-13 00:33:16 +00:00
Dave Richer
231d20149a Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1147)
Update Packages.
2024-01-10 00:05:51 +00:00
Dave Richer
dc3d522443 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1146)
Feature/IO-1828 Front End Package Updates
2024-01-09 23:39:53 +00:00
Dave Richer
1b5ee1d07a Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1144)
Feature/IO-1828 Front End Package Updates
2024-01-08 20:45:25 +00:00
Patrick Fic
1c4879e57b Merge remote-tracking branch 'origin/client-update' into test-beta 2024-01-05 09:44:48 -08:00
138 changed files with 8737 additions and 2218 deletions

View File

@@ -41,7 +41,7 @@ jobs:
app-build:
docker:
- image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client
steps:
@@ -106,7 +106,7 @@ jobs:
test-app-build:
docker:
- image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client
steps:
@@ -217,4 +217,4 @@ workflows:
#- admin-app-build:
#filters:
#branches:
#only: master
#only: master

16
.prettierrc.js Normal file
View File

@@ -0,0 +1,16 @@
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

@@ -0,0 +1,316 @@
## Microsoft Teams
Integrating Microsoft Teams into your Node.js backend and React frontend application can enhance communication and workflow efficiency,
especially in a body shop management context for the automotive industry. Microsoft Teams provides several ways to integrate its functionalities into your application,
including bots, tabs, messaging extensions, webhooks, and connectors. Here's an overview of the options and how to implement them:
### 0.5 - Share to Teams (TM)
- https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/share-to-teams-from-web-apps
- https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/share-to-teams-from-personal-app-or-tab
- https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/device-capabilities/people-picker-capability
- (Example of some Teams code Integrations) https://github.com/microsoft/teams-powerapps-app-templates/
### 1. Microsoft Graph API
The Microsoft Graph API is the gateway to data and intelligence in Microsoft 365, including Teams. It allows you to work with Teams data like channels, messages, and more. You can use it to post status changes back to Teams or read data from Teams to affect your site.
Backend (Node.js): Use the @microsoft/microsoft-graph-client package to make API calls to Teams. You will need to handle authentication with Azure AD, which can be done using the @azure/identity package to get tokens for Graph API requests.
*Implementation Steps*:
Register your application in Azure AD to get the client_id and client_secret.
(Is client_id and client_secret going to be something required for every customer or is it going to be a singleton)
Implement OAuth 2.0 authorization flow to obtain access tokens.
(This will need to be tracked by association to a bodyshop)
bodyshop->Channels | People ->Messages
Use the access token to make requests to the Microsoft Graph API to interact with Teams.
### 2. Webhooks and Connectors
Webhooks allow your application to send notifications to a Teams channel, which is useful for posting status changes. Connectors are a set of predefined webhooks that offer a more integrated experience.
Setup:
In Microsoft Teams, configure an incoming webhook for the channel you want to post messages to.
Use the webhook URL to send JSON payloads from your Node.js backend, which can then be displayed in Teams.
### 3. Bots
Bots in Teams can interact with users to take commands or post information. You can use the Bot Framework along with the Teams activity handler to create bots that can communicate with your React frontend and Node.js backend.
Backend (Node.js): Use the botbuilder package to create and manage your bot's interactions with Teams.
*Implementation Steps*:
Create a bot registration in Azure Bot Services.
Implement the bot logic in your Node.js application using the Bot Framework SDK.
Use the Microsoft Bot Framework's TeamsActivityHandler to respond to Teams-specific activities.
### 4. Tabs
Tabs in Teams allow you to integrate web-based content as part of Teams, which can be your React application or specific parts of it. This is particularly useful for creating a seamless experience within Teams.
*Implementation Steps*:
- Create a Teams app manifest that defines your tab and its configuration.
- Host your React application or the specific parts you want to embed as a tab.
- Use the Teams SDK in your React application to interact with Teams context and APIs.
*Getting Started*:
Microsoft Teams Toolkit for Visual Studio Code: This toolkit simplifies the process of setting up your Teams application, including authentication, configuration, and deployment.
Documentation and Samples: Microsoft provides extensive documentation and sample code for developing Teams applications, which can be invaluable for getting started and solving specific challenges.
Implementing these features requires a good understanding of both the Microsoft Teams platform and your application's architecture.
Start small, perhaps by implementing notifications via webhooks, and gradually add more complex integrations like bots or tabs based on your needs and user feedback.
### Examples:
##### Posting Messages to a Teams Channel Using Incoming Webhooks
1 - Set up an Incoming Webhook in Microsoft Teams:
- Go to the Teams channel where you want to post messages.
- Click on the three dots (...) next to the channel name and select Connectors.
- Search for Incoming Webhook, click Add, and then Configure.
- Name your webhook, upload an image if desired, and click Create.
- Copy the webhook URL provided.
(Patrick Note: The clients might have issues with setting up webhooks, it requires a lot of user configuration and our users are not technical).
2 - Node.js Code to Post a Message:
Ensure you have axios or any HTTP client library installed in your Node.js project. If not, you can install axios via npm: npm install axios.
Use the following code snippet to post a message to the Teams channel:
```javascript
const axios = require('axios');
// Replace 'YOUR_WEBHOOK_URL' with your actual webhook URL
const webhookUrl = 'YOUR_WEBHOOK_URL';
const message = {
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"summary": "Issue 176715375",
"themeColor": "0078D7",
"title": "Issue opened: \"Push notifications not working\"",
"sections": [{
"activityTitle": "A new issue was created",
"activitySubtitle": "Today, 2:36 PM",
"activityImage": "https://example.com/issues/image.png",
"facts": [{
"name": "Repository:",
"value": "Repo name"
},
{
"name": "Issue #:",
"value": "176715375"
}
],
"markdown": true
}]
};
axios.post(webhookUrl, message)
.then(response => console.log('Successfully sent message to Teams channel'))
.catch(error => console.error('Error sending message to Teams channel', error));
```
#### Creating a Simple Bot for Teams
1 Prerequisites:
- Register your bot with the Microsoft Bot Framework and get your bot's App ID and App Password.
- Use the Bot Framework SDK for JavaScript.
2 Install Dependencies:
- You need to install the botbuilder package. Run npm install botbuilder.
The following example demonstrates a simple bot that echoes back received messages.
```javascript
const { BotFrameworkAdapter, MemoryStorage, ConversationState, TurnContext } = require('botbuilder');
// Create adapter.
// See https://aka.ms/about-bot-adapter to learn more about adapters.
const adapter = new BotFrameworkAdapter({
appId: process.env.MicrosoftAppId,
appPassword: process.env.MicrosoftAppPassword
});
// Create conversation state with in-memory storage provider.
const conversationState = new ConversationState(new MemoryStorage());
adapter.use(conversationState);
// Listen for incoming requests.
server.post('/api/messages', (req, res) => {
adapter.processActivity(req, res, async (context) => {
// Echo back what the user said
if (context.activity.type === 'message') {
await context.sendActivity(`You said '${context.activity.text}'`);
}
});
});
```
Running Your Bot:
- Make sure to expose your bot endpoint (/api/messages in this case) to the internet using a service like ngrok.
- Update your bot's messaging endpoint in the Microsoft Bot Framework portal to point to the ngrok URL.
### Slash Commands (Bot)
1 - Create a Microsoft Teams App: You need to develop a Teams app that can interact with users through commands. This involves using the Microsoft Teams Developer Platform and possibly the Bot Framework.
2 - Use Bots for Custom Commands: The primary way to introduce custom slash commands in Teams is through bots. Bots can respond to specific commands (or messages) that are input by users. When you create a bot for Teams, you can define custom commands that the bot will recognize and respond to.
3 - Developing the Bot: You can develop a bot using the Microsoft Bot Framework. This allows your bot to receive and send messages to a Teams channel or chat. Within your bot's code, you can define what actions to take when it receives specific commands.
4 - Register Your Bot with Microsoft Bot Framework: Register your bot with the Microsoft Bot Framework and configure it to work with Microsoft Teams. This step involves getting a Microsoft App ID and password that are necessary for your bot to communicate with the Teams platform.
5 - Add Your Bot to Microsoft Teams: Once your bot is developed and registered, you can package your Teams app (which includes the bot) and upload it to Microsoft Teams. This will make your bot available to users within Teams, where they can interact with it using the custom commands you've defined.
6 - Handling Slash Commands: In the context of your bot's code, you will need to interpret messages that start with a slash (/) as commands. You can then parse the command text and perform the appropriate actions or respond accordingly.
7 - Publish Your App: For broader distribution, you can publish your Teams app to the Teams app store or distribute it within your organization through the Teams admin center.
/ <command> <arb> ....
(Unrelated to teams but then used in teams for functionality)
- A Job has a todo list
- a todo item may have an employee
- a todo item may have a due date
- a todo item may have a priority
--- Call notes ----
- Tasks (TODO List)
- EMail reminders
## Slack
Integrating Slack into your Node.js backend and React frontend application can significantly streamline communication and operations for your automotive industry body shop management software. Slack offers various integration points including bots, apps, webhooks, and its rich API to facilitate interactions between your application and Slack workspace. Here's how you can leverage these integration points:
### 1. **Slack Web API**
The Slack Web API allows you to interact with Slack, enabling functionalities like sending messages, managing channels, and more directly from your application.
- **Backend (Node.js)**: Utilize the `@slack/web-api` package to make API calls from your Node.js backend. This will be the backbone for actions such as posting status updates to Slack channels or handling commands from Slack that can affect your site.
- **Implementation Steps**:
1. Create a Slack app in your Slack workspace and obtain the API tokens.
2. Use the `@slack/web-api` package to authenticate and interact with Slack API endpoints.
3. Implement features such as sending messages or processing events from Slack.
### 2. **Incoming Webhooks**
For simpler integrations focused on sending notifications to Slack channels, incoming webhooks are straightforward and effective. They allow you to send messages to a specific channel without a full-blown app.
- **Setup**:
1. Create an incoming webhook from the Slack app configuration page.
2. Use the webhook URL to send messages from your Node.js backend by making simple HTTP POST requests with your message payload.
### 3. **Bots**
Slack bots can facilitate interactive experiences within your Slack workspace, responding to commands, posting notifications, or even pulling data from your site on demand.
- **Backend (Node.js)**: Leverage the `@slack/bolt` framework, which simplifies creating Slack bots with event handling, messaging, and built-in OAuth support.
- **Implementation Steps**:
1. Create a Slack app and enable bot features.
2. Use the `@slack/bolt` package to develop your bot, handling events like messages or commands.
3. Deploy your bot and set it to listen to incoming events from Slack.
### 4. **Slack Block Kit**
For more engaging and interactive messages, Slack's Block Kit provides a UI framework that allows you to create richly formatted messages, modals, and more.
- **Frontend (React)** and **Backend (Node.js)**: Utilize Slack's Block Kit to design complex messages with buttons, sections, and interactive components. You can send these payloads through the Web API or from bots to enhance your app's interaction with users.
### Getting Started
- **Slack API Documentation and Tools**: Slack's API documentation is comprehensive and includes tutorials, tooling (like Block Kit Builder), and SDK documentation to help you get started.
- **Testing and Development**: Slack provides a sandbox environment for you to test your app's integrations and interactions without affecting your live workspace.
To integrate Slack into your application effectively, start by planning out the interactions you need (e.g., notifications, commands, information retrieval) and map these to the appropriate Slack features. Then, incrementally build and test each integration, utilizing Slack's development tools and your existing Node.js and React knowledge to create a seamless experience for your users.
#### Examples
1. Incoming Webhooks
Incoming Webhooks are a simple way to post messages from your Node.js application into Slack channels. They're perfect for notifying team members about status changes or updates.
Setup:
1 - Create a new Slack app in your workspace and enable incoming webhooks.
2 - Add a new webhook to your app and choose the channel it will post to.
3 - Use the webhook URL to send messages from your backend.
Node.js Example:
```javascript
const axios = require('axios');
const webhookUrl = 'YOUR_SLACK_WEBHOOK_URL';
async function postMessageToSlack(message) {
await axios.post(webhookUrl, {
text: message, // Your message here
});
}
postMessageToSlack('New status update on the automotive project!').catch(console.error);
```
2. Bots
Slack bots can interact with users via messages, respond to commands, and even post updates. They're great for building interactive features.
Setup:
1 - Create a Slack app and add the Bot Token Scopes (like chat:write).
2 - Install the app to your workspace to get your bot token.
3 - Use the @slack/bolt framework for easy bot development in Node.js.
Node.js Example:
```javascript
const { App } = require('@slack/bolt');
const app = new App({
token: process.env.SLACK_BOT_TOKEN, // Set your bot's access token here
signingSecret: process.env.SLACK_SIGNING_SECRET // Set your app's signing secret here
});
app.message('hello', async ({ message, say }) => {
await say(`Hey there <@${message.user}>!`);
});
(async () => {
await app.start(process.env.PORT || 3000);
console.log('Slack bot is running!');
})();
```
3. Slash Commands
Slash Commands allow users to interact with your application directly from Slack by typing commands that start with /.
Setup:
1 - Create a Slack app and configure a new Slash Command (e.g., /statusupdate).
2 - Point the command request URL to your Node.js backend endpoint.
3 - Implement the endpoint to handle the command and respond accordingly.
Node.js Example:
```javascript
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.post('/slack/commands/statusupdate', (req, res) => {
const { text, user_name } = req.body; // Command input and user info
// Logic to handle the command goes here. For example, update a status or fetch information.
res.send(`Status update received: ${text} - from ${user_name}`);
});
app.listen(process.env.PORT || 3000, () => console.log('Server is running'));
```
4. Interactive Components
Interactive components like buttons or menus can make your Slack messages more engaging and interactive.
Setup:
1 - Enable Interactivity in your Slack app settings.
2 - Implement an endpoint in your Node.js backend to handle interactions.
3 - Send messages with interactive components from your backend.
Implementing these features into your Node.js and React application will enable a more dynamic and integrated experience for users of your body shop management software. Start with the simpler integrations like webhooks and progressively incorporate bots and interactive components as needed.
ACTION ITEMS:
- Slot in tasks, this will be a dependency for things we want to do in the future.
- This would involve GUI components
- This would involve a backend component

View File

@@ -0,0 +1,189 @@
# 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

658
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,13 +11,13 @@
"@craco/craco": "^7.1.0",
"@fingerprintjs/fingerprintjs": "^4.2.2",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.1.0",
"@sentry/cli": "^2.28.0",
"@sentry/react": "^7.100.0",
"@sentry/tracing": "^7.100.0",
"@reduxjs/toolkit": "^2.2.1",
"@sentry/cli": "^2.28.6",
"@sentry/react": "^7.104.0",
"@sentry/tracing": "^7.104.0",
"@splitsoftware/splitio-react": "^1.11.0",
"@tanem/react-nprogress": "^5.0.51",
"antd": "^5.14.0",
"antd": "^5.14.2",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"axios": "^1.6.7",
@@ -25,54 +25,54 @@
"dayjs": "^1.11.10",
"dayjs-business-days2": "^1.2.2",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.1",
"dotenv": "^16.4.5",
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^10.8.0",
"firebase": "^10.8.1",
"graphql": "^16.6.0",
"i18next": "^23.8.2",
"i18next": "^23.10.0",
"i18next-browser-languagedetector": "^7.0.2",
"jsoneditor": "^10.0.0",
"jsoneditor": "^10.0.1",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.55",
"logrocket": "^7.0.0",
"libphonenumber-js": "^1.10.57",
"logrocket": "^8.0.1",
"markerjs2": "^2.32.0",
"normalize-url": "^8.0.0",
"phone": "^3.1.42",
"preval.macro": "^5.0.0",
"prop-types": "^15.8.1",
"query-string": "^8.2.0",
"query-string": "^9.0.0",
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"react": "^18.2.0",
"react-big-calendar": "^1.8.7",
"react-big-calendar": "^1.11.0",
"react-color": "^2.19.3",
"react-cookie": "^7.0.2",
"react-cookie": "^7.1.0",
"react-dom": "^18.2.0",
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.0",
"react-grid-layout": "1.3.4",
"react-i18next": "^14.0.4",
"react-i18next": "^14.0.5",
"react-icons": "^5.0.1",
"react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.7.0",
"react-intersection-observer": "^9.8.1",
"react-markdown": "^9.0.1",
"react-number-format": "^5.1.4",
"react-number-format": "^5.3.3",
"react-redux": "^9.1.0",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.22.0",
"react-router-dom": "^6.22.2",
"react-scripts": "^5.0.1",
"react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.5",
"recharts": "^2.11.0",
"recharts": "^2.12.2",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.0",
"sass": "^1.70.0",
"sass": "^1.71.1",
"socket.io-client": "^4.7.4",
"styled-components": "^6.1.8",
"subscriptions-transport-ws": "^0.11.0",
@@ -84,7 +84,7 @@
"workbox-precaching": "^7.0.0",
"workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0",
"yauzl": "^2.10.0"
"yauzl": "^3.1.1"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
@@ -123,9 +123,9 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@sentry/webpack-plugin": "^2.14.0",
"@sentry/webpack-plugin": "^2.14.2",
"@testing-library/cypress": "^10.0.1",
"cypress": "^13.6.4",
"cypress": "^13.6.6",
"eslint-plugin-cypress": "^2.15.1",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",

View File

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

View File

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

View File

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

View File

@@ -5,10 +5,22 @@ import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {DELETE_BILL} from "../../graphql/bills.queries";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import {insertAuditTrail} from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
export default function BillDeleteButton({bill, callback}) {
const mapStateToProps = createStructuredSelector({});
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 {t} = useTranslation();
const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL);
const handleDelete = async () => {
@@ -35,7 +47,12 @@ export default function BillDeleteButton({bill, callback}) {
});
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);
} else {

View File

@@ -30,8 +30,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(
@@ -145,7 +145,8 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
type: "billupdated",
});
await refetch();
form.setFieldsValue(transformData(data));

View File

@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(

View File

@@ -31,8 +31,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({jobid, billid, operation}) =>
dispatch(insertAuditTrail({jobid, billid, operation})),
insertAuditTrail: ({jobid, billid, operation, type}) =>
dispatch(insertAuditTrail({jobid, billid, operation, type })),
});
const Templates = TemplateList("job_special");
@@ -88,6 +88,7 @@ function BillEnterModalContainer({
location,
outstanding_returns,
inventory,
federal_tax_exempt,
...remainingValues
} = values;
@@ -164,7 +165,7 @@ function BillEnterModalContainer({
mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1),
}),
});
type: "jobmodifylbradj",});
});
const jobUpdate = client.mutate({
@@ -313,7 +314,8 @@ function BillEnterModalContainer({
operation: AuditTrailMapping.billposted(
r1.data.insert_bills.returning[0].invoice_number
),
});
type: "billposted",
});
if (enterAgain) {
form.resetFields();

View File

@@ -50,17 +50,17 @@ export function BillsListTableComponent({
const Templates = TemplateList("bill");
const bills = billsQuery.data ? billsQuery.data.bills : [];
const {refetch} = billsQuery;
const { refetch } = billsQuery;
const recordActions = (record, showView = false) => (
<Space wrap>
{showView && (
<Button onClick={() => handleOnRowClick(record)}>
<EditFilled/>
<EditFilled />
</Button>
)}
<BillDeleteButton bill={record}/>
<BillDeleteButton bill={record} jobid={job.id} />
<BillDetailEditReturnComponent
data={{bills_by_pk: {...record, jobid: job.id}}}
data={{ bills_by_pk: { ...record, jobid: job.id } }}
disabled={
record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid ||

View File

@@ -23,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type})),
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
});
@@ -88,7 +88,7 @@ const CardPaymentModalComponent = ({
insertAuditTrail({
jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(),
})
type: "failedpayment",})
);
});
};
@@ -280,24 +280,22 @@ const CardPaymentModalComponent = ({
<Input
className="ipayfield"
data-ipayname="account"
//type="hidden"
type="hidden"
value={
payments && data && data.jobs.length > 0
? data.jobs.map((j) => j.ro_number).join(", ")
: null
}
hidden
/>
<Input
className="ipayfield"
data-ipayname="email"
// type="hidden"
type="hidden"
value={
payments && data && data.jobs.length > 0
? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea
: null
}
hidden
/>
</>
);
@@ -325,9 +323,8 @@ const CardPaymentModalComponent = ({
<Input
className="ipayfield"
data-ipayname="amount"
//type="hidden"
type="hidden"
value={totalAmountToCharge?.toFixed(2)}
hidden
/>
<Button
type="primary"

View File

@@ -66,7 +66,30 @@ export default function ContractFormComponent({
<FormDateTimePicker/>
</Form.Item>
)}
</LayoutFormRow>
{create && (
<Form.Item
shouldUpdate={(p, c) => p.scheduledreturn !== c.scheduledreturn}
>
{() => {
const insuranceOver =
selectedCar &&
selectedCar.insuranceexpires &&
dayjs(selectedCar.insuranceexpires)
.endOf("day")
.isBefore(dayjs(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 grow>
<Form.Item
label={t("contracts.fields.kmstart")}
@@ -88,16 +111,17 @@ export default function ContractFormComponent({
>
{() => {
const mileageOver =
selectedCar &&
selectedCar.nextservicekm <= form.getFieldValue("kmstart");
selectedCar && selectedCar.nextservicekm
? selectedCar.nextservicekm <= form.getFieldValue("kmstart")
: false;
const dueForService =
selectedCar &&
selectedCar.nextservicedate &&
dayjs(selectedCar.nextservicedate).isBefore(
dayjs(selectedCar.nextservicedate)
.endOf("day")
.isSameOrBefore(
dayjs(form.getFieldValue("scheduledreturn"))
);
);
if (mileageOver || dueForService)
return (
<Space direction="vertical" style={{color: "tomato"}}>

View File

@@ -1,22 +1,34 @@
import {SyncOutlined, WarningFilled} from "@ant-design/icons";
import {Button, Card, Dropdown, Input, Space, Table, Tooltip,} from "antd";
import { SyncOutlined, WarningFilled } from "@ant-design/icons";
import {
Button,
Card,
Dropdown,
Input,
Space,
Table,
Tooltip,
} from "antd";
import dayjs from "../../utils/day";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {Link} from "react-router-dom";
import {DateTimeFormatter} from "../../utils/DateFormatter";
import {GenerateDocument} from "../../utils/RenderTemplate";
import {TemplateList} from "../../utils/TemplateConstants";
import {alphaSort} from "../../utils/sorters";
import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
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({
sortedInfo: {},
filteredInfo: {text: ""},
});
const [searchText, setSearchText] = useState("");
const {t} = useTranslation();
const [filter, setFilter] = useLocalStorage(
"filter_courtesy_cars_list",
null
);
const { t } = useTranslation();
const columns = [
{
@@ -42,6 +54,7 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
filteredValue: filter?.status || null,
filters: [
{
text: t("courtesycars.status.in"),
@@ -64,19 +77,34 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => {
const {nextservicedate, nextservicekm, mileage} = record;
const { nextservicedate, nextservicekm, mileage, insuranceexpires } =
record;
const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
const dueForService =
nextservicedate && dayjs(nextservicedate).endOf('day').isSameOrBefore(dayjs());
const insuranceOver =
insuranceexpires &&
dayjs(insuranceexpires).endOf("day").isBefore(dayjs());
return (
<Space>
{t(record.status)}
{(mileageOver || dueForService) && (
<Tooltip title={t("contracts.labels.cardueforservice")}>
<WarningFilled style={{color: "tomato"}}/>
{(mileageOver || dueForService || insuranceOver) && (
<Tooltip
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" }} />
</Tooltip>
)}
</Space>
@@ -88,6 +116,7 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
dataIndex: "readiness",
key: "readiness",
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
filteredValue: filter?.readiness || null,
filters: [
{
text: t("courtesycars.readiness.ready"),
@@ -203,7 +232,8 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
];
const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter});
setState({ ...state, sortedInfo: sorter });
setFilter(filters);
};
const tableData = searchText

View File

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

View File

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

View File

@@ -0,0 +1,169 @@
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 dayjs from '../../../utils/day';
import DashboardRefreshRequired from "../refresh-required.component";
import axios from "axios";
const fortyFiveDaysAgo = () => dayjs().subtract(45, 'day').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_order_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: "${dayjs().subtract(45, 'day').toISOString()}"
}
}) {
id
actual_in
} `;

View File

@@ -1,221 +1,469 @@
import {BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined,} from "@ant-design/icons";
import {Card, Space, Table, Tooltip} from "antd";
import {
BranchesOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
import dayjs from "../../../utils/day";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {Link} from "react-router-dom";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
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 OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component";
import {pageLimit} from "../../../utils/config";
export default function DashboardScheduledInToday({data, ...cardProps}) {
const {t} = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
if (!data) return null;
if (!data.scheduled_in_today)
return <DashboardRefreshRequired {...cardProps} />;
export default function DashboardScheduledInToday({ data, ...cardProps }) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const appt = []; // Flatten Data
data.scheduled_in_today.forEach((item) => {
if (item.job) {
var i = {
canceled: item.canceled,
id: item.id,
alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no,
jobid: item.job.jobid,
ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent,
ownerid: item.job.ownerid,
ownr_co_nm: item.job.ownr_co_nm,
ownr_ea: item.job.ownr_ea,
ownr_fn: item.job.ownr_fn,
ownr_ln: item.job.ownr_ln,
ownr_ph1: item.job.ownr_ph1,
ownr_ph2: item.job.ownr_ph2,
production_vars: item.job.production_vars,
ro_number: item.job.ro_number,
suspended: item.job.suspended,
v_make_desc: item.job.v_make_desc,
v_model_desc: item.job.v_model_desc,
v_model_yr: item.job.v_model_yr,
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
start: dayjs(item.start).format("hh:mm a"),
title: item.title,
};
appt.push(i);
}
});
appt.sort(function (a, b) {
return new dayjs(a.start) - new dayjs(b.start);
});
const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage(
"isTvModeScheduledIn",
false
);
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.jobid}
onClick={(e) => e.stopPropagation()}
>
<Space>
{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>
)}
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record}/>
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record}/>
</span>
);
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid}/>
),
},
{
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}/>
),
},
{
title: t("jobs.fields.ownr_ea"),
dataIndex: "ownr_ea",
key: "ownr_ea",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid}/>
),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
},
{
title: t("jobs.fields.ins_co_nm"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
},
{
title: t("appointments.fields.time"),
dataIndex: "start",
key: "start",
ellipsis: true,
responsive: ["md"],
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
responsive: ["md"],
},
];
if (!data) return null;
if (!data.scheduled_in_today)
return <DashboardRefreshRequired {...cardProps} />;
const handleTableChange = (sorter) => {
setState({...state, sortedInfo: sorter});
};
const appt = []; // Flatten Data
data.scheduled_in_today.forEach((item) => {
if (item.job) {
var i = {
canceled: item.canceled,
id: item.id,
alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no,
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,
iouparent: item.job.iouparent,
ownerid: item.job.ownerid,
ownr_co_nm: item.job.ownr_co_nm,
ownr_ea: item.job.ownr_ea,
ownr_fn: item.job.ownr_fn,
ownr_ln: item.job.ownr_ln,
ownr_ph1: item.job.ownr_ph1,
ownr_ph2: item.job.ownr_ph2,
production_vars: item.job.production_vars,
ro_number: item.job.ro_number,
suspended: item.job.suspended,
v_make_desc: item.job.v_make_desc,
v_model_desc: item.job.v_model_desc,
v_model_yr: item.job.v_model_yr,
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
start: item.start,
title: item.title,
};
appt.push(i);
}
});
appt.sort(function (a, b) {
return dayjs(a.start) - dayjs(b.start);
});
return (
<Card
title={t("dashboard.titles.scheduledintoday", {
date: dayjs().startOf("day").format("MM/DD/YYYY"),
})}
{...cardProps}
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()}
>
<div style={{height: "100%"}}>
<Table
onChange={handleTableChange}
pagination={{position: "top", defaultPageSize: pageLimit}}
columns={columns}
scroll={{x: true, y: "calc(100% - 2em)"}}
rowKey="id"
style={{height: "85%"}}
dataSource={appt}
/>
</div>
</Card>
);
<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 = [
{
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"),
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>
{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>
)}
</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()}
>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("dashboard.labels.phone"),
dataIndex: "ownr_ph",
key: "ownr_ph",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<Space size="small" wrap>
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
</Space>
),
},
{
title: t("jobs.fields.ownr_ea"),
dataIndex: "ownr_ea",
key: "ownr_ea",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
),
},
{
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()}
>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
},
{
title: t("jobs.fields.ins_co_nm"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder:
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
filters:
(appt &&
appt
.map((j) => j.ins_co_nm)
.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"),
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),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Card
title={t("dashboard.titles.scheduledindate", {
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}
>
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}
pagination={false}
columns={isTvModeScheduledIn ? tvColumns : columns}
scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id"
style={{ height: "85%" }}
dataSource={appt}
size={isTvModeScheduledIn ? "small" : "middle"}
/>
</div>
</Card>
);
}
export const DashboardScheduledInTodayGql = `
scheduled_in_today: appointments(where: {start: {_gte: "${dayjs()
.startOf("day")
.toISOString()}", _lte: "${dayjs()
.endOf("day")
.toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) {
.endOf("day")
.toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) {
canceled
id
job {
alt_transport
clm_no
jobid: id
joblines(where: {removed: {_eq: false}}) {
mod_lb_hrs
mod_lbr_ty
}
ins_co_nm
iouparent
ownerid

View File

@@ -1,37 +1,273 @@
import {BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined,} from "@ant-design/icons";
import {Card, Space, Table, Tooltip} from "antd";
import {Card, Space, Switch, Table, Tooltip, Typography} from "antd";
import dayjs from "../../../utils/day";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
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 OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, {OwnerNameDisplayFunction,} from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component";
import {pageLimit} from "../../../utils/config";
export default function DashboardScheduledOutToday({data, ...cardProps}) {
const {t} = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
filteredInfo: {},});
const [isTvModeScheduledOut, setIsTvModeScheduledOut] = useLocalStorage(
"isTvModeScheduledOut",
false
);
if (!data) return null;
if (!data.scheduled_out_today)
return <DashboardRefreshRequired {...cardProps} />;
const filteredScheduledOutToday = data.scheduled_out_today.map((item) => {
const scheduledOutToday = data.scheduled_out_today.map((item) => {
const joblines_body = item.joblines
? item.joblines
.filter((l) => l.mod_lbr_ty !== "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
: 0;
const joblines_ref = item.joblines
? item.joblines
.filter((l) => l.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
: 0;
return {
...item,
scheduled_completion: dayjs(item.scheduled_completion).format("hh:mm a"),
timestamp: dayjs(item.scheduled_completion).valueOf(),
}
}).sort((a, b) => a.timestamp - b.timestamp);
joblines_body,
joblines_ref,
};
});
const columns = [
console.log('Scheduled Out Today')
console.dir(scheduledOutToday);
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) => {
console.log('Render record out today');
console.dir(record);
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<span style={{fontSize: tvFontSize, fontWeight: tvFontWeight}}>
<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:
(scheduledOutToday &&
scheduledOutToday
.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:
(scheduledOutToday &&
scheduledOutToday
.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 = [
{
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"),
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()}
@@ -58,7 +294,10 @@ export default function DashboardScheduledOutToday({data, ...cardProps}) {
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
@@ -75,24 +314,16 @@ export default function DashboardScheduledOutToday({data, ...cardProps}) {
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
title: t("dashboard.labels.phone"),
dataIndex: "ownr_ph",
key: "ownr_ph",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
render: (text, record) => (<Space size="small" wrap>
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid}/>
),
},
{
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}/>
),
</Space>),
},
{
title: t("jobs.fields.ownr_ea"),
@@ -101,7 +332,7 @@ export default function DashboardScheduledOutToday({data, ...cardProps}) {
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid}/>
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
),
},
{
@@ -109,7 +340,15 @@ export default function DashboardScheduledOutToday({data, ...cardProps}) {
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
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}
@@ -132,43 +371,78 @@ export default function DashboardScheduledOutToday({data, ...cardProps}) {
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
},
{
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder:
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
filters:
(scheduledOutToday &&
scheduledOutToday
.map((j) => j.ins_co_nm)
.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"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
responsive: ["md"],
},
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(scheduledOutToday &&
scheduledOutToday
.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 = (sorter) => {
setState({...state, sortedInfo: sorter});
const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter});
};
return (
<Card
title={t("dashboard.titles.scheduledouttoday", {
title={t("dashboard.titles.scheduledoutdate", {
date: dayjs().startOf("day").format("MM/DD/YYYY"),
})}
{...cardProps}
extra={
<Space>
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
<Switch
onClick={() => setIsTvModeScheduledOut(!isTvModeScheduledOut)}
defaultChecked={isTvModeScheduledOut}
/>
</Space>
}{...cardProps}
>
<div style={{height: "100%"}}>
<Table
onChange={handleTableChange}
pagination={{position: "top", defaultPageSize: pageLimit}}
columns={columns}
pagination={false}
columns={isTvModeScheduledOut ? tvColumns : columns}
scroll={{x: true, y: "calc(100% - 2em)"}}
rowKey="id"
style={{height: "85%"}}
dataSource={filteredScheduledOutToday}
dataSource={scheduledOutToday}
size={isTvModeScheduledOut ? "small" : "middle"}
/>
</div>
</Card>
@@ -185,6 +459,10 @@ export const DashboardScheduledOutTodayGql = `
alt_transport
clm_no
jobid: id
joblines(where: {removed: {_eq: false}}) {
mod_lb_hrs
mod_lbr_ty
}
ins_co_nm
iouparent
ownerid
@@ -197,6 +475,7 @@ export const DashboardScheduledOutTodayGql = `
production_vars
ro_number
scheduled_completion
status
suspended
v_make_desc
v_model_desc

View File

@@ -42,6 +42,9 @@ import DashboardScheduledInToday, {
import DashboardScheduledOutToday, {
DashboardScheduledOutTodayGql,
} 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 {GenerateDashboardData} from "./dashboard-grid.utils";
@@ -260,6 +263,7 @@ const componentList = {
w: 2,
h: 2,
},
// Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings
MonthlyEmployeeEfficency: {
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
component: DashboardMonthlyEmployeeEfficiency,
@@ -270,26 +274,31 @@ const componentList = {
h: 2,
},
ScheduleInToday: {
label: i18next.t("dashboard.titles.scheduledintoday", {
date: dayjs().startOf("day").format("MM/DD/YYYY"),
}),
label: i18next.t("dashboard.titles.scheduledintoday"),
component: DashboardScheduledInToday,
gqlFragment: DashboardScheduledInTodayGql,
minW: 10,
minW: 6,
minH: 2,
w: 10,
h: 2,
h: 3,
},
ScheduleOutToday: {
label: i18next.t("dashboard.titles.scheduledouttoday", {
date: dayjs().startOf("day").format("MM/DD/YYYY"),
}),
label: i18next.t("dashboard.titles.scheduledouttoday"),
component: DashboardScheduledOutToday,
gqlFragment: DashboardScheduledOutTodayGql,
minW: 10,
minW: 6,
minH: 2,
w: 10,
h: 2,
h: 3,
},
JobLifecycle: {
label: i18next.t("dashboard.titles.joblifecycle"),
component: JobLifecycleDashboardComponent,
gqlFragment: JobLifecycleDashboardGQL,
minW: 6,
minH: 3,
w: 6,
h: 3,
},
};
@@ -301,8 +310,7 @@ const createDashboardQuery = (state) => {
.map((item, index) => componentList[item.i].gqlFragment || "")
.join("");
return gql`
query QUERY_DASHBOARD_DETAILS {
${componentBasedAdditions || ""}
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
monthly_sales: jobs(where: {_and: [
{ voided: {_eq: false}},
{date_invoiced: {_gte: "${dayjs()
@@ -312,7 +320,7 @@ const createDashboardQuery = (state) => {
.endOf("month")
.endOf("day")
.toISOString()}"}}]}) {
id
id
ro_number
date_invoiced
job_totals
@@ -376,6 +384,5 @@ const createDashboardQuery = (state) => {
}
}
}
}
`;
}`;
};

View File

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

View File

@@ -36,7 +36,7 @@ const DateTimePicker = (
disabledDate: (d) => dayjs().isAfter(d),
})}
onChange={onChange}
showSecond={false}
disableSeconds={true}
minuteStep={15}
onBlur={onBlur}
format="hh:mm a"

View File

@@ -53,7 +53,7 @@ export default function ScheduleEventContainer({bodyshop, event, refetch}) {
insertAuditTrail({
jobid: event.job.id,
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
})
type: "appointmentcancel",})
);
}
if (!!jobUpdate.errors) {

View File

@@ -28,8 +28,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function JobChecklistForm({
@@ -183,7 +183,7 @@ export function JobChecklistForm({
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered)
),
});
type: "jobchecklist",});
} else {
notification["error"]({
message: t("checklist.errors.complete", {

View File

@@ -18,10 +18,8 @@ export default function JobDetailCardsTotalsComponent({loading, data}) {
/>
<Statistic
className="imex-flex-row__margin-large"
title={t("jobs.fields.ded_amt")}
value={Dinero({
amount: Math.round((data.ded_amt || 0) * 100),
}).toFormat()}
title={t("jobs.fields.customerowing")}
value={Dinero(data.job_totals.totals.custPayable.total).toFormat()}
/>
<Statistic
className="imex-flex-row__margin-large"

View File

@@ -15,8 +15,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(
mapStateToProps,
@@ -47,7 +47,8 @@ export function JobEmployeeAssignmentsContainer({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentchange(operation, name),
});
type: "jobassignmentchange",
});
if (!!result.errors) {
notification["error"]({
@@ -77,7 +78,7 @@ export function JobEmployeeAssignmentsContainer({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentremoved(operation),
});
type: "jobassignmentremoved",});
setLoading(false);
};

View File

@@ -17,8 +17,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(
mapStateToProps,
@@ -102,7 +102,7 @@ export function JobLineConvertToLabor({
hours: calculateAdjustment({mod_lbr_ty, job, jobline}).toFixed(1),
mod_lbr_ty,
}),
});
type: "jobmodifylbradj",});
setLoading(false);
setVisibility(false);
};

View File

@@ -42,6 +42,7 @@ export default function ScoreboardAddButton({
const handleFinish = async (values) => {
logImEXEvent("job_close_add_to_scoreboard");
values.date = dayjs(values.date).format("YYYY-MM-DD");
setLoading(true);
let result;
@@ -169,7 +170,7 @@ export default function ScoreboardAddButton({
return acc + job.lbr_adjustments[val];
}, 0);
form.setFieldsValue({
date: new dayjs(),
date: dayjs(),
bodyhrs: Math.round(v.bodyhrs * 10) / 10,
painthrs: Math.round(v.painthrs * 10) / 10,
});

View File

@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
@@ -32,7 +32,7 @@ export function JobsAdminStatus({insertAuditTrail, bodyshop, job}) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobstatuschange(status),
});
type: "admin_jobstatuschange",});
// refetch();
})
.catch((error) => {

View File

@@ -20,8 +20,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(
@@ -57,7 +57,7 @@ export function JobsAdminDatesChange({insertAuditTrail, job}) {
? DateTimeFormat(changedAuditFields[key])
: changedAuditFields[key]
),
});
type: "admin_jobfieldchange",});
});
if (!!!result.errors) {

View File

@@ -24,8 +24,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(
mapStateToProps,
@@ -60,7 +60,7 @@ export function JobAdminMarkReexport({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkforreexport(),
});
type: "admin_jobmarkforreexport",});
} else {
notification["error"]({
message: t("jobs.errors.saving", {
@@ -100,7 +100,7 @@ export function JobAdminMarkReexport({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkexported(),
});
type: "admin_jobmarkexported",});
} else {
notification["error"]({
message: t("jobs.errors.saving", {
@@ -125,7 +125,7 @@ export function JobAdminMarkReexport({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobuninvoice(),
});
type: "admin_jobuninvoice",});
} else {
notification["error"]({
message: t("jobs.errors.saving", {

View File

@@ -10,8 +10,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR);
@@ -34,7 +34,7 @@ export function JobsAdminRemoveAR({insertAuditTrail, job}) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_job_remove_from_ar(value),
});
type: "admin_job_remove_from_ar",});
setSwitchValue(value);
} else {
notification["error"]({

View File

@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
@@ -46,6 +46,7 @@ export function JobsAdminUnvoid({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobunvoid(),
type: "admin_jobunvoid",
});
} else {
notification["error"]({

View File

@@ -1,6 +1,6 @@
import {gql, useApolloClient, useLazyQuery, useMutation, useQuery,} from "@apollo/client";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import {Col, notification, Row} from "antd";
import {Col, Row, notification} from "antd";
import Axios from "axios";
import Dinero from "dinero.js";
import dayjs from "../../utils/day";
@@ -21,8 +21,8 @@ import {INSERT_NEW_NOTE} from "../../graphql/notes.queries";
import {SEARCH_VEHICLE_BY_VIN} from "../../graphql/vehicles.queries";
import {insertAuditTrail} from "../../redux/application/application.actions";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
import confirmDialog from "../../utils/asyncConfirm";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import confirmDialog from "../../utils/asyncConfirm";
import CriticalPartsScan from "../../utils/criticalPartsScan";
import AlertComponent from "../alert/alert.component";
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
@@ -38,8 +38,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export function JobsAvailableContainer({bodyshop, currentUser, insertAuditTrail,}) {
@@ -63,7 +63,15 @@ export function JobsAvailableContainer({bodyshop, currentUser, insertAuditTrail,
const [selectedJob, setSelectedJob] = useState(null);
const [selectedOwner, setSelectedOwner] = useState(null);
const [partsQueueToggle, setPartsQueueToggle] = useState(bodyshop.md_functionality_toggles.parts_queue_toggle);
const [partsQueueToggle, setPartsQueueToggle] = useState(
bodyshop.md_functionality_toggles.parts_queue_toggle
);
const [updateSchComp, setSchComp] = useState({
actual_in: dayjs(),
checked: false,
scheduled_completion: dayjs(),
automatic: false,
});
const [insertLoading, setInsertLoading] = useState(false);
@@ -169,10 +177,11 @@ export function JobsAvailableContainer({bodyshop, currentUser, insertAuditTrail,
});
//Job has been inserted. Clean up the available jobs record.
insertAuditTrail({
jobid: r.data.insert_jobs.returning[0].id,
operation: AuditTrailMapping.jobimported(),
});
insertAuditTrail({
jobid: r.data.insert_jobs.returning[0].id,
operation: AuditTrailMapping.jobimported(),
type: "jobimported",
});
deleteJob({
variables: {id: estData.id},
@@ -187,8 +196,10 @@ export function JobsAvailableContainer({bodyshop, currentUser, insertAuditTrail,
notification["error"]({
message: t("jobs.errors.creating", {error: err.message}),
});
refetch().catch(e => {
console.error(`Something went wrong in jobs available table container - ${err.message || ''}`)
refetch().catch((e) => {
console.error(`Something went wrong in jobs available table container - ${err.message || ""
}`
);
});
setInsertLoading(false);
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
@@ -217,6 +228,23 @@ export function JobsAvailableContainer({bodyshop, currentUser, insertAuditTrail,
//IO-539 Check for Parts Rate on PAL for SGI use case.
await CheckTaxRates(supp, bodyshop);
if (updateSchComp.checked === true) {
if (updateSchComp.automatic === true) {
const job_hrs = supp.joblines.data.reduce(
(acc, val) => acc + val.mod_lb_hrs,
0
);
const num_days = job_hrs / bodyshop.target_touchtime;
supp.actual_in = updateSchComp.actual_in;
supp.scheduled_completion = dayjs(
updateSchComp.actual_in
).businessDaysAdd(num_days,
"day"
);
} else {
supp.scheduled_completion = updateSchComp.scheduled_completion;
}
}
delete supp.owner;
delete supp.vehicle;
delete supp.ins_co_nm;
@@ -298,24 +326,25 @@ export function JobsAvailableContainer({bodyshop, currentUser, insertAuditTrail,
setInsertLoading(false);
});
await insertNote({
variables: {
noteInput: [
{
jobid: selectedJob,
created_by: currentUser.email,
audit: true,
text: t("jobs.labels.supplementnote"),
},
],
},
});
insertAuditTrail({
jobid: selectedJob,
operation: AuditTrailMapping.jobsupplement(),
});
}
};
await insertNote({
variables: {
noteInput: [
{
jobid: selectedJob,
created_by: currentUser.email,
audit: true,
text: t("jobs.labels.supplementnote"),
},
],
},
});
insertAuditTrail({
jobid: selectedJob,
operation: AuditTrailMapping.jobsupplement(),
type: "jobsupplement",
});
}
};
const owner =
estDataRaw.data &&
@@ -389,7 +418,8 @@ export function JobsAvailableContainer({bodyshop, currentUser, insertAuditTrail,
onCancel={onJobModalCancel}
modalSearchState={modalSearchState}
partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle} updateSchComp={updateSchComp}
setSchComp={setSchComp}
/>
<Row gutter={[16, 16]}>
<Col span={24}>

View File

@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function JobsChangeStatus({job, bodyshop, jobRO, insertAuditTrail}) {
@@ -35,7 +35,7 @@ export function JobsChangeStatus({job, bodyshop, jobRO, insertAuditTrail}) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobstatuschange(status),
});
type: "jobstatuschange",});
// refetch();
})
.catch((error) => {

View File

@@ -9,10 +9,12 @@ import {createStructuredSelector} from "reselect";
import {auth, logImEXEvent} from "../../firebase/firebase.utils";
import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries";
import {UPDATE_JOB} from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
function updateJobCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
@@ -40,9 +47,10 @@ export function JobsCloseExportButton({
disabled,
setSelectedJobs,
refetch,
insertAuditTrail,
}) {
const history = useNavigate();
const {t} = useTranslation();
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOB);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [loading, setLoading] = useState(false);
@@ -181,6 +189,10 @@ export function JobsCloseExportButton({
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),
type: "jobexported",});
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
);
@@ -192,12 +204,20 @@ export function JobsCloseExportButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
if (
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
successfulTransactions.length > 0
) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),type: "jobexported",
});
updateJobCache([
...new Set(
successfulTransactions.map(
@@ -227,4 +247,7 @@ export function JobsCloseExportButton({
);
}
export default connect(mapStateToProps, null)(JobsCloseExportButton);
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsCloseExportButton);

View File

@@ -17,8 +17,8 @@ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function JobsConvertButton({
@@ -70,7 +70,8 @@ export function JobsConvertButton({
operation: AuditTrailMapping.jobconverted(
res.data.update_jobs.returning[0].ro_number
),
});
type: "jobconverted",
});
setOpen(false);
}

View File

@@ -29,6 +29,7 @@ export default function AddToProduction(
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobinproductionchange(!remove),
type: "jobinproductionchange",
})
);
if (completionCallback) completionCallback();
@@ -40,8 +41,4 @@ export default function AddToProduction(
}),
});
});
//insert the new job. call the callback with the returned ID when done.
return;
}

View File

@@ -48,8 +48,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({context: context, modal: "timeTicket"})),
setCardPaymentContext: (context) =>
dispatch(setModalContext({context: context, modal: "cardPayment"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
setMessage: (text) => dispatch(setMessage(text)),
@@ -237,6 +237,11 @@ export function JobsDetailHeaderActions({
message: JSON.stringify(result.errors),
}),
});
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobvoid(),
type: "jobvoid",
});
return;
}
if (e.key === "email")
@@ -351,6 +356,11 @@ export function JobsDetailHeaderActions({
notification["success"]({
message: t("jobs.successes.voided"),
});
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobvoid(),
type: "jobvoid",
});
//go back to jobs list.
history(`/manage/`);
} else {
@@ -467,9 +477,29 @@ export function JobsDetailHeaderActions({
? !job.production_vars.alert
: true
),
type: "alertToggle",
});
};
const handleSuspend = (e) => {
logImEXEvent("production_toggle_alert");
//e.stopPropagation();
updateJob({
variables: {
jobId: job.id,
job: {
suspended: !job.suspended,
},
},
});
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobsuspend(
!!job.suspended ? !job.suspended : true
),
type: "jobsuspend",
});
};
// Function to handle OK
const handleCancelScheduleOK = async () => {
@@ -499,24 +529,12 @@ export function JobsDetailHeaderActions({
jobid: job.id,
operation:
AuditTrailMapping.appointmentcancel(lost_sale_reason),
type: "appointmentcancel",
});
}
};
const handleSuspend = (e) => {
logImEXEvent("production_toggle_alert");
//e.stopPropagation();
updateJob({
variables: {
jobId: job.id,
job: {
suspended: !job.suspended,
},
},
});
};
const popOverContent = (
<Card>
<div>

View File

@@ -1,5 +1,5 @@
import {BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled,} from "@ant-design/icons";
import {Card, Col, Row, Space, Tag, Tooltip} from "antd";
import {Card, Col, Divider, Row, Space, Tag, Tooltip} from "antd";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
@@ -22,6 +22,7 @@ import ProductionListColumnProductionNote
from "../production-list-columns/production-list-columns.productionnote.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
import "./jobs-detail-header.styles.scss";
import dayjs from "../../utils/day";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -58,6 +59,13 @@ export function JobsDetailHeader({job, bodyshop, disabled}) {
${job.v_make_desc || ""}
${job.v_model_desc || ""}`.trim();
const bodyHrs = job.joblines
.filter((j) => j.mod_lbr_ty !== "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
const refinishHrs = job.joblines
.filter((line) => line.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
const ownerTitle = OwnerNameDisplayFunction(job).trim();
return (
@@ -89,7 +97,9 @@ export function JobsDetailHeader({job, bodyshop, disabled}) {
{job.status === bodyshop.md_ro_statuses.default_scheduled &&
job.scheduled_in ? (
<Tag>
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
<Link to={`/manage/schedule?date=${dayjs(job.scheduled_in).format('YYYY-MM-DD')}`}>
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
</Link>
</Tag>
) : null}
</Space>
@@ -119,11 +129,14 @@ export function JobsDetailHeader({job, bodyshop, disabled}) {
</DataLabel>
{job?.cccontracts?.length > 0 && (
<DataLabel label={t("jobs.labels.contracts")}>
{job.cccontracts.map((c) => (
{job.cccontracts.map((c, index) => (
<Space wrap>
<Link
key={c.id}
to={`/manage/courtesycars/contracts/${c.id}`}
>{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}</Link>
>{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}{index !== job.cccontracts.length - 1 ? "," : null}
</Link>
</Space>
))}
</DataLabel>
)}
@@ -132,7 +145,7 @@ export function JobsDetailHeader({job, bodyshop, disabled}) {
<ProductionListColumnProductionNote record={job}/>
</DataLabel>
<Space>
<Space wrap>
{job.special_coverage_policy && (
<Tag color="tomato">
<Space>
@@ -292,6 +305,11 @@ export function JobsDetailHeader({job, bodyshop, disabled}) {
>
<div>
<JobEmployeeAssignments job={job}/>
<Divider style={{ margin: ".5rem" }} />
<DataLabel label={t("jobs.labels.labor_hrs")}>
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} /{" "}
{(bodyHrs + refinishHrs).toFixed(1)}
</DataLabel>
</div>
</Card>
</Col>

View File

@@ -9,7 +9,12 @@ import {createStructuredSelector} from "reselect";
import {auth, logImEXEvent} from "../../firebase/firebase.utils";
import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries";
import {UPDATE_JOBS} from "../../graphql/jobs.queries";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({
@@ -17,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
function updateJobCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
@@ -38,8 +48,9 @@ export function JobsExportAllButton({
loadingCallback,
completedCallback,
refetch,
insertAuditTrail,
}) {
const {t} = useTranslation();
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOBS);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
@@ -168,47 +179,66 @@ export function JobsExportAllButton({
},
});
if (!!!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map(
(job) => job.id
)
);
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
updateJobCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "jobid"
: "id"
]
)
),
]);
}
}
})
);
if (!!!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
jobUpdateResponse.data.update_jobs.returning.forEach((job) => {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobexported(),
type: "jobexported",
});
});
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map(
(job) => job.id
)
);
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
}
if (
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
successfulTransactions.length > 0
) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
const successfulTransactionsSet = [
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "jobid"
: "id"
]
)
),
];
if (successfulTransactionsSet.length > 0) {
insertAuditTrail({
jobid: successfulTransactionsSet[0],
operation: AuditTrailMapping.jobexported(),
type: "jobexported",
});
}
updateJobCache(successfulTransactionsSet);
}
}
})
);
if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false);
@@ -222,4 +252,7 @@ export function JobsExportAllButton({
);
}
export default connect(mapStateToProps, null)(JobsExportAllButton);
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsExportAllButton);

View File

@@ -1,9 +1,11 @@
import {SyncOutlined} from "@ant-design/icons";
import {Button, Checkbox, Divider, Input, Table} from "antd";
import React from "react";
import {Button, Checkbox, Divider, Input, Space, Table} from "antd";
import dayjs from "../../utils/day";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {Link} from "react-router-dom";
import PhoneFormatter from "../../utils/PhoneFormatter";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function JobsFindModalComponent({
@@ -16,11 +18,13 @@ export default function JobsFindModalComponent({
jobsListRefetch,
partsQueueToggle,
setPartsQueueToggle,
updateSchComp,
setSchComp,
}) {
const {t} = useTranslation();
const [modalSearch, setModalSearch] = modalSearchState;
const [importOptions, setImportOptions] = importOptionsState;
const [checkUTT, setCheckUTT] = useState(false);
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -142,6 +146,35 @@ export default function JobsFindModalComponent({
if (record) {
if (record.id) {
setSelectedJob(record.id);
if (record.actual_in && record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: record.actual_in,
scheduled_completion: record.scheduled_completion,
});
} else {
if (record.actual_in && !record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: record.actual_in,
scheduled_completion: dayjs(),
});
}
if (!record.actual_in && record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: dayjs(),
scheduled_completion: dayjs(record.scheduled_completion),
});
}
if (!record.actual_in && !record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: dayjs(),
scheduled_completion: dayjs(),
});
}
}
return;
}
}
@@ -177,6 +210,35 @@ export default function JobsFindModalComponent({
rowSelection={{
onSelect: (props) => {
setSelectedJob(props.id);
if (props.actual_in && props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: props.actual_in,
scheduled_completion: props.scheduled_completion,
});
} else {
if (props.actual_in && !props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: props.actual_in,
scheduled_completion: dayjs(),
});
}
if (!props.actual_in && props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: dayjs(),
scheduled_completion: dayjs(props.scheduled_completion),
});
}
if (!props.actual_in && !props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: dayjs(),
scheduled_completion: dayjs(),
});
}
}
},
type: "radio",
selectedRowKeys: [selectedJob],
@@ -189,7 +251,7 @@ export default function JobsFindModalComponent({
};
}}
/>
<Divider/>
<Divider/><Space>
<Checkbox
defaultChecked={importOptions.overrideHeader}
onChange={(e) =>
@@ -206,7 +268,40 @@ export default function JobsFindModalComponent({
onChange={(e) => setPartsQueueToggle(e.target.checked)}
>
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
</Checkbox>
</Checkbox><Checkbox
checked={updateSchComp.checked}
onChange={(e) =>
setSchComp({...updateSchComp, checked: e.target.checked})
}
>
{t("jobs.labels.update_scheduled_completion")}
</Checkbox>
{updateSchComp.checked === true ? (
<>
{checkUTT === false ? (
<FormDateTimePickerComponent
value={updateSchComp.scheduled_completion}
onChange={(e) => {
setSchComp({...updateSchComp, scheduled_completion: e});
}}
/>
) : null}
<Checkbox
checked={checkUTT}
onChange={(e) => {
setCheckUTT(e.target.checked);
setSchComp({
...updateSchComp,
scheduled_completion: null,
automatic: true,
});
}}
>
{t("jobs.labels.calc_scheuled_completion")}
</Checkbox>
</>
) : null}
</Space>
</div>
);
}

View File

@@ -27,7 +27,8 @@ export default connect(
modalSearchState,
partsQueueToggle,
setPartsQueueToggle,
...modalProps
updateSchComp,
setSchComp, ...modalProps
}) {
const {t} = useTranslation();
@@ -95,7 +96,8 @@ export default connect(
modalSearchState={modalSearchState}
partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle}
/>
updateSchComp={updateSchComp}
setSchComp={setSchComp}/>
</Modal>
);
});

View File

@@ -16,6 +16,7 @@ import useLocalStorage from "../../utils/useLocalStorage";
import AlertComponent from "../alert/alert.component";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -142,7 +143,8 @@ export function JobsList({bodyshop}) {
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
@@ -187,7 +189,8 @@ export function JobsList({bodyshop}) {
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sorter: (a, b) =>
statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: filter?.status || null,
@@ -218,7 +221,15 @@ export function JobsList({bodyshop}) {
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
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}
@@ -265,7 +276,9 @@ export function JobsList({bodyshop}) {
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
filteredValue: filter?.ins_co_nm || null,
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder:
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,filteredValue: filter?.ins_co_nm || null,
filters:
(jobs &&
jobs
@@ -301,7 +314,13 @@ export function JobsList({bodyshop}) {
dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator",
ellipsis: true,
responsive: ["xl"],
responsive: ["xl"],sorter: (a, b) =>
alphaSort(
`${a.est_ct_fn || ""} ${a.est_ct_ln || ""}`.trim(),
`${b.est_ct_fn || ""} ${b.est_ct_ln || ""}`.trim()
),
sortOrder:
state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order,
filterSearch: true,
filteredValue: filter?.estimator || null,
filters:

View File

@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobNotesContainer);
@@ -46,7 +46,7 @@ export function JobNotesContainer({jobId, insertAuditTrail}) {
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobnotedeleted(),
});
type: "jobnotedeleted",});
});
setDeleteLoading(false);
};

View File

@@ -21,8 +21,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(
mapStateToProps,
@@ -77,7 +77,7 @@ export function LaborAllocationsAdjustmentEdit({
values.hours -
((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1),
}),
});
type: "jobmodifylbradj",});
}
setLoading(false);
setOpen(false);

View File

@@ -19,8 +19,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function NoteUpsertModalContainer({
@@ -70,7 +70,7 @@ export function NoteUpsertModalContainer({
insertAuditTrail({
jobid: context.jobId,
operation: AuditTrailMapping.jobnoteupdated(),
});
type: "jobnoteupdated",});
});
if (refetch) refetch();
toggleModalVisible();
@@ -102,7 +102,7 @@ export function NoteUpsertModalContainer({
insertAuditTrail({
jobid: newJobId,
operation: AuditTrailMapping.jobnoteadded(),
});
type: "jobnoteadded",});
});
}
@@ -115,7 +115,7 @@ export function NoteUpsertModalContainer({
insertAuditTrail({
jobid: context.jobId,
operation: AuditTrailMapping.jobnoteadded(),
});
type: "jobnoteadded",});
}
};

View File

@@ -44,6 +44,15 @@ function OwnerDetailJobsComponent({bodyshop, owner}) {
title: t("jobs.fields.vehicle"),
dataIndex: "vehicleid",
key: "vehicleid",
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 === "vehicleid" && state.sortedInfo.order,
render: (text, record) =>
record.vehicleid ? (
<Link to={`/manage/vehicles/${record.vehicleid}`}>
@@ -67,9 +76,15 @@ function OwnerDetailJobsComponent({bodyshop, owner}) {
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sorter: (a, b) =>
statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters: bodyshop.md_ro_statuses.statuses.map((status) => ({
text: status,
value: status,
})),
onFilter: (value, record) => value.includes(record.status),
},
{

View File

@@ -1,19 +1,28 @@
import {useApolloClient, useMutation, useQuery} from "@apollo/client";
import {Form, Modal, notification} from "antd";
import dayjs from "../../utils/day";
import React, {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {auth, logImEXEvent} from "../../firebase/firebase.utils";
import {UPDATE_JOB_LINE_STATUS} from "../../graphql/jobs-lines.queries";
import {INSERT_NEW_PARTS_ORDERS, QUERY_PARTS_ORDER_OEC,} from "../../graphql/parts-orders.queries";
import {QUERY_ALL_VENDORS_FOR_ORDER} from "../../graphql/vendors.queries";
import {insertAuditTrail} from "../../redux/application/application.actions";
import {setEmailOptions} from "../../redux/email/email.actions";
import {setModalContext, toggleModalVisible,} from "../../redux/modals/modals.actions";
import {selectPartsOrder} from "../../redux/modals/modals.selectors";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
import { useMutation, useQuery, useApolloClient } from "@apollo/client";
import { Form, Modal, notification } from "antd";
import dayjs from '../../utils/day';
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent, auth } from "../../firebase/firebase.utils";
import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries";
import {
INSERT_NEW_PARTS_ORDERS,
QUERY_PARTS_ORDER_OEC,
} from "../../graphql/parts-orders.queries";
import { QUERY_ALL_VENDORS_FOR_ORDER } from "../../graphql/vendors.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setEmailOptions } from "../../redux/email/email.actions";
import {
setModalContext,
toggleModalVisible,
} from "../../redux/modals/modals.actions";
import { selectPartsOrder } from "../../redux/modals/modals.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import {GenerateDocument} from "../../utils/RenderTemplate";
import {TemplateList} from "../../utils/TemplateConstants";
@@ -36,8 +45,8 @@ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("partsOrder")),
setBillEnterContext: (context) =>
dispatch(setModalContext({context: context, modal: "billEnter"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function PartsOrderModalContainer({
@@ -135,7 +144,8 @@ export function PartsOrderModalContainer({
: AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number
),
});
type: isReturn ? "jobspartsreturn" : "jobspartsorder",
});
const jobLinesResult = await updateJobLines({
variables: {

View File

@@ -0,0 +1,77 @@
import {useQuery} from "@apollo/client";
import {Card, Divider, Drawer, Grid} from "antd";
import queryString from "query-string";
import React from "react";
import {useTranslation} from "react-i18next";
import {Link, useNavigate, useLocation} from "react-router-dom";
import {QUERY_PARTS_QUEUE_CARD_DETAILS} from "../../graphql/jobs.queries";
import AlertComponent from "../alert/alert.component";
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import PartsQueueJobLinesComponent from "./parts-queue-job-lines.component";
export default function PartsQueueDetailCard() {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = {
xs: "100%",
sm: "100%",
md: "100%",
lg: "75%",
xl: "75%",
xxl: "60%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
: "100%";
const searchParams = queryString.parse(useLocation().search);
const {selected} = searchParams;
const history = useNavigate();
const {loading, error, data} = useQuery(QUERY_PARTS_QUEUE_CARD_DETAILS, {
variables: {id: selected},
skip: !selected,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const {t} = useTranslation();
const handleDrawerClose = () => {
delete searchParams.selected;
history({
search: queryString.stringify({
...searchParams,
}),
});
};
return (
<Drawer
open={!!selected}
destroyOnClose
width={drawerPercentage}
placement="right"
onClose={handleDrawerClose}
>
{loading ? <LoadingSpinner/> : null}
{error ? <AlertComponent message={error.message} type="error"/> : null}
{data ? (
<Card
title={
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>
{data.jobs_by_pk.ro_number || t("general.labels.na")}
</Link>
}
>
<JobsDetailHeader job={data ? data.jobs_by_pk : null}/>
<Divider type="horizontal"/>
<PartsQueueJobLinesComponent
jobLines={data.jobs_by_pk ? data.jobs_by_pk.joblines : null}
/>
</Card>
) : null}
</Drawer>
);
}

View File

@@ -0,0 +1,209 @@
import {Card, Table} from "antd";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectJobReadOnly} from "../../redux/application/application.selectors";
import {selectBodyshop} from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {onlyUnique} from "../../utils/arrayHelper";
import {alphaSort} from "../../utils/sorters";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({});
export function PartsQueueJobLinesComponent({jobRO, loading, jobLines}) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const {t} = useTranslation();
const columns = [
{
title: "#",
dataIndex: "line_no",
key: "line_no",
sorter: (a, b) => a.line_no - b.line_no,
sortOrder:
state.sortedInfo.columnKey === "line_no" && state.sortedInfo.order,
},
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
onCell: (record) => ({
className: record.manual_line && "job-line-manual",
style: {
...(record.critical ? {boxShadow: " -.5em 0 0 #FFC107"} : {}),
},
}),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true,
},
{
title: t("joblines.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) =>
`${record.oem_partno || ""} ${
record.alt_partno ? `(${record.alt_partno})` : ""
}`.trim(),
},
{
title: t("joblines.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
filteredValue: state.filteredInfo.part_type || null,
sorter: (a, b) => alphaSort(a.part_type, b.part_type),
sortOrder:
state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order,
filters: [
{
text: t("jobs.labels.partsfilter"),
value: [
"PAN",
"PAC",
"PAR",
"PAL",
"PAA",
"PAM",
"PAP",
"PAS",
"PASL",
"PAG",
],
},
{
text: t("joblines.fields.part_types.PAN"),
value: ["PAN"],
},
{
text: t("joblines.fields.part_types.PAP"),
value: ["PAP"],
},
{
text: t("joblines.fields.part_types.PAL"),
value: ["PAL"],
},
{
text: t("joblines.fields.part_types.PAA"),
value: ["PAA"],
},
{
text: t("joblines.fields.part_types.PAG"),
value: ["PAG"],
},
{
text: t("joblines.fields.part_types.PAS"),
value: ["PAS"],
},
{
text: t("joblines.fields.part_types.PASL"),
value: ["PASL"],
},
{
text: t("joblines.fields.part_types.PAC"),
value: ["PAC"],
},
{
text: t("joblines.fields.part_types.PAR"),
value: ["PAR"],
},
{
text: t("joblines.fields.part_types.PAM"),
value: ["PAM"],
},
],
onFilter: (value, record) => value.includes(record.part_type),
render: (text, record) =>
record.part_type
? t(`joblines.fields.part_types.${record.part_type}`)
: null,
},
{
title: t("joblines.fields.part_qty"),
dataIndex: "part_qty",
key: "part_qty",
},
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
sorter: (a, b) => a.act_price - b.act_price,
sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) => (
<CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511"
? record.prt_dsmk_m
: record.act_price}
</CurrencyFormatter>
),
},
{
title: t("joblines.fields.location"),
dataIndex: "location",
key: "location",
},
{
title: t("joblines.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: state.filteredInfo.status || null,
filters:
(jobLines &&
jobLines
.map((l) => l.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.status),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState((state) => ({
...state,
filteredInfo: filters,
sortedInfo: sorter,
}));
};
return (
<Card title={t("jobs.labels.parts_lines")}>
<Table
columns={columns}
rowKey="id"
loading={loading}
pagination={false}
dataSource={jobLines}
onChange={handleTableChange}
/>
</Card>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(PartsQueueJobLinesComponent);

View File

@@ -8,31 +8,30 @@ import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {Link, useLocation, useNavigate} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import AlertComponent from "../../components/alert/alert.component";
import JobPartsQueueCount from "../../components/job-parts-queue-count/job-parts-queue-count.component";
import JobRemoveFromPartsQueue
from "../../components/job-remove-from-parst-queue/job-remove-from-parts-queue.component";
import OwnerNameDisplay from "../../components/owner-name-display/owner-name-display.component";
import ProductionListColumnComment
from "../../components/production-list-columns/production-list-columns.comment.component";
import {QUERY_PARTS_QUEUE} from "../../graphql/jobs.queries";
import {selectBodyshop} from "../../redux/user/user.selectors";
import {DateTimeFormatter, TimeAgoFormatter} from "../../utils/DateFormatter";
import {onlyUnique} from "../../utils/arrayHelper";
import {pageLimit} from "../../utils/config";
import {alphaSort, dateSort} from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import {pageLimit} from "../../utils/config";
import AlertComponent from "../alert/alert.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import JobRemoveFromPartsQueue from "../job-remove-from-parst-queue/job-remove-from-parts-queue.component";
import OwnerNameDisplay, {OwnerNameDisplayFunction,} from "../owner-name-display/owner-name-display.component";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function PartsQueuePageComponent({bodyshop}) {
export function PartsQueueListComponent({bodyshop}) {
const searchParams = queryString.parse(useLocation().search);
const {
//page,
selected,
sortcolumn,
sortorder,
statusFilters,
statusFilters
} = searchParams;
const history = useNavigate();
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
@@ -41,19 +40,10 @@ export function PartsQueuePageComponent({bodyshop}) {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
// offset: page ? (page - 1) * 25 : 0,
// limit: 25,
statuses: (statusFilters && JSON.parse(statusFilters)) ||
bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
order: [
{
[sortcolumn || "ro_number"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
],
},
});
@@ -109,6 +99,16 @@ export function PartsQueuePageComponent({bodyshop}) {
history({search: queryString.stringify(searchParams)});
};
const handleOnRowClick = (record) => {
if (record?.id) {
history({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
};
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -127,7 +127,8 @@ export function PartsQueuePageComponent({bodyshop}) {
title: t("jobs.fields.owner"),
dataIndex: "ownr_ln",
key: "ownr_ln",
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => {
return record.ownerid ? (
@@ -141,6 +142,56 @@ export function PartsQueuePageComponent({bodyshop}) {
);
},
},
{
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: sortcolumn === "vehicle" && sortorder,
render: (text, record) => {
return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
},
{
title: t("jobs.fields.ins_co_nm_short"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder: sortcolumn === "ins_co_nm" && sortorder,
filteredValue: filter?.ins_co_nm || null,
filters:
(jobs &&
jobs
.map((j) => j.ins_co_nm)
.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("jobs.fields.status"),
dataIndex: "status",
@@ -172,23 +223,16 @@ export function PartsQueuePageComponent({bodyshop}) {
),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
sorter: (a, b) =>
dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder: sortcolumn === "scheduled_completion" && sortorder,
render: (text, record) => (
<DateTimeFormatter>{record.scheduled_completion}</DateTimeFormatter>
),
},
// {
// title: t("vehicles.fields.plate_no"),
@@ -199,15 +243,8 @@ export function PartsQueuePageComponent({bodyshop}) {
// render: (text, record) => {
// return record.plate_no ? record.plate_no : "";
// },
// },
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
ellipsis: true,
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder: sortcolumn === "clm_no" && sortorder,
},
//},
// {
// title: t("jobs.fields.clm_total"),
// dataIndex: "clm_total",
@@ -309,9 +346,23 @@ export function PartsQueuePageComponent({bodyshop}) {
style={{height: "100%"}}
scroll={{x: true}}
onChange={handleTableChange}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selected],
type: "radio",
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
},
};
}}
/>
</Card>
);
}
export default connect(mapStateToProps, null)(PartsQueuePageComponent);
export default connect(mapStateToProps, null)(PartsQueueListComponent);

View File

@@ -59,8 +59,8 @@ const PaymentExpandedRowComponent = ({record, bodyshop}) => {
await insertPayment({
variables: {
paymentInput: {
amount: -refund_response.data.amount,
transactionid: payment_response.response.receiptelements.transid,
amount: -refund_response?.data?.amount,
transactionid: payment_response?.response?.receiptelements?.transid,
payer: record.payer,
type: "Refund",
jobid: payment_response.jobid,

View File

@@ -2,14 +2,17 @@ import {Button, Form, Input, Space} from "antd";
import {PageHeader} from "@ant-design/pro-layout";
import React from "react";
import {useTranslation} from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectAuthLevel,
selectBodyshop,
} from "../../redux/user/user.selectors";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, {PhoneItemFormatterValidation,} from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectAuthLevel, selectBodyshop,} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
authLevel: selectAuthLevel,
@@ -43,13 +46,16 @@ export function PhonebookFormComponent({
return (
<div>
<PageHeader
title={`${form.getFieldValue("firstname") || ""} ${
title={<Form.Item shouldUpdate>
{() =>`${form.getFieldValue("firstname") || ""} ${
form.getFieldValue("lastname") || ""
}${
form.getFieldValue("company")
? ` - ${form.getFieldValue("company")}`
: ""
}`}
: ""}`
}
</Form.Item>
}
extra={
<Space>
<Button

View File

@@ -31,8 +31,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function ProductionBoardKanbanComponent({
@@ -135,7 +135,8 @@ export function ProductionBoardKanbanComponent({
insertAuditTrail({
jobid: card.id,
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
});
type: "jobstatuschange",
});
if (update.errors) {
notification["error"]({

View File

@@ -27,6 +27,7 @@ export function ProductionColumnsComponent({
bodyshop,
data,
tableState,
refetch,
}) {
const [columns, setColumns] = columnState;
const {t} = useTranslation();
@@ -50,6 +51,7 @@ export function ProductionColumnsComponent({
data,
state: tableState,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
refetch,
});
const menu = {

View File

@@ -13,8 +13,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function ProductionListColumnAlert({record, insertAuditTrail}) {
@@ -46,7 +46,7 @@ export function ProductionListColumnAlert({record, insertAuditTrail}) {
? !record.production_vars.alert
: true
),
}).then(() => {
type: "alertToggle",}).then(() => {
if (record.refetch) record.refetch();
});
};

View File

@@ -1,7 +1,6 @@
import {BranchesOutlined, PauseCircleOutlined} from "@ant-design/icons";
import {Space, Tooltip} from "antd";
import {Checkbox,Space, Tooltip} from "antd";
import i18n from "i18next";
import dayjs from "../../utils/day";
import {Link} from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {TimeFormatter} from "../../utils/DateFormatter";
@@ -10,7 +9,9 @@ import {onlyUnique} from "../../utils/arrayHelper";
import {alphaSort, dateSort, statusSort} from "../../utils/sorters";
import JobAltTransportChange from "../job-at-change/job-at-change.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
import ProductionListColumnAlert from "./production-list-columns.alert.component";
import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component";
@@ -26,7 +27,7 @@ import ProductionListColumnCategory from "./production-list-columns.status.categ
import ProductionListColumnStatus from "./production-list-columns.status.component";
import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.component";
const r = ({technician, state, activeStatuses, data, bodyshop}) => {
const r = ({technician, state, activeStatuses, data, bodyshop, refetch}) => {
return [
{
title: i18n.t("jobs.actions.viewdetail"),
@@ -84,7 +85,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
<OwnerNameDisplay ownerObject={record}/>
</Link>
),
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
},
@@ -95,8 +96,10 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
ellipsis: true,
sorter: (a, b) =>
alphaSort(
a.v_make_desc + a.v_model_desc,
b.v_make_desc + b.v_model_desc
`${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,
@@ -185,17 +188,12 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
state.sortedInfo.columnKey === "date_next_contact" &&
state.sortedInfo.order,
render: (text, record) => (
<span
style={{
color:
record.date_next_contact &&
dayjs(record.date_next_contact).isBefore(dayjs())
? "red"
: "",
}}
>
<ProductionListDate record={record} field="date_next_contact" time/>
</span>
<ProductionListDate
record={record}
field="date_next_contact"
pastIndicator
time
/>
),
},
{
@@ -291,7 +289,21 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
dataIndex: "special_coverage_policy",
key: "special_coverage_policy",
ellipsis: true,
},
sorter: (a, b) =>
Number(a.special_coverage_policy) - Number(b.special_coverage_policy),
sortOrder:
state.sortedInfo.columnKey === "special_coverage_policy" &&
state.sortedInfo.order,
filters: [
{ text: "True", value: true },
{ text: "False", value: false },
],
onFilter: (value, record) =>
value.includes(record.special_coverage_policy),
render: (text, record) => (
<Checkbox disabled checked={record.special_coverage_policy} />
),
},
{
title: i18n.t("jobs.fields.alt_transport"),
@@ -302,7 +314,16 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
render: (text, record) => (
filters:
(bodyshop &&
bodyshop.appt_alt_transport.map((s) => {
return {
text: s,
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),render: (text, record) => (
<div>
{record.alt_transport}
<JobAltTransportChange job={record}/>
@@ -382,7 +403,11 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
title: i18n.t("production.labels.alert"),
dataIndex: "alert",
key: "alert",
sorter: (a, b) =>
Number(a.production_vars?.alert || false) -
Number(b.production_vars?.alert || false),
sortOrder:
state.sortedInfo.columnKey === "alert" && state.sortedInfo.order,
render: (text, record) => <ProductionListColumnAlert record={record}/>,
},
{
@@ -473,6 +498,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
),
render: (text, record) => (
<ProductionListEmployeeAssignment
refetch={refetch}
record={record}
type="employee_body"
/>
@@ -493,6 +519,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
render: (text, record) => (
<ProductionListEmployeeAssignment
record={record}
refetch={refetch}
type="employee_prep"
/>
),
@@ -509,7 +536,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
bodyshop.employees?.find((e) => e.id === b.employee_csr)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment record={record} type="employee_csr"/>
<ProductionListEmployeeAssignment refetch={refetch} record={record} type="employee_csr"/>
),
},
{
@@ -529,6 +556,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
render: (text, record) => (
<ProductionListEmployeeAssignment
record={record}
refetch={refetch}
type="employee_refinish"
/>
),

View File

@@ -3,12 +3,12 @@ import {useMutation} from "@apollo/client";
import {
Button,
Col,
notification,
Popover,
Row,
Select,
Space,
Spin,
Spin,notification,
} from "antd";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
@@ -25,14 +25,15 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function ProductionListEmpAssignment({
insertAuditTrail,
bodyshop,
record,
refetch,
type,
}) {
const {t} = useTranslation();
@@ -55,7 +56,8 @@ export function ProductionListEmpAssignment({
insertAuditTrail({
jobid: record.id,
operation: AuditTrailMapping.jobassignmentchange(empAssignment, name),
});
type: "jobassignmentchange",
});
if (!!result.errors) {
notification["error"]({
@@ -64,6 +66,9 @@ export function ProductionListEmpAssignment({
}),
});
}
await refetch();
setLoading(false);
};
const handleRemove = async (operation) => {
@@ -80,7 +85,8 @@ export function ProductionListEmpAssignment({
insertAuditTrail({
jobid: record.id,
operation: AuditTrailMapping.jobassignmentremoved(empAssignment),
});
type: "jobassignmentremoved",
});
if (!!result.errors) {
notification["error"]({
@@ -89,6 +95,9 @@ export function ProductionListEmpAssignment({
}),
});
}
await refetch();
setLoading(false);
};

View File

@@ -12,8 +12,8 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function ProductionListColumnCategory({record, bodyshop}) {

View File

@@ -5,16 +5,15 @@ import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {logImEXEvent} from "../../firebase/firebase.utils";
import {UPDATE_JOB} from "../../graphql/jobs.queries";
import {selectBodyshop} from "../../redux/user/user.selectors";
import {insertAuditTrail} from "../../redux/application/application.actions";
import {insertAuditTrail} from "../../redux/application/application.actions";import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function ProductionListColumnStatus({
@@ -41,7 +40,8 @@ export function ProductionListColumnStatus({
insertAuditTrail({
jobid: record.id,
operation: AuditTrailMapping.jobstatuschange(key),
});
type: "jobstatuschange",
});
setLoading(false);
};

View File

@@ -18,6 +18,7 @@ const mapStateToProps = createStructuredSelector({
});
export function ProductionListTable({
refetch,
bodyshop,
technician,
currentUser,
@@ -38,6 +39,7 @@ export function ProductionListTable({
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state,
data: data,
@@ -95,6 +97,7 @@ export function ProductionListTable({
...ProductionListColumns({
technician,
state,
refetch,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
}).find((e) => e.key === k.key),

View File

@@ -64,6 +64,7 @@ export function ProductionListTable({loading, data, refetch, bodyshop, technicia
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state,
data,
@@ -84,6 +85,7 @@ export function ProductionListTable({loading, data, refetch, bodyshop, technicia
...ProductionListColumns({
bodyshop,
technician,
refetch,
state,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
@@ -262,6 +264,7 @@ export function ProductionListTable({loading, data, refetch, bodyshop, technicia
state={state}
setState={setState}
setColumns={setColumns}
refetch={refetch}
data={data}
/>

View File

@@ -0,0 +1,432 @@
import {Button, Card, Checkbox, Col, Form, Input, InputNumber, Row, Select} from "antd";
import React, {useCallback, useEffect, useMemo, useState} from "react";
import {fetchFilterData} from "../../utils/RenderTemplate";
import {DeleteFilled} from "@ant-design/icons";
import {useTranslation} from "react-i18next";
import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import {generateInternalReflections} from "./report-center-modal-utils";
import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx";
export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) {
return (
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
return <RenderFilters form={form} templateId={key} bodyshop={bodyshop}/>;
}}
</Form.Item>
);
}
/**
* Filters Section
* @param filters
* @param form
* @param bodyshop
* @returns {JSX.Element}
* @constructor
*/
function FiltersSection({filters, form, bodyshop}) {
const {t} = useTranslation();
return (
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}>
<Form.List name={["filters"]}>
{(fields, {add, remove}) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={10}>
<Form.Item
key={`${index}field`}
label={t('reportcenter.labels.advanced_filters_filter_field')}
name={[field.name, "field"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
getPopupContainer={trigger => trigger.parentNode}
onChange={() => {
// Clear related Fields
form.setFieldValue(['filters', field.name, 'value'], null);
form.setFieldValue(['filters', field.name, 'operator'], null);
}}
options={
filters.map((f) => {
return {
value: f.name,
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
}
})
}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
dependencies={[['filters', field.name, "field"],['filters', field.name, "value"]]}
>
{
() => {
const name = form.getFieldValue(['filters', field.name, "field"]);
const type = filters.find(f => f.name === name)?.type;
return <Form.Item
key={`${index}operator`}
label={t('reportcenter.labels.advanced_filters_filter_operator')}
name={[field.name, "operator"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
getPopupContainer={trigger => trigger.parentNode}
options={ getWhereOperatorsByType(type)}
onChange={() => {
// Clear related Fields
form.setFieldValue(['filters', field.name, 'value'], undefined);
}}
/>
</Form.Item>
}
}
</Form.Item>
</Col>
<Col span={6}>
<Form.Item dependencies={[
['filters', field.name, "field"],
['filters', field.name, "operator"]
]}
>
{
() => {
// Because it looks cleaner than inlining.
const name = form.getFieldValue(['filters', field.name, "field"]);
const type = filters.find(f => f.name === name)?.type;
const reflector = filters.find(f => f.name === name)?.reflector;
const operator = form.getFieldValue(['filters', field.name, "operator"]);
const operatorType = operator ? getWhereOperatorsByType(type).find((o) => o.value === operator)?.type : null;
return <Form.Item
key={`${index}value`}
label={t('reportcenter.labels.advanced_filters_filter_value')}
name={[field.name, "value"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
{
(() => {
const generateReflections = (reflector) => {
if (!reflector) return [];
const {name} = reflector;
const path = name?.split('.');
const upperPath = path?.[0];
const finalPath = path?.slice(1).join('.');
return generateInternalReflections({
bodyshop,
upperPath,
finalPath,
t
});
};
const reflections = reflector ? generateReflections(reflector) : [];
const fieldPath = [[field.name, "value"]];
// We have reflections so we will use a select box
if (reflections.length > 0) {
// We have reflections and the operator type is array, so we will use a select box with multiple options
if (operatorType === "array") {
return (
<Select
disabled={!operator}
mode="multiple"
options={reflections}
getPopupContainer={trigger => trigger.parentNode}
onChange={(value) => {
form.setFieldValue(fieldPath, value);
}}
/>
);
}
return (
<Select
options={reflections}
getPopupContainer={trigger => trigger.parentNode}
onChange={(value) => {
form.setFieldValue(fieldPath, value);
}}
/>
);
}
// We have a type of number, so we will use a number input
if (type === "number") {
return (
<InputNumber
disabled={!operator}
onChange={(value) => form.setFieldValue(fieldPath, value)}/>
);
}
// We have a type of date, so we will use a date picker
if (type === "date") {
return (
<FormDatePicker
disabled={!operator}
onChange={(date) => form.setFieldValue(fieldPath, date)}
/>
);
}
// we have a type of boolean, so we will use a select box with a true or false option.
if (type === "boolean" || type === "bool") {
return (
<Select
disabled={!operator}
getPopupContainer={trigger => trigger.parentNode}
options={[
{
label: t('reportcenter.labels.advanced_filters_true'),
value: true
},
{
label: t('reportcenter.labels.advanced_filters_false'),
value: false
}
]}
onChange={(value) => form.setFieldValue(fieldPath, value)}
/>
);
}
return (
<Input
disabled={!operator}
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}
/>
);
})()
}
</Form.Item>
}
}
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{margin: "1rem", paddingTop: '23px'}}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{width: "100%"}}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</Card>
);
}
/**
* Sorters Section
* @param sorters
* @param form
* @returns {JSX.Element}
* @constructor
*/
function SortersSection({sorters}) {
const {t} = useTranslation();
return (
<Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}>
<Form.List name={["sorters"]}>
{(fields, {add, remove}) => {
return (
<div>
Sorters
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={11}>
<Form.Item
key={`${index}field`}
label={t('reportcenter.labels.advanced_filters_sorter_field')}
name={[field.name, "field"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={
sorters.map((f) => ({
value: f.name,
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
}))
}
getPopupContainer={trigger => trigger.parentNode}
/>
</Form.Item>
</Col>
<Col span={11}>
<Form.Item
key={`${index}direction`}
label={t('reportcenter.labels.advanced_filters_sorter_direction')}
name={[field.name, "direction"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={getOrderOperatorsByType()}
getPopupContainer={trigger => trigger.parentNode}
/>
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{margin: "1rem", paddingTop: '23px'}}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{width: "100%"}}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</Card>
);
}
/**
* Render Filters
* @param templateId
* @param form
* @param bodyshop
* @returns {JSX.Element|null}
* @constructor
*/
function RenderFilters({templateId, form, bodyshop}) {
const [state, setState] = useState(null);
const [visible, setVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation();
const fetch = useCallback(async () => {
// Reset all the filters and Sorters.
form.resetFields(['filters']);
form.resetFields(['sorters']);
form.resetFields(['defaultSorters']);
setIsLoading(true);
const data = await fetchFilterData({name: templateId});
// We have Success
if (data?.success) {
if (data?.data?.sorters && data?.data?.sorters.length > 0) {
const defaultSorters = data?.data?.sorters.filter((sorter) => sorter.hasOwnProperty('default')).map((sorter) => {
return {
field: sorter.name,
direction: sorter.default.direction
};
}).sort((a, b) => a.default.order - b.default.order);
form.setFieldValue('defaultSorters', JSON.stringify(defaultSorters));
}
// Set the state
setState(data.data);
}
// Something went wrong fetching filter data
else {
setState(null);
}
setIsLoading(false);
}, [templateId, form]);
useEffect(() => {
if (templateId) {
fetch();
}
}, [templateId, fetch]);
const filters = useMemo(() => state?.filters || [], [state]);
const sorters = useMemo(() => state?.sorters || [], [state]);
if (!templateId) return null;
if (isLoading) return <LoadingSkeleton/>;
if (!state) return null;
return (
<div style={{marginTop: '10px'}}>
<Checkbox
checked={visible}
onChange={(e) => setVisible(e.target.checked)}
children={t('reportcenter.labels.advanced_filters')}
/>
{visible && (
<div>
{filters.length > 0 && (
<FiltersSection filters={filters} form={form} bodyshop={bodyshop}/>
)}
{sorters.length > 0 && (
<SortersSection sorters={sorters} form={form}/>
)}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,161 @@
import {uniqBy} from "lodash";
/**
* Get value from path
* @param obj
* @param path
* @returns {*}
*/
const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj);
/**
* Generate options from array
* @param bodyshop
* @param path
* @returns {unknown[]}
*/
const generateOptionsFromArray = (bodyshop, path) => {
const options = getValueFromPath(bodyshop, path);
return uniqBy(options.map((value) => ({
label: value,
value: value,
})), 'value');
}
/**
* Valid internal reflections
* Note: This is intended for future functionality
* @type {{special: string[], bodyshop: [{name: string, type: string}]}}
*/
const VALID_INTERNAL_REFLECTIONS = {
bodyshop: [
{
name: 'md_ro_statuses.statuses',
type: 'kv-to-v'
}
],
};
/**
* Generate options
* @param bodyshop
* @param path
* @param labelPath
* @param valuePath
* @returns {{label: *, value: *}[]}
*/
const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => {
const options = getValueFromPath(bodyshop, path);
return uniqBy(Object.values(options).map((value) => ({
label: value[labelPath],
value: value[valuePath],
})), 'value');
}
/**
* Generate special reflections
* @param bodyshop
* @param finalPath
* @param t - i18n
* @returns {{label: *, value: *}[]|{label: *, value: *}[]|{label: string, value: *}[]|*[]}
*/
const generateSpecialReflections = (bodyshop, finalPath, t) => {
switch (finalPath) {
case 'payment_payers':
return [
{
label: t("payments.labels.customer"),
value: t("payments.labels.customer"),
},
{
label: t("payments.labels.insurance"),
value: t("payments.labels.insurance"),
},
// This is a weird one supposedly only used by one shop and could potentially be
// placed behind a SplitSDK
{
label: t("payments.labels.external"),
value: t("payments.labels.external"),
}
];
case 'payment_types':
return generateOptionsFromArray(bodyshop, 'md_payment_types');
case 'alt_transports':
return generateOptionsFromArray(bodyshop, 'appt_alt_transport');
case 'lost_sale_reasons':
return generateOptionsFromArray(bodyshop, 'md_lost_sale_reasons');
// Special case because Referral Sources is an Array, not an Object.
case 'referral_source':
return generateOptionsFromArray(bodyshop, 'md_referral_sources');
case 'class':
return generateOptionsFromArray(bodyshop, 'md_classes');
case 'cost_centers':
return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name');
// Special case because Categories is an Array, not an Object.
case 'categories':
return generateOptionsFromArray(bodyshop, 'md_categories');
case 'insurance_companies':
return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name');
case 'employee_teams':
return generateOptionsFromObject(bodyshop, 'employee_teams', 'name', 'id');
// Special case because Employees uses a concatenation of first_name and last_name
case 'employees':
const employeesOptions = getValueFromPath(bodyshop, 'employees');
return uniqBy(Object.values(employeesOptions).map((value) => ({
label: `${value.first_name} ${value.last_name}`,
value: value.id,
})), 'value');
case 'last_names':
return generateOptionsFromObject(bodyshop, 'employees', 'last_name', 'last_name');
case 'first_names':
return generateOptionsFromObject(bodyshop, 'employees', 'first_name', 'first_name');
case 'job_statuses':
const statusOptions = getValueFromPath(bodyshop, 'md_ro_statuses.statuses');
return Object.values(statusOptions).map((value) => ({
label: value,
value
}));
default:
console.error('Invalid Special reflection provided by Report Filters');
return [];
}
}
/**
* Generate bodyshop reflections
* @param bodyshop
* @param finalPath
* @returns {{label: *, value: *}[]|*[]}
*/
const generateBodyshopReflections = (bodyshop, finalPath) => {
const options = getValueFromPath(bodyshop, finalPath);
const reflectionRenderer = VALID_INTERNAL_REFLECTIONS.bodyshop.find(reflection => reflection.name === finalPath);
if (reflectionRenderer?.type === 'kv-to-v') {
return Object.values(options).map((value) => ({
label: value,
value
}));
}
return [];
}
/**
* Generate internal reflections based on the path and bodyshop
* @param bodyshop
* @param upperPath
* @param finalPath
* @param t - i18n
* @returns {{label: *, value: *}[]|[]|{label: *, value: *}[]|{label: string, value: *}[]|{label: *, value: *}[]|*[]}
*/
const generateInternalReflections = ({bodyshop, upperPath, finalPath, t}) => {
switch (upperPath) {
case 'special':
return generateSpecialReflections(bodyshop, finalPath, t);
case 'bodyshop':
return generateBodyshopReflections(bodyshop, finalPath);
default:
return [];
}
};
export {generateInternalReflections}

View File

@@ -9,15 +9,18 @@ import {createStructuredSelector} from "reselect";
import {QUERY_ACTIVE_EMPLOYEES} from "../../graphql/employees.queries";
import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries";
import {selectReportCenter} from "../../redux/modals/modals.selectors";
import DatePIckerRanges from "../../utils/DatePickerRanges";
import DatePickerRanges from "../../utils/DatePickerRanges";
import {GenerateDocument} from "../../utils/RenderTemplate";
import {TemplateList} from "../../utils/TemplateConstants";
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import "./report-center-modal.styles.scss";
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
import {selectBodyshop} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
reportCenterModal: selectReportCenter,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -27,7 +30,7 @@ export default connect(
mapDispatchToProps
)(ReportCenterModalComponent);
export function ReportCenterModalComponent({reportCenterModal}) {
export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
const [form] = Form.useForm();
const [search, setSearch] = useState("");
@@ -63,7 +66,7 @@ export function ReportCenterModalComponent({reportCenterModal}) {
const end = values.dates ? values.dates[1] : null;
const {id} = values;
await GenerateDocument(
const templateConfig =
{
name: values.key,
variables: {
@@ -78,7 +81,16 @@ export function ReportCenterModalComponent({reportCenterModal}) {
...(id ? {id: id} : {}),
},
},
filters: values.filters,
sorters: values.sorters,
};
if (_.isString(values.defaultSorters) && !_.isEmpty(values.defaultSorters)) {
templateConfig.defaultSorters = JSON.parse(values.defaultSorters);
}
await GenerateDocument(
templateConfig,
{
to: values.to,
subject: Templates[values.key]?.subject,
@@ -116,6 +128,7 @@ export function ReportCenterModalComponent({reportCenterModal}) {
onChange={(e) => setSearch(e.target.value)}
value={search}
/>
<Form.Item name="defaultSorters" hidden/>
<Form.Item
name="key"
label={t("reportcenter.labels.key")}
@@ -148,7 +161,7 @@ export function ReportCenterModalComponent({reportCenterModal}) {
<Typography.Title level={4}>
{t(`reportcenter.labels.groups.${key}`)}
</Typography.Title>
<ul style={{columns: "2 auto"}}>
<ul style={{listStyleType: 'none', columns: "2 auto"}}>
{grouped[key].map((item) => (
<li key={item.key}>
<Radio key={item.key} value={item.key}>
@@ -180,6 +193,7 @@ export function ReportCenterModalComponent({reportCenterModal}) {
);
}}
</Form.Item>
<ReportCenterModalFiltersSortersComponent form={form} bodyshop={bodyshop}/>
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
@@ -248,7 +262,7 @@ export function ReportCenterModalComponent({reportCenterModal}) {
>
<DatePicker.RangePicker
format="MM/DD/YYYY"
presets={DatePIckerRanges}
presets={DatePickerRanges}
/>
</Form.Item>
);
@@ -304,3 +318,4 @@ export function ReportCenterModalComponent({reportCenterModal}) {
</div>
);
}

View File

@@ -48,7 +48,7 @@ export function ScheduleCalendarContainer({calculateScheduleLoad}) {
if (error) return <AlertComponent message={error.message} type="error"/>;
let normalizedData = [
...data.appointments.map((e) => {
//Required becuase Hasura returns a string instead of a date object.
//Required because Hasura returns a string instead of a date object.
return Object.assign(
{},
e,
@@ -57,7 +57,7 @@ export function ScheduleCalendarContainer({calculateScheduleLoad}) {
);
}),
...data.employee_vacation.map((e) => {
//Required becuase Hasura returns a string instead of a date object.
//Required because Hasura returns a string instead of a date object.
return {
...e,
title: `${

View File

@@ -30,8 +30,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function ScheduleJobModalContainer({
@@ -142,7 +142,7 @@ export function ScheduleJobModalContainer({
operation: AuditTrailMapping.appointmentinsert(
DateTimeFormat(values.start)
),
});
type: "appointmentinsert",});
}
if (!!appt.errors) {

View File

@@ -26,6 +26,8 @@ export function ScoreboardDayStats({bodyshop, date, entries}) {
return acc + value.bodyhrs;
}, 0);
const numJobs = entries.length;
return (
<Card
title={dayjs(date).format("D - ddd")}
@@ -34,17 +36,18 @@ export function ScoreboardDayStats({bodyshop, date, entries}) {
>
<Statistic
valueStyle={{color: dailyBodyTarget > bodyHrs ? "red" : "green"}}
label="B"
label="Body"
value={bodyHrs.toFixed(1)}
/>
<Statistic
valueStyle={{color: dailyPaintTarget > paintHrs ? "red" : "green"}}
label="P"
label="Refinish"
value={paintHrs.toFixed(1)}
/>
<Divider style={{margin: 0}}/>
<Statistic value={(bodyHrs + paintHrs).toFixed(1)}/>
<Statistic label="Total" value={(bodyHrs + paintHrs).toFixed(1)}/>
<Divider style={{ margin: 0 }} />
<Statistic label="Jobs" value={numJobs} />
</Card>
);
}

View File

@@ -1,5 +1,6 @@
import {useMutation} from "@apollo/client";
import {Button, Card, Dropdown, Form, InputNumber, notification} from "antd";
import {Button, Card, Dropdown, Form, InputNumber, notification, Space,} from "antd";
import dayjs from "../../utils/day";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {UPDATE_SCOREBOARD_ENTRY} from "../../graphql/scoreboard.queries";
@@ -13,6 +14,7 @@ export default function ScoreboardEntryEdit({entry}) {
const handleFinish = async (values) => {
setLoading(true);
values.date = dayjs(values.date).format("YYYY-MM-DD");
const result = await updateScoreboardentry({
variables: {sbId: entry.id, sbInput: values},
});
@@ -82,13 +84,14 @@ export default function ScoreboardEntryEdit({entry}) {
>
<InputNumber precision={1}/>
</Form.Item>
<Space wrap>
<Button type="primary" loading={loading} htmlType="submit">
{t("general.actions.save")}
</Button>
<Button onClick={() => setOpen(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</Form>
</Card>
)

View File

@@ -1,3 +1,4 @@
import {SyncOutlined} from "@ant-design/icons";
import {useQuery} from "@apollo/client";
import {Button, Card, Input, Modal, Space, Table, Typography} from "antd";
import React, {useState} from "react";
@@ -5,12 +6,13 @@ import {useTranslation} from "react-i18next";
import {Link} from "react-router-dom";
import {QUERY_SCOREBOARD_PAGINATED} from "../../graphql/scoreboard.queries";
import {DateFormatter} from "../../utils/DateFormatter";
import {pageLimit} from "../../utils/config";
import {alphaSort, dateSort} from "../../utils/sorters";
import AlertComponent from "../alert/alert.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, {OwnerNameDisplayFunction,} from "../owner-name-display/owner-name-display.component";
import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component";
import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component";
import {SyncOutlined} from "@ant-design/icons";
import {pageLimit} from "../../utils/config";
export default function ScoreboardJobsList({scoreBoardlist}) {
const {t} = useTranslation();
@@ -45,7 +47,7 @@ export default function ScoreboardJobsList({scoreBoardlist}) {
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
render: (text, record) => (
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), render: (text, record) => (
<Link to={"/manage/jobs/" + record.job.id}>
{record.job.ro_number || t("general.labels.na")}
</Link>
@@ -55,8 +57,11 @@ export default function ScoreboardJobsList({scoreBoardlist}) {
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
ellipsis: true, sorter: (a, b) =>
alphaSort(
OwnerNameDisplayFunction(a.job),
OwnerNameDisplayFunction(b.job)
),
render: (text, record) => <OwnerNameDisplay ownerObject={record.job}/>,
},
{
@@ -64,7 +69,15 @@ export default function ScoreboardJobsList({scoreBoardlist}) {
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => (
sorter: (a, b) =>
alphaSort(
`${a.job.v_model_yr || ""} ${a.job.v_make_desc || ""} ${
a.job.v_model_desc || ""
}`,
`${b.job.v_model_yr || ""} ${b.job.v_make_desc || ""} ${
b.job.v_model_desc || ""
}`
), render: (text, record) => (
<span>{`${record.job.v_model_yr || ""} ${
record.job.v_make_desc || ""
} ${record.job.v_model_desc || ""}`}</span>
@@ -74,17 +87,20 @@ export default function ScoreboardJobsList({scoreBoardlist}) {
title: t("scoreboard.fields.date"),
dataIndex: "date",
key: "date",
sorter: (a, b) => dateSort(a.date, b.date),
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
},
{
title: t("scoreboard.fields.painthrs"),
dataIndex: "painthrs",
key: "painthrs",
},
{
title: t("scoreboard.fields.bodyhrs"),
dataIndex: "bodyhrs",
key: "bodyhrs",
sorter: (a, b) => Number(a.bodyhrs) - Number(b.bodyhrs),
},
{
title: t("scoreboard.fields.painthrs"),
dataIndex: "painthrs",
key: "painthrs",
sorter: (a, b) => Number(a.painthrs) - Number(b.painthrs),
},
{
title: t("general.labels.actions"),
@@ -105,11 +121,9 @@ export default function ScoreboardJobsList({scoreBoardlist}) {
open={state.open}
destroyOnClose
width="80%"
closable={false}
cancelButtonProps={{style: {display: "none"}}}
onOk={() => {
setState((state) => ({...state, open: false}));
}}
onCancel={() =>
onOk={() =>
setState((state) => ({
...state,
open: false,
@@ -161,9 +175,7 @@ export default function ScoreboardJobsList({scoreBoardlist}) {
</Card>
</Modal>
<Button
onClick={() => {
setState((state) => ({...state, open: true}))
}}
onClick={() => setState((state) => ({...state, open: true}))}
>
{t("scoreboard.labels.entries")}
</Button>

View File

@@ -26,227 +26,260 @@ export function ScoreboardTargetsTable({bodyshop, scoreBoardlist}) {
const values = useMemo(() => {
const dateHash = _.groupBy(scoreBoardlist, "date");
let ret = {
todayBody: 0,
todayPaint: 0,
weeklyPaint: 0,
weeklyBody: 0,
toDateBody: 0,
toDatePaint: 0,
};
let ret = {
todayBody: 0,
todayPaint: 0,
todayJobs: 0,
weeklyPaint: 0,
weeklyJobs: 0,
weeklyBody: 0,
toDateBody: 0,
toDatePaint: 0,
toDateJobs: 0,
};
const today = dayjs();
if (dateHash[today.format("YYYY-MM-DD")]) {
dateHash[today.format("YYYY-MM-DD")].forEach((d) => {
ret.todayBody = ret.todayBody + d.bodyhrs;
ret.todayPaint = ret.todayPaint + d.painthrs;
});
}
const today = dayjs();
if (dateHash[today.format("YYYY-MM-DD")]) {
dateHash[today.format("YYYY-MM-DD")].forEach((d) => {
ret.todayBody = ret.todayBody + d.bodyhrs;
ret.todayPaint = ret.todayPaint + d.painthrs;
ret.todayJobs++;
});
}
let StartOfWeek = dayjs().startOf("week");
while (StartOfWeek.isSameOrBefore(today)) {
if (dateHash[StartOfWeek.format("YYYY-MM-DD")]) {
dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => {
ret.weeklyBody = ret.weeklyBody + d.bodyhrs;
ret.weeklyPaint = ret.weeklyPaint + d.painthrs;
});
}
StartOfWeek = StartOfWeek.add(1, "day");
}
let StartOfWeek = dayjs().startOf("week");
while (StartOfWeek.isSameOrBefore(today)) {
if (dateHash[StartOfWeek.format("YYYY-MM-DD")]) {
dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => {
ret.weeklyBody = ret.weeklyBody + d.bodyhrs;
ret.weeklyPaint = ret.weeklyPaint + d.painthrs;
ret.weeklyJobs++;
});
}
StartOfWeek = StartOfWeek.add(1, "day");
}
let startOfMonth = dayjs().startOf("month");
while (startOfMonth.isSameOrBefore(today)) {
if (dateHash[startOfMonth.format("YYYY-MM-DD")]) {
dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => {
ret.toDateBody = ret.toDateBody + d.bodyhrs;
ret.toDatePaint = ret.toDatePaint + d.painthrs;
});
}
startOfMonth = startOfMonth.add(1, "day");
}
let startOfMonth = dayjs().startOf("month");
while (startOfMonth.isSameOrBefore(today)) {
if (dateHash[startOfMonth.format("YYYY-MM-DD")]) {
dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => {
ret.toDateBody = ret.toDateBody + d.bodyhrs;
ret.toDatePaint = ret.toDatePaint + d.painthrs;
ret.toDateJobs++;
});
}
startOfMonth = startOfMonth.add(1, "day");
}
return ret;
}, [scoreBoardlist]);
return (
<Card
title={t("scoreboard.labels.targets")}
extra={<ScoreboardJobsList scoreBoardlist={scoreBoardlist}/>}
>
<Row gutter={rowGutter}>
<Col xs={24} sm={{offset: 0, span: 4}} lg={{span: 4}}>
<Statistic
title={t("scoreboard.labels.workingdays")}
value={Util.CalculateWorkingDaysThisMonth()}
prefix={<CalendarOutlined/>}
/>
</Col>
<Col xs={24} sm={{offset: 0, span: 20}} lg={{offset: 0, span: 20}}>
<Row>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.dailytarget")}
value={bodyshop.scoreboard_target.dailyBodyTarget}
prefix="B"
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.dailyactual")}
value={values.todayBody.toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.weeklytarget")}
value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.weeklyactual")}
value={values.weeklyBody.toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.monthlytarget")}
value={Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.asoftodaytarget")}
value={Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.todateactual")}
value={values.toDateBody.toFixed(1)}
/>
</Col>
</Row>
<Row>
<Col {...statSpans}>
<Statistic
value={bodyshop.scoreboard_target.dailyPaintTarget}
prefix="P"
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.todayPaint.toFixed(1)}/>
</Col>
<Col {...statSpans}>
<Statistic
value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.weeklyPaint.toFixed(1)}/>
</Col>
<Col {...statSpans}>
<Statistic
value={Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.toDatePaint.toFixed(1)}/>
</Col>
</Row>
<Row>
<Divider style={{margin: 5}}/>
</Row>
<Row>
<Col {...statSpans}></Col>
<Col {...statSpans}>
<Statistic
value={(values.todayPaint + values.todayBody).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(values.toDatePaint + values.toDateBody).toFixed(1)}
/>
</Col>
</Row>
</Col>
</Row>
</Card>
);
return (
<Card
title={t("scoreboard.labels.targets")}
extra={<ScoreboardJobsList scoreBoardlist={scoreBoardlist} />}
>
<Row gutter={rowGutter}>
<Col xs={24} sm={{ offset: 0, span: 4 }} lg={{ span: 4 }}>
<Statistic
title={t("scoreboard.labels.workingdays")}
value={Util.CalculateWorkingDaysThisMonth()}
prefix={<CalendarOutlined />}
/>
</Col>
<Col xs={24} sm={{ offset: 0, span: 20 }} lg={{ offset: 0, span: 20 }}>
<Row>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.dailytarget")}
value={bodyshop.scoreboard_target.dailyBodyTarget}
prefix={t("scoreboard.labels.bodyabbrev")}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.dailyactual")}
value={values.todayBody.toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.weeklytarget")}
value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.weeklyactual")}
value={values.weeklyBody.toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.monthlytarget")}
value={Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.asoftodaytarget")}
value={Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.todateactual")}
value={values.toDateBody.toFixed(1)}
/>
</Col>
</Row>
<Row>
<Col {...statSpans}>
<Statistic
value={bodyshop.scoreboard_target.dailyPaintTarget}
prefix={t("scoreboard.labels.refinishabbrev")}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.todayPaint.toFixed(1)} />
</Col>
<Col {...statSpans}>
<Statistic
value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.weeklyPaint.toFixed(1)} />
</Col>
<Col {...statSpans}>
<Statistic
value={Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.toDatePaint.toFixed(1)} />
</Col>
</Row>
<Row>
<Divider style={{ margin: 5 }} />
</Row>
<Row>
<Col {...statSpans}>
<Statistic
value={"\u00A0"}
prefix={t("scoreboard.labels.total")}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(values.todayPaint + values.todayBody).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(values.toDatePaint + values.toDateBody).toFixed(1)}
/>
</Col>
</Row>
<Row>
<Divider style={{ margin: 5 }} />
</Row>
<Row>
<Col {...statSpans}>
<Statistic
value={"\u00A0"}
prefix={t("scoreboard.labels.jobs")}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.todayJobs} />
</Col>
<Col {...statSpans} />
<Col {...statSpans}>
<Statistic value={values.weeklyJobs} />
</Col>
<Col {...statSpans} />
<Col {...statSpans} />
<Col {...statSpans}>
<Statistic value={values.toDateJobs} />
</Col>
</Row>
</Col>
</Row>
</Card>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(ScoreboardTargetsTable);

View File

@@ -1,5 +1,5 @@
import React from "react";
import {Form} from "antd";
import React from "react";
import ConfigFormComponents from "../config-form-components/config-form-components.component";
export default function ShopCsiConfigForm({selectedCsi}) {
@@ -10,7 +10,7 @@ export default function ShopCsiConfigForm({selectedCsi}) {
return (
<div>
The Config Form {readOnly}
{readOnly}
{selectedCsi && (
<Form form={form} onFinish={handleFinish}>
<ConfigFormComponents

View File

@@ -1,7 +1,8 @@
import {CheckCircleFilled} from "@ant-design/icons";
import {useQuery} from "@apollo/client";
import {Button, Col, List, Row} from "antd";
import React, {useState} from "react";
import {useQuery} from "@apollo/client";
import {useTranslation} from "react-i18next";
import {GET_ALL_QUESTION_SETS} from "../../graphql/csi.queries";
import {DateFormatter} from "../../utils/DateFormatter";
@@ -21,7 +22,7 @@ export default function ShopCsiConfig() {
if (error) return <AlertComponent message={error.message} type="error"/>;
return (
<div>
The Config Form
<Row>
<Col span={3}>
<List
@@ -42,7 +43,8 @@ export default function ShopCsiConfig() {
)}
/>
</Col>
<Col span={21}>
<Col span={1}/>
<Col span={20}>
<ShopCsiConfigForm selectedCsi={selectedCsi}/>
</Col>
</Row>

View File

@@ -1,5 +1,5 @@
import {DeleteFilled} from "@ant-design/icons";
import {Button, Form, Input, InputNumber, Select, Switch, Typography,} from "antd";
import {Button, Form, Input, InputNumber, Select, Space, Switch, Typography,} from "antd";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import styled from "styled-components";
@@ -9,6 +9,7 @@ import {selectBodyshop} from "../../redux/user/user.selectors";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
const SelectorDiv = styled.div`
.ant-form-item .ant-select {
@@ -180,7 +181,7 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
</LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.dms.cdk.payers")}>
<Form.List name={["cdk_configuration", "payers"]}>
{(fields, {add, remove}) => {
{(fields, {add, remove, move}) => {
return (
<div>
{fields.map((field, index) => (
@@ -238,11 +239,18 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
</Select>
</Form.Item>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<Space align="center">
d
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow>
</Form.Item>
))}
@@ -334,7 +342,7 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
id="costs"
>
<Form.List name={["md_responsibility_centers", "costs"]}>
{(fields, {add, remove}) => {
{(fields, {add, remove, move}) => {
return (
<div>
{fields.map((field, index) => (
@@ -451,12 +459,18 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
<Input onBlur={handleBlur}/>
</Form.Item>
)}
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<Space align="center">
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow>
</Form.Item>
))}
@@ -482,7 +496,7 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
id="profits"
>
<Form.List name={["md_responsibility_centers", "profits"]}>
{(fields, {add, remove}) => {
{(fields, {add, remove, move}) => {
return (
<div>
{fields.map((field, index) => (
@@ -584,11 +598,18 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
<Input onBlur={handleBlur}/>
</Form.Item>
)}
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<Space align="center">
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow>
</Form.Item>
))}
@@ -613,7 +634,7 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
<>
<Form.List name={["md_responsibility_centers", "dms_defaults"]}>
{(fields, {add, remove}) => {
{(fields, {add, remove, move}) => {
return (
<div>
{fields.map((field, index) => (

View File

@@ -35,7 +35,7 @@ export default function ShopInfoSchedulingComponent({form}) {
},
]}
>
<TimePicker showSecond={false} format="HH:mm"/>
<TimePicker disableSeconds={true} format="HH:mm"/>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.schedule_end_time")}
@@ -47,7 +47,7 @@ export default function ShopInfoSchedulingComponent({form}) {
},
]}
>
<TimePicker showSecond={false} format="HH:mm"/>
<TimePicker disableSeconds={true} format="HH:mm"/>
</Form.Item>
<Form.Item
name={["appt_alt_transport"]}

View File

@@ -6,8 +6,10 @@ import {Link} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {alphaSort, statusSort} from "../../utils/sorters";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { alphaSort, statusSort } from "../../utils/sorters";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
const mapStateToProps = createStructuredSelector({
@@ -45,6 +47,10 @@ export function VehicleDetailJobsComponent({vehicle, bodyshop}) {
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => (
<Link to={`/manage/owners/${record.owner.id}`}>
<OwnerNameDisplay ownerObject={record}/>
@@ -63,9 +69,15 @@ export function VehicleDetailJobsComponent({vehicle, bodyshop}) {
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sorter: (a, b) =>
statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters: bodyshop.md_ro_statuses.statuses.map((status) => ({
text: status,
value: status,
})),
onFilter: (value, record) => value.includes(record.status),
},
{

View File

@@ -6,6 +6,7 @@ import {useTranslation} from "react-i18next";
import {Link, useLocation, useNavigate} from "react-router-dom";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
import {pageLimit} from "../../utils/config";
import { alphaSort } from '../../utils/sorters';
export default function VehiclesListComponent({
loading,
@@ -32,6 +33,8 @@ export default function VehiclesListComponent({
title: t("vehicles.fields.v_vin"),
dataIndex: "v_vin",
key: "v_vin",
sorter: (a, b) => alphaSort(a.v_vin, b.v_vin),
sortOrder: state.sortedInfo.columnKey === "v_vin" && state.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/vehicles/" + record.id}>
<VehicleVinDisplay>{record.v_vin || "N/A"}</VehicleVinDisplay>
@@ -52,8 +55,10 @@ export default function VehiclesListComponent({
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate",
key: "plate",
dataIndex: "plate_no",
key: "plate_no",
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder: state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
render: (text, record) => {
return (
<span>{`${record.plate_st || ""} | ${record.plate_no || ""}`}</span>

View File

@@ -4,6 +4,8 @@ import {getAuth, updatePassword, updateProfile} from "firebase/auth";
import {getFirestore} from "firebase/firestore";
import {getMessaging, getToken, onMessage} from "firebase/messaging";
import {store} from "../redux/store";
import axios from "axios";
import { checkBeta } from "../utils/handleBeta";
const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
initializeApp(config);
@@ -86,6 +88,17 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
null,
...additionalParams,
};
axios.post("/ioevent", {
useremail:
(state.user && state.user.currentUser && state.user.currentUser.email) ||
null,
bodyshopid:
(state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
operationName: eventName,
variables: additionalParams,
dbevent: false,
env: checkBeta() ? "beta" : "master",
});
// console.log(
// "%c[Analytics]",
// "background-color: green ;font-weight:bold;",

View File

@@ -40,6 +40,7 @@ export const INSERT_AUDIT_TRAIL = gql`
bodyshopid
created
operation
type
useremail
}
}

View File

@@ -22,6 +22,7 @@ export const QUERY_AVAILABLE_CC = gql`
]
status: { _eq: "courtesycars.status.in" }
}
order_by: { fleetnumber: asc }
) {
color
dailycost
@@ -29,6 +30,7 @@ export const QUERY_AVAILABLE_CC = gql`
fleetnumber
fuel
id
insuranceexpires
make
mileage
model
@@ -57,7 +59,7 @@ export const CHECK_CC_FLEET_NUMBER = gql`
`;
export const QUERY_ALL_CC = gql`
query QUERY_ALL_CC {
courtesycars {
courtesycars(order_by: { fleetnumber: asc }) {
color
created_at
dailycost

View File

@@ -57,19 +57,16 @@ export const INSERT_CSI = gql`
`;
export const QUERY_CSI_RESPONSE_PAGINATED = gql`
query QUERY_CSI_RESPONSE_PAGINATED(
$offset: Int
$limit: Int
$order: [csi_order_by!]!
) {
csi(offset: $offset, limit: $limit, order_by: $order) {
query QUERY_CSI_RESPONSE_PAGINATED{
csi(order_by: { completedon: desc_nulls_last }) {
id
completedon
job {
ownr_fn
ownr_ln
ownerid
ro_number
id
}
}
@@ -83,6 +80,7 @@ export const QUERY_CSI_RESPONSE_PAGINATED = gql`
export const QUERY_CSI_RESPONSE_BY_PK = gql`
query QUERY_CSI_RESPONSE_BY_PK($id: uuid!) {
csi_by_pk(id: $id) {
completedon
relateddata
valid
validuntil

View File

@@ -110,10 +110,9 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
export const QUERY_PARTS_QUEUE = gql`
query QUERY_PARTS_QUEUE(
$statuses: [String!]!
$offset: Int
$limit: Int
$order: [jobs_order_by!]
) {
, $offset: Int
, $limit: Int) {
jobs_aggregate(where: { _and: [{ status: { _in: $statuses } }] }) {
aggregate {
count(distinct: true)
@@ -125,7 +124,7 @@ export const QUERY_PARTS_QUEUE = gql`
}
offset: $offset
limit: $limit
order_by: $order
order_by: { ro_number: desc }
) {
ownr_fn
ownr_ln
@@ -142,7 +141,9 @@ export const QUERY_PARTS_QUEUE = gql`
v_color
vehicleid
scheduled_in
scheduled_completion
id
ins_co_nm
clm_no
ro_number
status
@@ -2338,3 +2339,163 @@ export const MARK_JOB_AS_UNINVOICED = gql`
}
}
`;
export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
query QUERY_JOB_CARD_DETAILS($id: uuid!) {
jobs_by_pk(id: $id) {
actual_completion
actual_delivery
actual_in
alt_transport
available_jobs {
id
}
area_of_damage
ca_gst_registrant
cccontracts {
agreementnumber
courtesycar {
id
make
model
year
plate
fleetnumber
}
id
scheduledreturn
start
status
}
clm_no
clm_total
comment
date_estimated
date_exported
date_invoiced
date_last_contacted
date_next_contact
date_open
date_repairstarted
date_scheduled
ded_amt
employee_body
employee_body_rel {
id
first_name
last_name
}
employee_csr
employee_csr_rel {
id
first_name
last_name
}
employee_prep
employee_prep_rel {
id
first_name
last_name
}
employee_refinish
employee_refinish_rel {
id
first_name
last_name
}
est_co_nm
est_ct_fn
est_ct_ln
est_ea
est_ph1
id
ins_co_nm
ins_ct_fn
ins_ct_ln
ins_ea
ins_ph1
inproduction
job_totals
joblines(
order_by: { line_no: asc }
where: {
part_type: {
_in: [
"PAN"
"PAC"
"PAR"
"PAL"
"PAA"
"PAM"
"PAP"
"PAG"
]
}
removed: { _eq: false }
}
) {
act_price
alt_partno
db_ref
id
line_desc
line_no
location
mod_lbr_ty
mod_lb_hrs
oem_partno
part_qty
part_type
prt_dsmk_m
status
}
lbr_adjustments
ownr_co_nm
ownr_ea
ownr_fn
ownr_ln
ownr_ph1
ownr_ph2
owner {
id
allow_text_message
preferred_contact
tax_number
}
owner_owing
plate_no
plate_st
po_number
production_vars
ro_number
scheduled_completion
scheduled_delivery
scheduled_in
special_coverage_policy
status
suspended
updated_at
vehicle {
id
jobs {
id
clm_no
ro_number
}
notes
plate_no
v_color
v_make_desc
v_model_desc
v_model_yr
}
vehicleid
v_color
v_make_desc
v_model_desc
v_model_yr
v_vin
voided
}
}
`;

View File

@@ -1,47 +1,48 @@
import {gql} from "@apollo/client";
export const QUERY_VEHICLE_BY_ID = gql`
query QUERY_VEHICLE_BY_ID($id: uuid!) {
vehicles_by_pk(id: $id) {
created_at
db_v_code
id
plate_no
plate_st
v_vin
v_type
v_trimcode
v_tone
v_stage
v_prod_dt
v_paint_codes
v_options
v_model_yr
v_model_desc
v_mldgcode
v_makecode
v_make_desc
v_engine
v_cond
v_color
v_bstyle
updated_at
trim_color
notes
jobs(order_by: { date_open: desc }) {
id
ro_number
ownr_fn
ownr_ln
owner {
id
}
clm_no
status
clm_total
}
query QUERY_VEHICLE_BY_ID($id: uuid!) {
vehicles_by_pk(id: $id) {
created_at
db_v_code
id
plate_no
plate_st
v_vin
v_type
v_trimcode
v_tone
v_stage
v_prod_dt
v_paint_codes
v_options
v_model_yr
v_model_desc
v_mldgcode
v_makecode
v_make_desc
v_engine
v_cond
v_color
v_bstyle
updated_at
trim_color
notes
jobs(order_by: { date_open: desc }) {
id
ro_number
ownr_co_nm
ownr_fn
ownr_ln
owner {
id
}
clm_no
status
clm_total
}
}
}
`;
export const UPDATE_VEHICLE = gql`

View File

@@ -1,88 +1,75 @@
import {useMutation, useQuery} from "@apollo/client";
//import {useMutation, useQuery } from "@apollo/client";
import {Button, Form, Layout, Result, Typography} from "antd";
import React, {useState} from "react";
import axios from "axios";
import React, {useCallback, useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {useParams} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import AlertComponent from "../../components/alert/alert.component";
import ConfigFormComponents from "../../components/config-form-components/config-form-components.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import {COMPLETE_SURVEY, QUERY_SURVEY} from "../../graphql/csi.queries";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectCurrentUser} from "../../redux/user/user.selectors";
import {DateTimeFormat} from "./../../utils/DateFormatter";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(CsiContainerPage);
export function CsiContainerPage({currentUser}) {
const {surveyId} = useParams();
const [form] = Form.useForm();
const [axiosResponse, setAxiosResponse] = useState(null);
const [submitting, setSubmitting] = useState({
loading: false,
submitted: false,
});
const {loading, error, data} = useQuery(QUERY_SURVEY, {
variables: {surveyId},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const {t} = useTranslation();
const [completeSurvey] = useMutation(COMPLETE_SURVEY);
if (loading) return <LoadingSpinner/>;
if (error || !!!data.csi_by_pk)
return (
<div>
<Result
status="error"
title={t("csi.errors.notfoundtitle")}
subTitle={t("csi.errors.notfoundsubtitle")}
>
{error ? (
<div>ERROR: {error.graphQLErrors.map((e) => e.message)}</div>
) : null}
</Result>
</div>
);
const handleFinish = async (values) => {
setSubmitting({...submitting, loading: true});
const getAxiosData = useCallback(async () => {
try {
try {
window.$crisp.push(["do", "chat:hide"]);
} catch {
console.log("Unable to attach to crisp instance. ");
}
setSubmitting((prevSubmitting) => ({...prevSubmitting, loading: true}));
const result = await completeSurvey({
variables: {
surveyId,
survey: {
response: values,
valid: false,
completedon: new Date(),
},
},
});
if (!!!result.errors) {
setSubmitting({...submitting, loading: false, submitted: true});
} else {
setSubmitting({
...submitting,
const response = await axios.post("/csi/lookup", {
surveyId
});
setSubmitting((prevSubmitting) => ({
...prevSubmitting,
loading: false,
error: JSON.stringify(result.errors),
}));
setAxiosResponse(response.data);
} catch (error) {
console.error(`Something went wrong...: ${error.message}`);
console.dir({
stack: error?.stack,
message:
error?.message,
});
}
};
}, [setAxiosResponse, surveyId]);
const {
relateddata: {bodyshop, job},
csiquestion: {config: csiquestions},
} = data.csi_by_pk;
useEffect(() => {
getAxiosData().catch((err) =>
console.error(
`Something went wrong fetching axios data: ${err.message || ""}`
)
);
}, [getAxiosData]);
if (currentUser && currentUser.authorized)
// Return if authorized
if (currentUser && currentUser.authorized) {
return (
<Layout
style={{height: "100vh", display: "flex", flexDirection: "column"}}
@@ -94,11 +81,68 @@ export function CsiContainerPage({currentUser}) {
/>
</Layout>
);
}
if (submitting.loading) return <LoadingSpinner/>;
const handleFinish = async (values) => {
try {
setSubmitting({...submitting, loading: true, submitting: true});
const result = await axios.post("/csi/submit", {surveyId, values});
console.log("result", result);
if (!!!result.errors && result.data.update_csi.affected_rows > 0) {
setSubmitting({...submitting, loading: false, submitted: true});
}
} catch (error) {
console.error(`Something went wrong...: ${error.message}`);
console.dir({
stack: error?.stack,
message: error?.message,
});
}
};
if (!axiosResponse || axiosResponse.csi_by_pk === null) {
// Do something here , this is where you would return a loading box or something
return (
<>
<Layout style={{display: "flex", flexDirection: "column"}}>
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
textAlign: "center",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Form>
<Result
status="error"
title={t("csi.errors.notfoundtitle")}
subTitle={t("csi.errors.notfoundsubtitle")}
/>
</Form>
</Layout.Content>
<Layout.Footer>
{t("csi.labels.copyright")}{" "}
{t("csi.fields.surveyid", {surveyId: surveyId})}
</Layout.Footer>
</Layout>
</>
);
} else {
const {
relateddata: {bodyshop, job},
csiquestion: {config: csiquestions},
} = axiosResponse.csi_by_pk;
return (
<Layout
style={{height: "100vh", display: "flex", flexDirection: "column"}}
>
<Layout style={{display: "flex", flexDirection: "column"}}>
<div
style={{
display: "flex",
@@ -108,24 +152,35 @@ export function CsiContainerPage({currentUser}) {
>
<div style={{display: "flex", alignItems: "center", margin: "2em"}}>
{bodyshop.logo_img_path && bodyshop.logo_img_path.src ? (
<img src={bodyshop.logo_img_path.src} alt="Logo"/>
) : null}
<div style={{margin: "2em"}}>
<strong>{bodyshop.shopname || ""}</strong>
<div>{`${bodyshop.address1 || ""}`}</div>
<div>{`${bodyshop.address2 || ""}`}</div>
<div>{`${bodyshop.city || ""} ${bodyshop.state || ""} ${
bodyshop.zip_post || ""
}`}</div>
<img src={bodyshop.logo_img_path.src} alt={bodyshop.shopname.concat("Logo")}
height={bodyshop.logo_img_path.height}
width={bodyshop.logo_img_path.width}
/>) : null}
<div style={{margin: "2em", verticalAlign: "middle"}}>
<Typography.Title level={4} style={{margin: 0}}>
{bodyshop.shopname || ""}
</Typography.Title>
<Typography.Paragraph style={{margin: 0}}>
{`${bodyshop.address1 || ""}${bodyshop.address2 ? ", " : ""}${
bodyshop.address2 || ""
}`.trim()}
</Typography.Paragraph>
<Typography.Paragraph style={{margin: 0}}>
{`${bodyshop.city || ""}${
bodyshop.city && bodyshop.state ? ", " : ""
}${bodyshop.state || ""} ${bodyshop.zip_post || ""}`.trim()}
</Typography.Paragraph>
</div>
</div>
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
<strong>{`Hi ${job.ownr_co_nm || job.ownr_fn || ""}!`}</strong>
<strong>{t("csi.labels.greeting", {
name: job.ownr_co_nm || job.ownr_fn || "",
})}</strong>
<Typography.Paragraph>
{`At ${
{t("csi.labels.intro", {
shopname:
bodyshop.shopname || ""
}, we value your feedback. We would love to
hear what you have to say. Please fill out the form below.`}
})}
</Typography.Paragraph>
</div>
@@ -158,21 +213,42 @@ export function CsiContainerPage({currentUser}) {
}}
>
<Form form={form} onFinish={handleFinish}>
<ConfigFormComponents componentList={csiquestions}/>
{axiosResponse.csi_by_pk.valid ? (
<><ConfigFormComponents componentList={csiquestions}/>
<Button
loading={submitting.loading}
type="primary"
htmlType="submit"
htmlType="submit" style={{float: "right"}}
>
{t("general.actions.submit")}
</Button>
</>
) : (
<>
<Result
title={t("csi.errors.surveycompletetitle")}
status="warning"
subTitle={t("csi.errors.surveycompletesubtitle", {
date: DateTimeFormat(axiosResponse.csi_by_pk.completedon),
})}
/>
<Typography.Paragraph
type="secondary"
style={{textAlign: "center"}}
>
{t("csi.successes.submittedsub")}
</Typography.Paragraph>
</>
)}
</Form>
</Layout.Content>
)}
)}
<Layout.Footer>
{`Copyright ImEX.Online. Survey ID: ${surveyId}`}
{t("csi.labels.copyright")}{" "}
{t("csi.fields.surveyid", {surveyId: surveyId})}
</Layout.Footer>
</Layout>
);
}
}

View File

@@ -16,8 +16,13 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
import {OwnerNameDisplayFunction} from "../../components/owner-name-display/owner-name-display.component";
import {auth} from "../../firebase/firebase.utils";
import {QUERY_JOB_EXPORT_DMS} from "../../graphql/jobs.queries";
import {setBreadcrumbs, setSelectedHeader,} from "../../redux/application/application.actions";
import {
insertAuditTrail,
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import {selectBodyshop} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -26,6 +31,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
@@ -45,7 +52,12 @@ export const socket = SocketIO(
}
);
export function DmsContainer({bodyshop, setBreadcrumbs, setSelectedHeader}) {
export function DmsContainer({
bodyshop,
setBreadcrumbs,
setSelectedHeader,
insertAuditTrail,
}) {
const {t} = useTranslation();
const [logLevel, setLogLevel] = useState("DEBUG");
const history = useNavigate();
@@ -103,6 +115,10 @@ export function DmsContainer({bodyshop, setBreadcrumbs, setSelectedHeader}) {
notification.success({
message: t("jobs.successes.exported"),
});
insertAuditTrail({
jobid: payload,
operation: AuditTrailMapping.jobexported(),
type: "jobexported",});
history("/manage/accounting/receivables");
});

View File

@@ -12,7 +12,8 @@ import AlertComponent from "../../components/alert/alert.component";
import {QUERY_EXPORT_LOG_PAGINATED} from "../../graphql/accounting.queries";
import {selectBodyshop} from "../../redux/user/user.selectors";
import {DateTimeFormatter} from "../../utils/DateFormatter";
import {pageLimit} from "../../utils/config";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "./../../utils/sorters";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -34,11 +35,42 @@ export function ExportLogsPageComponent({bodyshop}) {
limit: pageLimit,
order: [
{
[sortcolumn || "created_at"]: sortorder
...(sortcolumn === "ro_number"
? {
job: {
[sortcolumn|| "created_at"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
: "desc",},
}
: sortcolumn === "invoice_number"
? {
bill: {
[sortcolumn || "created_at"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
}
: sortcolumn === "paymentnum"
? {
payment: {
[sortcolumn || "created_at"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
}
: {
[sortcolumn || "created_at"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
}),
},
],
},
@@ -68,7 +100,8 @@ export function ExportLogsPageComponent({bodyshop}) {
title: t("general.labels.created_at"),
dataIndex: "created_at",
key: "created_at",
render: (text, record) => (
sorter: (a, b) => dateSort(a.created_at, b.created_at),
sortOrder: sortcolumn === "created_at" && sortorder,render: (text, record) => (
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
),
},
@@ -81,7 +114,8 @@ export function ExportLogsPageComponent({bodyshop}) {
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder,
render: (text, record) =>
record.job && (
<Link to={`/manage/jobs/${record.job.id}`}>
@@ -93,7 +127,8 @@ export function ExportLogsPageComponent({bodyshop}) {
title: t("bills.fields.invoice_number"),
dataIndex: "invoice_number",
key: "invoice_number",
render: (text, record) =>
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder: sortcolumn === "invoice_number" && sortorder,render: (text, record) =>
record.bill && (
<Link to={"/manage/bills?billid=" + (record.bill && record.bill.id)}>
{record.bill && record.bill.invoice_number}
@@ -103,7 +138,8 @@ export function ExportLogsPageComponent({bodyshop}) {
{
title: t("payments.fields.paymentnum"),
dataIndex: "paymentnum",
key: "paymentnum",
key: "paymentnum",sorter: (a, b) => alphaSort(a.paymentnum, b.paymentnum),
sortOrder: sortcolumn === "paymentnum" && sortorder,
render: (text, record) =>
record.payment && (
<Link
@@ -119,7 +155,13 @@ export function ExportLogsPageComponent({bodyshop}) {
{
title: t("general.labels.successful"),
dataIndex: "successful",
key: "successful",
key: "successful",sorter: (a, b) => Number(a.successful) - Number(b.successful),
sortOrder: sortcolumn === "successful" && sortorder,
filters: [
{ text: "True", value: true },
{ text: "False", value: false },
],
onFilter: (value, record) => record.successful === value,
render: (text, record) => (
<Checkbox disabled checked={record.successful}/>
),

View File

@@ -48,11 +48,11 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function JobsCloseComponent({job, bodyshop, jobRO, insertAuditTrail,}) {
export function JobsCloseComponent({job, bodyshop, jobRO, insertAuditTrail}) {
const {t} = useTranslation();
const [form] = Form.useForm();
const client = useApolloClient();
@@ -118,7 +118,7 @@ export function JobsCloseComponent({job, bodyshop, jobRO, insertAuditTrail,}) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobinvoiced(),
});
type: "jobinvoiced",});
// history.push(`/manage/jobs/${job.id}`);
} else {
setLoading(false);

View File

@@ -23,6 +23,7 @@ import {createStructuredSelector} from "reselect";
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container";
import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
import JobSyncButton from "../../components/job-sync-button/job-sync-button.component";
@@ -49,7 +50,6 @@ import {selectBodyshop} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import UndefinedToNull from "../../utils/undefinedtonull";
import {DateTimeFormat} from "./../../utils/DateFormatter";
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -58,8 +58,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) =>
dispatch(setModalContext({context: context, modal: "printCenter"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function JobsDetailPage({
@@ -172,7 +172,7 @@ export function JobsDetailPage({
? DateTimeFormat(changedAuditFields[key])
: changedAuditFields[key]
),
});
type: "jobfieldchange",});
});
await refetch();
@@ -304,7 +304,7 @@ export function JobsDetailPage({
{
key: 'lifecycle',
icon: <BarsOutlined/>,
label: t('menus.jobsdetail.lifecycle'),
label: t("menus.jobsdetail.lifecycle"),
children: <JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses}/>,
},
{

View File

@@ -1,9 +1,10 @@
import React, {useEffect} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import PartsQueueDetailCard from "../../components/parts-queue-card/parts-queue-card.component";
import PartsQueueList from "../../components/parts-queue-list/parts-queue.list.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import {setBreadcrumbs, setSelectedHeader,} from "../../redux/application/application.actions";
import PartsQueuePage from "./parts-queue.page.component";
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
@@ -23,7 +24,8 @@ export function PartsQueuePageContainer({setBreadcrumbs, setSelectedHeader}) {
return (
<RbacWrapper action="jobs:partsqueue">
<PartsQueuePage/>
<PartsQueueList/>
<PartsQueueDetailCard/>
</RbacWrapper>
);
}

View File

@@ -1,20 +1,17 @@
import {Col, Row} from "antd";
import {useQuery} from "@apollo/client";
import {Col, Row} from "antd";
import React, {useEffect} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {useLocation} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import AlertComponent from "../../components/alert/alert.component";
import CsiResponseFormContainer from "../../components/csi-response-form/csi-response-form.container";
import CsiResponseListPaginated
from "../../components/csi-response-list-paginated/csi-response-list-paginated.component";
import {QUERY_CSI_RESPONSE_PAGINATED} from "../../graphql/csi.queries";
import {setBreadcrumbs, setSelectedHeader} from "../../redux/application/application.actions";
import {selectBodyshop} from "../../redux/user/user.selectors";
from "../../components/csi-response-list-paginated/csi-response-list-paginated.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import {pageLimit} from "../../utils/config";
import queryString from "query-string";
import {QUERY_CSI_RESPONSE_PAGINATED} from "../../graphql/csi.queries";
import {setBreadcrumbs, setSelectedHeader,} from "../../redux/application/application.actions";
import {selectBodyshop} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -25,29 +22,18 @@ const mapDispatchToProps = (dispatch) => ({
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export function ShopCsiContainer({bodyshop, setBreadcrumbs, setSelectedHeader}) {
export function ShopCsiContainer({
bodyshop,
setBreadcrumbs,
setSelectedHeader,
}) {
const {t} = useTranslation();
const searchParams = queryString.parse(useLocation().search);
const {page, sortcolumn, sortorder} = searchParams;
const {loading, error, data, refetch} = useQuery(
QUERY_CSI_RESPONSE_PAGINATED,
{
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
offset: page ? (page - 1) * pageLimit : 0,
limit: pageLimit,
order: [
{
[sortcolumn || "completedon"]: sortorder
? sortorder === "descend"
? "desc_nulls_last"
: "asc"
: "desc_nulls_last",
},
],
},
}
);

View File

@@ -54,9 +54,9 @@ export const setOnline = (isOnline) => ({
payload: isOnline,
});
export const insertAuditTrail = ({jobid, billid, operation}) => ({
export const insertAuditTrail = ({jobid, billid, operation, type}) => ({
type: ApplicationActionTypes.INSERT_AUDIT_TRAIL,
payload: {jobid, billid, operation},
payload: {jobid, billid, operation, type },
});
export const setProblemJobs = (problemJobs) => ({
type: ApplicationActionTypes.SET_PROBLEM_JOBS,

View File

@@ -263,7 +263,7 @@ export function* onInsertAuditTrail() {
}
export function* insertAuditTrailSaga({
payload: {jobid, billid, operation},
payload: {jobid, billid, operation, type},
}) {
const state = yield select();
const bodyshop = state.user.bodyshop;
@@ -275,18 +275,18 @@ export function* insertAuditTrailSaga({
jobid,
billid,
operation,
useremail: currentUser.email,
type,useremail: currentUser.email,
},
};
yield client.mutate({
mutation: INSERT_AUDIT_TRAIL,
variables,
update(cache, {data}) {
update(cache, { data }) {
cache.modify({
fields: {
audit_trail(existingAuditTrail, {readField}) {
audit_trail(existingAuditTrail, { readField }) {
const newAuditTrail = cache.writeQuery({
data: data.insert_audit_trail_one,
data: data,
query: INSERT_AUDIT_TRAIL,
variables,
});

View File

@@ -107,13 +107,14 @@
"alerttoggle": "Alert Toggle set to {{status}}",
"appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.",
"appointmentinsert": "Appointment created. Appointment Date: {{start}}.",
"billposted": "Bill with invoice number {{invoice_number}} posted.",
"billdeleted": "Bill with invoice number {{invoice_number}} deleted.",
"billposted": "Bill with invoice number {{invoice_number}} posted.",
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
"failedpayment": "Failed payment",
"jobassignmentchange": "Employee {{name}} assigned to {{operation}}",
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
"jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.",
"jobconverted": "Job converted and assigned number {{ro_number}}.",
"jobconverted": "Job converted and assigned number {{ro_number}}.","jobexported": "Job has been exported.",
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
"jobimported": "Job imported.",
"jobinproductionchange": "Job production status set to {{inproduction}}",
@@ -126,8 +127,9 @@
"jobspartsorder": "Parts order {{order_number}} added to Job.",
"jobspartsreturn": "Parts return {{order_number}} added to Job.",
"jobstatuschange": "Job status changed to {{status}}.",
"jobsupplement": "Job supplement imported."
}
"jobsupplement": "Job supplement imported.",
"jobsuspend": "Suspend Toggle set to {{status}}",
"jobvoid": "Job has been voided."}
},
"billlines": {
"actions": {
@@ -255,6 +257,7 @@
"saving": "Error encountered while saving. {{message}}"
},
"fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"address1": "Address 1",
"address2": "Address 2",
"appt_alt_transport": "Appointment Alternative Transportation Options",
@@ -473,7 +476,7 @@
"editaccess": "Users -> Edit access"
}
},
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"responsibilitycenter": "Responsibility Center",
"responsibilitycenter_accountdesc": "Account Description",
"responsibilitycenter_accountitem": "Item",
@@ -604,7 +607,7 @@
"dms": {
"cdk": {
"controllist": "Control Number List",
"payers": "CDK Payers"
"payers": " Payers"
},
"cdk_dealerid": "CDK Dealer ID",
"pbs_serialnumber": "PBS Serial Number",
@@ -747,6 +750,7 @@
"driverinformation": "Driver's Information",
"findcontract": "Find Contract",
"findermodal": "Contract Finder",
"insuranceexpired": "The courtesy car insurance expires before the car is expected to return.",
"noteconvertedfrom": "R.O. created from converted Courtesy Car Contract {{agreementnumber}}.",
"populatefromjob": "Populate from Job",
"rates": "Contract Rates",
@@ -838,15 +842,22 @@
"creating": "Error creating survey {{message}}",
"notconfigured": "You do not have any current CSI Question Sets configured.",
"notfoundsubtitle": "We were unable to find a survey using the link you provided. Please ensure the URL is correct or reach out to your shop for more help.",
"notfoundtitle": "No survey found."
"notfoundtitle": "No survey found.",
"surveycompletesubtitle": "This survey was already completed on {{date}}.",
"surveycompletetitle": "Survey previously completed"
},
"fields": {
"completedon": "Completed On",
"created_at": "Created At"
"created_at": "Created At",
"surveyid": "Survey ID {{surveyId}}",
"validuntil": "Valid Until"
},
"labels": {
"nologgedinuser": "Please log out of ImEX Online",
"nologgedinuser_sub": "Users of ImEX Online cannot complete CSI surveys while logged in. Please log out and try again.",
"copyright": "Copyright © $t(titles.app). All Rights Reserved.",
"greeting": "Hi {{name}}!",
"intro": "At {{shopname}}, we value your feedback. We would love to hear what you have to say. Please fill out the form below.",
"nologgedinuser": "Please log out of $t(titles.app)",
"nologgedinuser_sub": "Users of $t(titles.app) cannot complete CSI surveys while logged in. Please log out and try again.",
"noneselected": "No response selected.",
"title": "Customer Satisfaction Survey"
},
@@ -867,6 +878,7 @@
"labels": {
"bodyhrs": "Body Hrs",
"dollarsinproduction": "Dollars in Production",
"phone": "Phone",
"prodhrs": "Production Hrs",
"refhrs": "Refinish Hrs"
},
@@ -882,8 +894,11 @@
"productiondollars": "Total Dollars in Production",
"productionhours": "Total Hours in Production",
"projectedmonthlysales": "Projected Monthly Sales",
"scheduledintoday": "Sheduled In Today: {{date}}",
"scheduledouttoday": "Sheduled Out Today: {{date}}"
"scheduledindate": "Sheduled In Today: {{date}}",
"scheduledintoday": "Sheduled In Today",
"scheduledoutdate": "Sheduled Out Today: {{date}}",
"scheduledouttoday": "Sheduled Out Today",
"joblifecycle": "Job Lifecycle"
}
},
"dms": {
@@ -1256,7 +1271,15 @@
"relative_end": "Relative End",
"relative_start": "Relative Start",
"start": "Start",
"value": "Value"
"value": "Value",
"status": "Status",
"percentage": "Percentage",
"human_readable": "Human Readable",
"status_count": "In Status"
},
"titles": {
"dashboard": "Job Lifecycle",
"top_durations": "Top Durations"
},
"content": {
"current_status_accumulated_time": "Current Status Accumulated Time",
@@ -1268,7 +1291,9 @@
"title": "Job Lifecycle Component",
"title_durations": "Historical Status Durations",
"title_loading": "Loading",
"title_transitions": "Transitions"
"title_transitions": "Transitions",
"calculated_based_on": "Calculated based on",
"jobs_in_since": "Jobs in since"
},
"errors": {
"fetch": "Error getting Job Lifecycle Data"
@@ -1760,6 +1785,7 @@
"ca_gst_all_if_null": "If the Job is marked as a \"GST Registrant\" and this value is set to $0, the customer will be responsible for paying all of the GST by default. ",
"calc_repair_days": "Calculated Repair Days",
"calc_repair_days_tt": "This is the approximate number of days required to complete the repair according to the target touch time in your shop configuration (current set to {{target_touchtime}}).",
"calc_scheuled_completion": "Calculate Scheduled Completion",
"cards": {
"customer": "Customer Information",
"damage": "Area of Damage",
@@ -1842,6 +1868,7 @@
"job": "Job Details",
"jobcosting": "Job Costing",
"jobtotals": "Job Totals",
"labor_hrs": "B/P/T Hrs",
"labor_rates_subtotal": "Labor Rates Subtotal",
"laborallocations": "Labor Allocations",
"labortotals": "Labor Totals",
@@ -1857,6 +1884,7 @@
"override_header": "Override estimate header on import?",
"ownerassociation": "Owner Association",
"parts": "Parts",
"parts_lines": "Parts Lines",
"parts_received": "Parts Rec.",
"parts_tax_rates": "Parts Tax rates",
"partsfilter": "Parts Only",
@@ -1918,6 +1946,7 @@
"total_sales": "Total Sales",
"totals": "Totals",
"unvoidnote": "This Job was unvoided.",
"update_scheduled_completion": "Update Scheduled Completion?",
"vehicle_info": "Vehicle",
"vehicleassociation": "Vehicle Association",
"viewallocations": "View Allocations",
@@ -2434,6 +2463,7 @@
"invoice_total_payable": "Invoice (Total Payable)",
"iou_form": "IOU Form",
"job_costing_ro": "Job Costing",
"job_lifecycle_ro": "Job Lifecycle",
"job_notes": "Job Notes",
"key_tag": "Key Tag",
"labels": {
@@ -2599,6 +2629,18 @@
"generate": "Generate"
},
"labels": {
"advanced_filters": "Advanced Filters and Sorters",
"advanced_filters_false": "False",
"advanced_filters_filter_field": "Field",
"advanced_filters_filter_operator": "Operator",
"advanced_filters_filter_value": "Value",
"advanced_filters_filters": "Filters",
"advanced_filters_hide": "Hide",
"advanced_filters_show": "Show",
"advanced_filters_sorter_direction": "Direction",
"advanced_filters_sorter_field": "Field",
"advanced_filters_sorters": "Sorters",
"advanced_filters_true": "True",
"dates": "Dates",
"employee": "Employee",
"filterson": "Filters on {{object}}: {{field}}",
@@ -2680,6 +2722,8 @@
"job_costing_ro_date_summary": "Job Costing by RO - Summary",
"job_costing_ro_estimator": "Job Costing by Estimator",
"job_costing_ro_ins_co": "Job Costing by RO Source",
"job_lifecycle_date_detail": "Job Lifecycle by Date - Detail",
"job_lifecycle_date_summary": "Job Lifecycle by Date - Summary",
"jobs_completed_not_invoiced": "Jobs Completed not Invoiced",
"jobs_invoiced_not_exported": "Jobs Invoiced not Exported",
"jobs_reconcile": "Parts/Sublet/Labor Reconciliation",
@@ -2769,7 +2813,8 @@
"allemployeetimetickets": "All Employee Time Tickets",
"asoftodaytarget": "As of Today",
"body": "Body",
"bodycharttitle": "Body Targets vs Actual",
"bodyabbrev": "B",
"bodycharttitle": "Body Targets vs Actual",
"calendarperiod": "Periods based on calendar weeks/months.",
"combinedcharttitle": "Combined Targets vs Actual",
"dailyactual": "Actual (D)",
@@ -2784,14 +2829,14 @@
"priorweek": "Prior Week",
"productivestatistics": "Productive Hours Statistics",
"productivetimeticketsoverdate": "Productive Hours over Selected Dates",
"refinish": "Refinish",
"refinish": "Refinish","refinishabbrev": "R",
"refinishcharttitle": "Refinish Targets vs Actual",
"targets": "Targets",
"thismonth": "This Month",
"thisweek": "This Week",
"timetickets": "Time Tickets",
"timeticketsemployee": "Time Tickets by Employee",
"todateactual": "Actual (MTD)",
"todateactual": "Actual (MTD)","total": "Total",
"totalhrs": "Total Hours",
"totaloverperiod": "Total over Selected Dates",
"weeklyactual": "Actual (W)",

View File

@@ -107,14 +107,16 @@
"alerttoggle": "",
"appointmentcancel": "",
"appointmentinsert": "",
"billposted": "",
"billdeleted": "",
"billposted": "",
"billupdated": "",
"failedpayment": "",
"jobassignmentchange": "",
"jobassignmentremoved": "",
"jobchecklist": "",
"jobconverted": "",
"jobfieldchanged": "",
"jobexported": "",
"jobfieldchanged": "",
"jobimported": "",
"jobinproductionchange": "",
"jobinvoiced": "",
@@ -126,8 +128,9 @@
"jobspartsorder": "",
"jobspartsreturn": "",
"jobstatuschange": "",
"jobsupplement": ""
}
"jobsupplement": "",
"jobsuspend": "",
"jobvoid": ""}
},
"billlines": {
"actions": {
@@ -255,6 +258,7 @@
"saving": ""
},
"fields": {
"ReceivableCustomField": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
@@ -473,7 +477,7 @@
"editaccess": ""
}
},
"ReceivableCustomField": "",
"responsibilitycenter": "",
"responsibilitycenter_accountdesc": "",
"responsibilitycenter_accountitem": "",
@@ -747,6 +751,7 @@
"driverinformation": "",
"findcontract": "",
"findermodal": "",
"insuranceexpired": "",
"noteconvertedfrom": "",
"populatefromjob": "",
"rates": "",
@@ -838,13 +843,20 @@
"creating": "",
"notconfigured": "",
"notfoundsubtitle": "",
"notfoundtitle": ""
"notfoundtitle": "",
"surveycompletesubtitle": "",
"surveycompletetitle": ""
},
"fields": {
"completedon": "",
"created_at": ""
"created_at": "",
"surveyid": "",
"validuntil": ""
},
"labels": {
"copyright": "",
"greeting": "",
"intro": "",
"nologgedinuser": "",
"nologgedinuser_sub": "",
"noneselected": "",
@@ -867,6 +879,7 @@
"labels": {
"bodyhrs": "",
"dollarsinproduction": "",
"phone": "",
"prodhrs": "",
"refhrs": ""
},
@@ -882,8 +895,11 @@
"productiondollars": "",
"productionhours": "",
"projectedmonthlysales": "",
"scheduledindate": "",
"scheduledintoday": "",
"scheduledouttoday": ""
"scheduledoutdate": "",
"scheduledouttoday": "",
"joblifecycle": ""
}
},
"dms": {
@@ -1256,7 +1272,15 @@
"relative_end": "",
"relative_start": "",
"start": "",
"value": ""
"value": "",
"status": "",
"percentage": "",
"human_readable": "",
"status_count": ""
},
"titles": {
"dashboard": "",
"top_durations": ""
},
"content": {
"current_status_accumulated_time": "",
@@ -1268,7 +1292,9 @@
"title": "",
"title_durations": "",
"title_loading": "",
"title_transitions": ""
"title_transitions": "",
"calculated_based_on": "",
"jobs_in_since": ""
},
"errors": {
"fetch": "Error al obtener los datos del ciclo de vida del trabajo"
@@ -1760,6 +1786,7 @@
"ca_gst_all_if_null": "",
"calc_repair_days": "",
"calc_repair_days_tt": "",
"calc_scheuled_completion": "",
"cards": {
"customer": "Información al cliente",
"damage": "Área de Daño",
@@ -1842,6 +1869,7 @@
"job": "",
"jobcosting": "",
"jobtotals": "",
"labor_hrs": "",
"labor_rates_subtotal": "",
"laborallocations": "",
"labortotals": "",
@@ -1857,6 +1885,7 @@
"override_header": "¿Anular encabezado estimado al importar?",
"ownerassociation": "",
"parts": "Partes",
"parts_lines": "",
"parts_received": "",
"parts_tax_rates": "",
"partsfilter": "",
@@ -1918,6 +1947,7 @@
"total_sales": "",
"totals": "",
"unvoidnote": "",
"update_scheduled_completion": "",
"vehicle_info": "Vehículo",
"vehicleassociation": "",
"viewallocations": "",
@@ -2434,6 +2464,7 @@
"invoice_total_payable": "",
"iou_form": "",
"job_costing_ro": "",
"job_lifecycle_ro": "",
"job_notes": "",
"key_tag": "",
"labels": {
@@ -2599,6 +2630,18 @@
"generate": ""
},
"labels": {
"advanced_filters": "",
"advanced_filters_false": "",
"advanced_filters_filter_field": "",
"advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "",
"advanced_filters_filters": "",
"advanced_filters_hide": "",
"advanced_filters_show": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_sorter_field": "",
"advanced_filters_sorters": "",
"advanced_filters_true": "",
"dates": "",
"employee": "",
"filterson": "",
@@ -2680,6 +2723,8 @@
"job_costing_ro_date_summary": "",
"job_costing_ro_estimator": "",
"job_costing_ro_ins_co": "",
"job_lifecycle_date_detail": "",
"job_lifecycle_date_summary": "",
"jobs_completed_not_invoiced": "",
"jobs_invoiced_not_exported": "",
"jobs_reconcile": "",
@@ -2769,7 +2814,8 @@
"allemployeetimetickets": "",
"asoftodaytarget": "",
"body": "",
"bodycharttitle": "",
"bodyabbrev": "",
"bodycharttitle": "",
"calendarperiod": "",
"combinedcharttitle": "",
"dailyactual": "",
@@ -2785,13 +2831,14 @@
"productivestatistics": "",
"productivetimeticketsoverdate": "",
"refinish": "",
"refinishcharttitle": "",
"refinishabbrev": "",
"refinishcharttitle": "",
"targets": "",
"thismonth": "",
"thisweek": "",
"timetickets": "",
"timeticketsemployee": "",
"todateactual": "",
"todateactual": "","total": "",
"totalhrs": "",
"totaloverperiod": "",
"weeklyactual": "",

View File

@@ -107,14 +107,16 @@
"alerttoggle": "",
"appointmentcancel": "",
"appointmentinsert": "",
"billposted": "",
"billdeleted": "",
"billposted": "",
"billupdated": "",
"failedpayment": "",
"jobassignmentchange": "",
"jobassignmentremoved": "",
"jobchecklist": "",
"jobconverted": "",
"jobfieldchanged": "",
"jobexported": "",
"jobfieldchanged": "",
"jobimported": "",
"jobinproductionchange": "",
"jobinvoiced": "",
@@ -126,8 +128,9 @@
"jobspartsorder": "",
"jobspartsreturn": "",
"jobstatuschange": "",
"jobsupplement": ""
}
"jobsupplement": "",
"jobsuspend": "",
"jobvoid": ""}
},
"billlines": {
"actions": {
@@ -255,6 +258,7 @@
"saving": ""
},
"fields": {
"ReceivableCustomField": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
@@ -473,7 +477,7 @@
"editaccess": ""
}
},
"ReceivableCustomField": "",
"responsibilitycenter": "",
"responsibilitycenter_accountdesc": "",
"responsibilitycenter_accountitem": "",
@@ -747,6 +751,7 @@
"driverinformation": "",
"findcontract": "",
"findermodal": "",
"insuranceexpired": "",
"noteconvertedfrom": "",
"populatefromjob": "",
"rates": "",
@@ -838,13 +843,20 @@
"creating": "",
"notconfigured": "",
"notfoundsubtitle": "",
"notfoundtitle": ""
"notfoundtitle": "",
"surveycompletesubtitle": "",
"surveycompletetitle": ""
},
"fields": {
"completedon": "",
"created_at": ""
"created_at": "",
"surveyid": "",
"validuntil": ""
},
"labels": {
"copyright": "",
"greeting": "",
"intro": "",
"nologgedinuser": "",
"nologgedinuser_sub": "",
"noneselected": "",
@@ -867,6 +879,7 @@
"labels": {
"bodyhrs": "",
"dollarsinproduction": "",
"phone": "",
"prodhrs": "",
"refhrs": ""
},
@@ -882,7 +895,9 @@
"productiondollars": "",
"productionhours": "",
"projectedmonthlysales": "",
"scheduledindate": "",
"scheduledintoday": "",
"scheduledoutdate": "",
"scheduledouttoday": ""
}
},
@@ -1256,7 +1271,15 @@
"relative_end": "",
"relative_start": "",
"start": "",
"value": ""
"value": "",
"status": "",
"percentage": "",
"human_readable": "",
"status_count": ""
},
"titles": {
"dashboard": "",
"top_durations": ""
},
"content": {
"current_status_accumulated_time": "",
@@ -1268,7 +1291,10 @@
"title": "",
"title_durations": "",
"title_loading": "",
"title_transitions": ""
"title_transitions": "",
"calculated_based_on": "",
"jobs_in_since": "",
"joblifecycle": ""
},
"errors": {
"fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches"
@@ -1760,6 +1786,7 @@
"ca_gst_all_if_null": "",
"calc_repair_days": "",
"calc_repair_days_tt": "",
"calc_scheuled_completion": "",
"cards": {
"customer": "Informations client",
"damage": "Zone de dommages",
@@ -1842,6 +1869,7 @@
"job": "",
"jobcosting": "",
"jobtotals": "",
"labor_hrs": "",
"labor_rates_subtotal": "",
"laborallocations": "",
"labortotals": "",
@@ -1857,6 +1885,7 @@
"override_header": "Remplacer l'en-tête d'estimation à l'importation?",
"ownerassociation": "",
"parts": "les pièces",
"parts_lines": "",
"parts_received": "",
"parts_tax_rates": "",
"partsfilter": "",
@@ -1918,6 +1947,7 @@
"total_sales": "",
"totals": "",
"unvoidnote": "",
"update_scheduled_completion": "",
"vehicle_info": "Véhicule",
"vehicleassociation": "",
"viewallocations": "",
@@ -2434,6 +2464,7 @@
"invoice_total_payable": "",
"iou_form": "",
"job_costing_ro": "",
"job_lifecycle_ro": "",
"job_notes": "",
"key_tag": "",
"labels": {
@@ -2599,6 +2630,18 @@
"generate": ""
},
"labels": {
"advanced_filters": "",
"advanced_filters_false": "",
"advanced_filters_filter_field": "",
"advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "",
"advanced_filters_filters": "",
"advanced_filters_hide": "",
"advanced_filters_show": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_sorter_field": "",
"advanced_filters_sorters": "",
"advanced_filters_true": "",
"dates": "",
"employee": "",
"filterson": "",
@@ -2680,6 +2723,8 @@
"job_costing_ro_date_summary": "",
"job_costing_ro_estimator": "",
"job_costing_ro_ins_co": "",
"job_lifecycle_date_detail": "",
"job_lifecycle_date_summary": "",
"jobs_completed_not_invoiced": "",
"jobs_invoiced_not_exported": "",
"jobs_reconcile": "",
@@ -2769,7 +2814,8 @@
"allemployeetimetickets": "",
"asoftodaytarget": "",
"body": "",
"bodycharttitle": "",
"bodyabbrev": "",
"bodycharttitle": "",
"calendarperiod": "",
"combinedcharttitle": "",
"dailyactual": "",
@@ -2785,13 +2831,14 @@
"productivestatistics": "",
"productivetimeticketsoverdate": "",
"refinish": "",
"refinishcharttitle": "",
"refinishabbrev": "",
"refinishcharttitle": "",
"targets": "",
"thismonth": "",
"thisweek": "",
"timetickets": "",
"timeticketsemployee": "",
"todateactual": "",
"todateactual": "","total": "",
"totalhrs": "",
"totaloverperiod": "",
"weeklyactual": "",

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