Compare commits

..

107 Commits

Author SHA1 Message Date
Dave
8422ea83ae feature/IO-3544-Ant-Select-Deprecation - Make cdklike-dms-post-form.jsx compatible with search on the payer name 2026-02-18 13:50:46 -05:00
Dave
1b84087ef8 feature/IO-3544-Ant-Select-Deprecation - Package Bumps 2026-02-18 12:31:55 -05:00
Dave
a9fdf3da18 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-3544-Ant-Select-Deprecation 2026-02-18 12:25:42 -05:00
Dave Richer
fa2c729ac2 Merged in release/2026-02-27 (pull request #3008)
feature/IO-3523-Fortellis-Corrections-2 - Fix

Approved-by: Allan Carr
2026-02-18 00:45:08 +00:00
Dave Richer
95bb5b03c2 Merged in feature/IO-3523-Fortellis-Corrections-2 (pull request #3006)
feature/IO-3523-Fortellis-Corrections-2 - Fix
2026-02-17 21:06:45 +00:00
Dave
318482c195 feature/IO-3523-Fortellis-Corrections-2 - Fix 2026-02-17 16:06:12 -05:00
Dave Richer
eea9e8e2cc Merged in release/2026-02-13 (pull request #3003)
feature/IO-3558-Reynolds-Part-2 - Send zeroed out estimate line

Approved-by: Allan Carr
2026-02-13 16:48:27 +00:00
Dave Richer
cde12f9970 Merged in feature/IO-3558-Reynolds-Part-2 (pull request #3002)
feature/IO-3558-Reynolds-Part-2 - Send zeroed out estimate line
2026-02-13 16:14:39 +00:00
Dave
48def2b74d feature/IO-3558-Reynolds-Part-2 - Send zeroed out estimate line 2026-02-13 11:13:42 -05:00
Dave Richer
dde7a99956 Merged in release/2026-02-13 (pull request #2999)
Release/2026 02 13 into master-AIO - IO-3503, IO-3510, IO-3521, IO-3533, IO-3551, IO-3556, IO-3557, IO-3558
2026-02-13 00:33:37 +00:00
Dave Richer
49fb2caac0 Merged in release/2026-02-13 (pull request #3000)
Release/2026 02 13
2026-02-13 00:32:41 +00:00
Dave Richer
df964aa14e Merged in feature/IO-3558-Reynolds-Part-2 (pull request #2997)
feature/IO-3558-Reynolds-Part-2 - Prevent exporting without early ro / add a way to fake sconvert state in admin panel
2026-02-12 22:13:38 +00:00
Dave
7619360f37 feature/IO-3558-Reynolds-Part-2 - Prevent exporting without early ro / add a way to fake sconvert state in admin panel 2026-02-12 17:13:06 -05:00
Dave Richer
f15f371e86 Merged in feature/IO-3556-Chattr-Integration (pull request #2995)
6feature/IO-3556-Chattr-Integration - Move to BULLMQ stack
2026-02-12 18:00:27 +00:00
Dave
34fe0cc3bf 6feature/IO-3556-Chattr-Integration - Move to BULLMQ stack 2026-02-12 12:56:17 -05:00
Allan Carr
7acaefb5c5 Merged in feature/IO-3557-Reynold-DMS-Info (pull request #2993)
IO-3557 Reynolds DMS Info

Approved-by: Dave Richer
2026-02-12 14:26:38 +00:00
Allan Carr
ab02da47a2 IO-3557 Reynolds DMS Info
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-02-11 18:39:36 -08:00
Dave
673670eeb4 Merge remote-tracking branch 'origin/release/2026-02-13' into feature/IO-3544-Ant-Select-Deprecation
# Conflicts:
#	client/src/components/jobs-convert-button/jobs-convert-button.component.jsx
2026-02-11 18:23:02 -05:00
Dave Richer
2a7dec90d5 Merged in feature/IO-3558-Reynolds-Part-2 (pull request #2990)
feature/IO-3558-Reynolds-Part-2 - Admin Panel
2026-02-11 23:13:22 +00:00
Dave
6e0b1f65a7 feature/IO-3558-Reynolds-Part-2 - Admin Panel 2026-02-11 18:12:56 -05:00
Dave Richer
8671d1254d Merged in feature/IO-3558-Reynolds-Part-2 (pull request #2987)
Feature/IO-3558 Reynolds Part 2
2026-02-11 22:25:29 +00:00
Dave
0ea254ed4e feature/IO-3558-Reynolds-Part-2 - Admin Panel 2026-02-11 16:57:13 -05:00
Dave
331dcfc063 feature/IO-3558-Reynolds-Part-2 - Admin Panel 2026-02-11 15:47:46 -05:00
Dave
c46804cfdf feature/IO-3558-Reynolds-Part-2 - Initial 2026-02-11 15:33:59 -05:00
Dave Richer
484d09d635 Merged in feature/IO-3556-Chattr-Integration (pull request #2986)
feature/IO-3556-Chattr-Integration - Switch Consent to true
2026-02-11 16:59:51 +00:00
Dave
188a7b47b1 feature/IO-3556-Chattr-Integration - Switch Consent to true 2026-02-11 11:59:16 -05:00
Dave Richer
a6ca93f482 Merged in feature/IO-3556-Chattr-Integration (pull request #2984)
feature/IO-3556-Chattr-Integration - Retry beef up / tweeks
2026-02-11 16:40:26 +00:00
Dave
d08bfc61cd feature/IO-3556-Chattr-Integration - Retry beef up / tweeks 2026-02-11 11:37:47 -05:00
Dave
e6100851b8 feature/IO-3544-Ant-Select-Deprecation - Packages 2026-02-11 10:14:55 -05:00
Dave
e9795072d5 Merge remote-tracking branch 'origin/release/2026-02-13' into feature/IO-3544-Ant-Select-Deprecation 2026-02-11 10:05:58 -05:00
Dave Richer
9b4de1645e Merged in feature/IO-3556-Chattr-Integration (pull request #2982)
fix
2026-02-11 14:54:33 +00:00
Dave
503c217c99 fix 2026-02-11 09:52:22 -05:00
Dave Richer
2333067e02 Merged in hotfix/2026-02-10-backend (pull request #2981)
hotfix/2026-02-10-backend - Move chatter DB stuff over
2026-02-10 23:13:50 +00:00
Dave Richer
953172493e Merged in feature/IO-3556-Chattr-Integration (pull request #2979)
hotfix/2026-02-10-backend - Move chatter DB stuff over
2026-02-10 23:05:13 +00:00
Dave
b444639fca Merge branch 'hotfix/2026-02-10-backend' into feature/IO-3556-Chattr-Integration 2026-02-10 18:03:37 -05:00
Dave
6ee7e56b9b hotfix/2026-02-10-backend - Move chatter DB stuff over 2026-02-10 17:58:26 -05:00
Dave Richer
ffd5acb21a Merged in feature/IO-3556-Chattr-Integration (pull request #2976)
Feature/IO-3556 Chattr Integration
2026-02-10 22:28:54 +00:00
Dave
0340ca5fcc feature/IO-3556-Chattr-Integration - Add in Redis caching for Chatter 2026-02-10 17:25:59 -05:00
Dave
1b2fc8b114 feature/IO-3556-Chattr-Integration 2026-02-10 17:17:44 -05:00
Dave
3745d7a414 feature/IO-3556-Chattr-Integration 2026-02-10 12:48:48 -05:00
Allan Carr
a0efac9bd8 Merged in feature/IO-3551-Export-Reports-Return-Data (pull request #2974)
IO-3551 Export Reports Return Data

Approved-by: Dave Richer
2026-02-09 15:34:22 +00:00
Allan Carr
17a772563c Merged in feature/IO-3521-Pagination-Export-Screens (pull request #2973)
IO-3521 Pagination Disable Show Size Changer

Approved-by: Dave Richer
2026-02-09 15:34:01 +00:00
Allan Carr
b1ce356bd8 IO-3551 Export Reports Return Data
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-02-06 20:45:44 -08:00
Allan Carr
9818cac30e IO-3521 Pagination Disable Show Size Changer
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-02-06 16:10:03 -08:00
Allan Carr
171277630e Merged in feature/IO-3503-Job-Costing-MASH-MAPA-Fix (pull request #2971)
IO-3503 Job Costing Fix

Approved-by: Dave Richer
2026-02-06 23:14:23 +00:00
Allan Carr
d8b400cb8c IO-3503 InstanceManager change
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-02-06 15:15:11 -08:00
Allan Carr
fe7bf684aa IO-3503 Job Costing Fix
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-02-06 15:04:00 -08:00
Dave Richer
7e6c97b3cf Merged in hotfix/2026-02-03 (pull request #2970)
Hotfix/2026 02 03
2026-02-06 22:20:58 +00:00
Dave Richer
773f3d4c84 Merged in release/2026-02-13 (pull request #2969)
IO-3533 Actual Cost Click to Focus
2026-02-06 19:54:36 +00:00
Allan Carr
9c6fe1905d Merged in feature/IO-3533-Actual-Cost-Click-to-Focus (pull request #2967)
IO-3533 Actual Cost Click to Focus

Approved-by: Dave Richer
2026-02-06 19:53:32 +00:00
Allan Carr
2126cccff1 IO-3533 Actual Cost Click to Focus
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-02-06 11:23:34 -08:00
Dave
40d5e02415 feature/IO-3feature/IO-3544-Ant-Select-Deprecation: Dep Bumps 2026-02-05 13:55:50 -05:00
Dave
5b891281d1 Merge remote-tracking branch 'origin/release/2026-02-13' into feature/IO-3544-Ant-Select-Deprecation
# Conflicts:
#	client/src/components/bill-form/bill-form.lines.component.jsx
2026-02-03 16:52:36 -05:00
Dave Richer
56559dd3ff Merged in hotfix/2026-02-03 (pull request #2965)
Hotfix/2026 02 03

Approved-by: Allan Carr
2026-02-03 21:49:28 +00:00
Dave
fde137d7f7 Merge branch 'feature/IO-3550-Labor-Adjustment-Popover' into hotfix/2026-02-03 2026-02-03 16:00:28 -05:00
Dave
b797bf7dc9 Merge branch 'feature/IO-3548-Bill-Modal-TabOrder' into hotfix/2026-02-03 2026-02-03 16:00:08 -05:00
Dave Richer
37c3be5cde Merged in feature/IO-3550-Labor-Adjustment-Popover (pull request #2963)
feature/IO-3550-Labor-Adjustment-Popover - Fix
2026-02-03 20:58:01 +00:00
Dave
b87d1a65fe feature/IO-3550-Labor-Adjustment-Popover - Fix 2026-02-03 15:57:24 -05:00
Dave Richer
35c832dbc3 Merged in feature/IO-3545-Production-Board-List-DND (pull request #2961)
feature/IO-3548-Bill-Modal-TabOrder
2026-02-03 20:51:19 +00:00
Dave
019b3cf4da feature/IO-3548-Bill-Modal-TabOrder 2026-02-03 15:50:48 -05:00
Dave Richer
27f4385539 Merged in feature/IO-3548-Bill-Modal-TabOrder (pull request #2959)
feature/IO-3548-Bill-Modal-TabOrder
2026-02-03 20:43:39 +00:00
Dave
ad520ab23e feature/IO-3548-Bill-Modal-TabOrder 2026-02-03 15:42:10 -05:00
Dave Richer
b3716521ec Merged in feature/IO-3545-Production-Board-List-DND (pull request #2957)
Feature/IO-3545 Production Board List DND
2026-02-03 20:30:42 +00:00
Dave
05ae0801e5 feature/IO-3545-Production-Board-List-DND - EMP assignment selector fix 2026-02-03 15:29:03 -05:00
Dave
332ade96e5 feature/IO-3545-Production-Board-List-DND - Checkpoint 2026-02-03 15:17:20 -05:00
Dave
3acec55c0e feature/IO-3545-Production-Board-List-DND - Checkpoint 2026-02-03 15:01:10 -05:00
Dave
da0462f14c feature/IO-3545-Production-Board-List-DND - Checkpoint 2026-02-03 14:56:04 -05:00
Dave
2cc9fa961e feature/IO-3545-Production-Board-List-DND - Checkpoint 2026-02-03 14:34:42 -05:00
Allan Carr
2646e85863 Merged in feature/IO-3510-Autohouse-Datapump-Enhancements (pull request #2956)
IO-3510 Autohouse Datapump Enhancements

Approved-by: Dave Richer
2026-02-03 19:20:30 +00:00
Dave
1b6fe4d18e feature/IO-3545-Production-Board-List-DND - Checkpoint 2026-02-03 13:26:17 -05:00
Dave
22aae0a7f1 feature/IO-3545-Production-Board-List-DND - Checkpoint 2026-02-03 13:21:32 -05:00
Dave
71043313d6 feature/IO-3544-Ant-Select-Deprecation - Fix filtering 2026-02-03 12:40:01 -05:00
Dave
c9620a3f6f feature/IO-3544-Ant-Select-Deprecation - Fix filtering 2026-02-03 11:10:44 -05:00
Dave Richer
cfbd6f93c3 Merged in master-AIO (pull request #2954)
Master AIO
2026-02-03 15:52:51 +00:00
Dave
cdfae5a429 feature/IO-3544-Ant-Select-Deprecation - finish 2026-02-03 10:51:14 -05:00
Patrick Fic
db1b701a96 Merged in hotfix/2026-02-02 (pull request #2951)
Hotfix/2026 02 02

Approved-by: Dave Richer
2026-02-02 22:49:14 +00:00
Dave
2746421c09 hotfix/2026-02-02 - 2026-02-02 17:48:03 -05:00
Dave
5217120994 hotfix/2026-02-02 - Parts order manual discounting box 2026-02-02 17:39:47 -05:00
Dave
77f72a2a12 Merge branch 'hotfix/2026-02-02' of bitbucket.org:snaptsoft/bodyshop into hotfix/2026-02-02 2026-02-02 17:11:06 -05:00
Dave
a84ad4ee32 hotfix/2026-02-02 - remove check on missing line ids 2026-02-02 17:10:56 -05:00
Dave Richer
2cacd75822 Merged in bugfix/IO-3533 (pull request #2948)
bugfix/IO-3533 - Fix
2026-02-02 22:05:56 +00:00
Dave
217a0b84ac bugfix/IO-3533 - Fix 2026-02-02 17:04:06 -05:00
Dave Richer
f53ed8c427 Merged in feature/IO-3532-parts-queue-ui-adjustments (pull request #2944)
feature/IO-3532-parts-queue-ui-adjustments

Approved-by: Patrick Fic
2026-02-02 21:51:31 +00:00
Dave Richer
f8b7588a04 Merged in feature/IO-3542-fix-searches (pull request #2945)
feature/IO-3542-fix-searches

Approved-by: Patrick Fic
2026-02-02 21:46:24 +00:00
Patrick Fic
ee3cb4456d Merged in feature/IO-3531-apollo-rerender (pull request #2946)
IO-3531 remove loading on parts order page.
2026-02-02 21:45:59 +00:00
Patrick Fic
ae05692c46 IO-3531 remove loading on parts order page. 2026-02-02 13:45:25 -08:00
Dave
e01a2af5a4 feature/IO-3542-fix-searches 2026-02-02 16:44:49 -05:00
Dave
9c0cb5f80b Merge branch 'feature/IO-3532-parts-queue-ui-adjustments' of bitbucket.org:snaptsoft/bodyshop into feature/IO-3532-parts-queue-ui-adjustments 2026-02-02 16:15:23 -05:00
Dave
1f726aca4d feature/IO-3532-parts-queue-ui-adjustments 2026-02-02 16:14:44 -05:00
Patrick Fic
b9f398cf2d Merged in feature/IO-3532-parts-queue-ui-adjustments (pull request #2943)
IO-3532 resolve tooltip on owner name.
2026-02-02 20:57:05 +00:00
Patrick Fic
ff73a14610 IO-3532 resolve tooltip on owner name. 2026-02-02 12:55:29 -08:00
Patrick Fic
1e44d4fe42 Merged in feature/IO-3539-print-center-popovers (pull request #2941)
IO-3539 resolve print center popoves.
2026-02-02 20:38:49 +00:00
Patrick Fic
0f42875d1b IO-3539 resolve print center popoves. 2026-02-02 12:38:29 -08:00
Patrick Fic
a0f1299006 Merged in feature/IO-3538-receivec-cm-on-parts-order (pull request #2940)
IO-3538 Resolve missing id on receive return.
2026-02-02 20:23:20 +00:00
Patrick Fic
87d8a5d746 IO-3538 Resolve missing id on receive return. 2026-02-02 12:22:58 -08:00
Patrick Fic
268851902a Merged in feature/IO-3535-fed-tax-toggle-bill-posting (pull request #2935)
IO-3535 Resolve federal tax default off on received parts order.
2026-02-02 20:07:26 +00:00
Dave Richer
68bb7d2529 Merged in bugfix/IO-3533 (pull request #2937)
bugfix/IO-3533 - Disable on blurr and on focus handlers in bill entry modal

Approved-by: Patrick Fic
2026-02-02 20:06:13 +00:00
Patrick Fic
d50db12330 Merged in feature/IO-3534-bill-discrep-coloring (pull request #2936)
IO-3534 Adjust value prop to content for antd prop change to fix color display.
2026-02-02 20:05:54 +00:00
Patrick Fic
1438986c18 Merged in feature/IO-3532-parts-queue-ui-adjustments (pull request #2938)
IO-3532 Resolve parts queue pages.
2026-02-02 20:05:15 +00:00
Patrick Fic
c047699fbb Merged in feature/IO-3531-apollo-rerender (pull request #2939)
IO-3531 Change global apollo config setting to prevent rerenders.
2026-02-02 20:03:29 +00:00
Patrick Fic
e5b7fcb919 IO-3531 Change global apollo config setting to prevent rerenders. 2026-02-02 12:02:11 -08:00
Patrick Fic
cadcfc9b0d IO-3532 Resolve parts queue pages. 2026-02-02 11:21:22 -08:00
Dave
55023ceaca feature/IO-3534-bill-discrep-coloring: Remove unused console.log 2026-02-02 12:47:04 -05:00
Dave
45e143578c bugfix/IO-3533 - Disable on blurr and on focus handlers in bill entry modal 2026-02-02 12:34:54 -05:00
Patrick Fic
28a41f7637 IO-3534 Adjust value prop to content for antd prop change to fix color display. 2026-02-02 09:33:37 -08:00
Patrick Fic
2a2edeadb9 IO-3535 Resolve federal tax default off on received parts order. 2026-02-02 09:25:58 -08:00
Allan Carr
52c9b9a290 IO-3510 Autohouse Datapump Enhancements
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-27 19:20:12 -08:00
126 changed files with 7847 additions and 3749 deletions

View File

@@ -13,4 +13,5 @@
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
bodyshop_translations.babel .env.localstack.docker
bodyshop_translations.babel

File diff suppressed because it is too large Load Diff

949
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,41 +8,45 @@
"private": true, "private": true,
"proxy": "http://localhost:4000", "proxy": "http://localhost:4000",
"dependencies": { "dependencies": {
"@amplitude/analytics-browser": "^2.34.0", "@amplitude/analytics-browser": "^2.35.0",
"@ant-design/pro-layout": "^7.22.6", "@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.1.3", "@apollo/client": "^4.1.4",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/is-prop-valid": "^1.4.0", "@emotion/is-prop-valid": "^1.4.0",
"@fingerprintjs/fingerprintjs": "^5.0.1", "@fingerprintjs/fingerprintjs": "^5.0.1",
"@firebase/analytics": "^0.10.19", "@firebase/analytics": "^0.10.19",
"@firebase/app": "^0.14.7", "@firebase/app": "^0.14.8",
"@firebase/auth": "^1.12.0", "@firebase/auth": "^1.12.0",
"@firebase/firestore": "^4.10.0", "@firebase/firestore": "^4.11.0",
"@firebase/messaging": "^0.12.22", "@firebase/messaging": "^0.12.22",
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.11.2", "@reduxjs/toolkit": "^2.11.2",
"@sentry/cli": "^3.1.0", "@sentry/cli": "^3.2.0",
"@sentry/react": "^10.38.0", "@sentry/react": "^10.39.0",
"@sentry/vite-plugin": "^4.8.0", "@sentry/vite-plugin": "^4.9.1",
"@splitsoftware/splitio-react": "^2.6.1", "@splitsoftware/splitio-react": "^2.6.1",
"@tanem/react-nprogress": "^5.0.58", "@tanem/react-nprogress": "^5.0.63",
"antd": "^6.2.2", "antd": "^6.3.0",
"apollo-link-logger": "^3.0.0", "apollo-link-logger": "^3.0.0",
"autosize": "^6.0.1", "autosize": "^6.0.1",
"axios": "^1.13.4", "axios": "^1.13.5",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"css-box-model": "^1.2.1", "css-box-model": "^1.2.1",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"dayjs-business-days2": "^1.3.2", "dayjs-business-days2": "^1.3.2",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^17.2.3", "dotenv": "^17.3.1",
"env-cmd": "^11.0.0", "env-cmd": "^11.0.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"graphql": "^16.12.0", "graphql": "^16.12.0",
"graphql-ws": "^6.0.7", "graphql-ws": "^6.0.7",
"i18next": "^25.8.0", "i18next": "^25.8.11",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.1",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.12.36", "libphonenumber-js": "^1.12.37",
"lightningcss": "^1.31.1", "lightningcss": "^1.31.1",
"logrocket": "^12.0.0", "logrocket": "^12.0.0",
"markerjs2": "^2.32.7", "markerjs2": "^2.32.7",
@@ -50,7 +54,7 @@
"normalize-url": "^8.1.1", "normalize-url": "^8.1.1",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"phone": "^3.1.70", "phone": "^3.1.70",
"posthog-js": "^1.336.4", "posthog-js": "^1.351.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^9.3.1", "query-string": "^9.3.1",
"raf-schd": "^4.0.3", "raf-schd": "^4.0.3",
@@ -59,7 +63,6 @@
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^8.0.1", "react-cookie": "^8.0.1",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.1", "react-grid-gallery": "^1.0.1",
"react-grid-layout": "^2.2.2", "react-grid-layout": "^2.2.2",
"react-i18next": "^16.5.4", "react-i18next": "^16.5.4",
@@ -84,7 +87,7 @@
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"sass": "^1.97.3", "sass": "^1.97.3",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"styled-components": "^6.3.8", "styled-components": "^6.3.10",
"vite-plugin-ejs": "^1.7.0", "vite-plugin-ejs": "^1.7.0",
"web-vitals": "^5.1.0" "web-vitals": "^5.1.0"
}, },
@@ -141,11 +144,11 @@
"@emotion/babel-plugin": "^11.13.5", "@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@playwright/test": "^1.58.0", "@playwright/test": "^1.58.2",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.4",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"browserslist": "^4.28.1", "browserslist": "^4.28.1",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
@@ -153,16 +156,16 @@
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.2", "eslint-plugin-react-compiler": "^19.1.0-rc.2",
"globals": "^17.2.0", "globals": "^17.3.0",
"jsdom": "^27.4.0", "jsdom": "^28.1.0",
"memfs": "^4.56.10", "memfs": "^4.56.10",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",
"playwright": "^1.58.0", "playwright": "^1.58.2",
"react-error-overlay": "^6.1.0", "react-error-overlay": "^6.1.0",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3", "source-map-explorer": "^2.5.3",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-plugin-babel": "^1.4.1", "vite-plugin-babel": "^1.5.1",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.25.0", "vite-plugin-node-polyfills": "^0.25.0",
"vite-plugin-pwa": "^1.2.0", "vite-plugin-pwa": "^1.2.0",

View File

@@ -446,3 +446,32 @@
//.rbc-time-header-gutter { //.rbc-time-header-gutter {
// padding: 0; // padding: 0;
//} //}
/* globally allow shrink inside table cells */
.prod-list-table .ant-table-cell,
.prod-list-table .ant-table-cell > * {
min-width: 0;
}
/* common AntD offenders */
.prod-list-table > .ant-table-cell .ant-space,
.ant-table-cell .ant-space-item {
min-width: 0;
}
/* Keep your custom header content on the left, push AntD sorter to the far right */
.prod-list-table .ant-table-column-sorters {
display: flex !important;
align-items: center;
width: 100%;
}
.prod-list-table .ant-table-column-title {
flex: 1 1 auto;
min-width: 0; /* allows ellipsis to work */
}
.prod-list-table .ant-table-column-sorter {
margin-left: auto;
flex: 0 0 auto;
}

View File

@@ -182,7 +182,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
<Table <Table
loading={loading} loading={loading}
dataSource={dataSource} dataSource={dataSource}
pagination={{ placement: "top", pageSize: exportPageLimit }} pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -195,7 +195,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
<Table <Table
loading={loading} loading={loading}
dataSource={dataSource} dataSource={dataSource}
pagination={{ placement: "top", pageSize: exportPageLimit }} pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -212,7 +212,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
<Table <Table
loading={loading} loading={loading}
dataSource={dataSource} dataSource={dataSource}
pagination={{ placement: "top", pageSize: exportPageLimit }} pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -29,19 +29,18 @@ export function AllocationsAssignmentComponent({
<Select <Select
id="employeeSelector" id="employeeSelector"
showSearch={{ showSearch={{
optionFilterProp: "children", optionFilterProp: "label",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}} }}
style={{ width: 200 }} style={{ width: 200 }}
placeholder="Select a person" placeholder="Select a person"
onChange={onChange} onChange={onChange}
> options={bodyshop.employees.map((emp) => ({
{bodyshop.employees.map((emp) => ( value: emp.id,
<Select.Option value={emp.id} key={emp.id}> key: emp.id,
{`${emp.first_name} ${emp.last_name}`} label: `${emp.first_name} ${emp.last_name}`
</Select.Option> }))}
))} />
</Select>
<InputNumber <InputNumber
defaultValue={assignment.hours} defaultValue={assignment.hours}
placeholder={t("joblines.fields.mod_lb_hrs")} placeholder={t("joblines.fields.mod_lb_hrs")}

View File

@@ -31,19 +31,17 @@ export default connect(
<div> <div>
<Select <Select
showSearch={{ showSearch={{
optionFilterProp: "children", optionFilterProp: "label",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}} }}
style={{ width: 200 }} style={{ width: 200 }}
placeholder="Select a person" placeholder="Select a person"
onChange={onChange} onChange={onChange}
> options={bodyshop.employees.map((emp) => ({
{bodyshop.employees.map((emp) => ( value: emp.id,
<Select.Option value={emp.id} key={emp.id}> label: `${emp.first_name} ${emp.last_name}`
{`${emp.first_name} ${emp.last_name}`} }))}
</Select.Option> />
))}
</Select>
<Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}> <Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
Assign Assign

View File

@@ -52,6 +52,9 @@ export default function BillCmdReturnsTableComponent({ form, returnLoading, retu
{fields.map((field, index) => ( {fields.map((field, index) => (
<tr key={field.key}> <tr key={field.key}>
<td> <td>
<Form.Item hidden key={`${index}id`} name={[field.name, "id"]}>
<ReadOnlyFormItemComponent />
</Form.Item>
<Form.Item <Form.Item
// label={t("joblines.fields.line_desc")} // label={t("joblines.fields.line_desc")}
key={`${index}line_desc`} key={`${index}line_desc`}

View File

@@ -99,20 +99,22 @@ export function BillFormItemsExtendedFormItem({
}} }}
</Form.Item> </Form.Item>
<Form.Item label={t("billlines.fields.cost_center")} name={["billlineskeys", record.id, "cost_center"]}> <Form.Item label={t("billlines.fields.cost_center")} name={["billlineskeys", record.id, "cost_center"]}>
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}> <Select
{bodyshopHasDmsKey(bodyshop) showSearch
? CiecaSelect(true, false) style={{ minWidth: "3rem" }}
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)} disabled={disabled}
</Select> options={
bodyshopHasDmsKey(bodyshop)
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => ({ value: item.name, label: item.name }))
}
/>
</Form.Item> </Form.Item>
<Form.Item label={t("billlines.fields.location")} name={["billlineskeys", record.id, "location"]}> <Form.Item label={t("billlines.fields.location")} name={["billlineskeys", record.id, "location"]}>
<Select disabled={disabled}> <Select
{bodyshop.md_parts_locations.map((loc, idx) => ( disabled={disabled}
<Select.Option key={idx} value={loc}> options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
{loc} />
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("billlines.fields.deductedfromlbr")} label={t("billlines.fields.deductedfromlbr")}
@@ -136,22 +138,10 @@ export function BillFormItemsExtendedFormItem({
]} ]}
name={["billlineskeys", record.id, "lbr_adjustment", "mod_lbr_ty"]} name={["billlineskeys", record.id, "lbr_adjustment", "mod_lbr_ty"]}
> >
<Select allowClear> <Select
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option> allowClear
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option> options={CiecaSelect(false, true)}
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option> />
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("jobs.labels.adjustmentrate")} label={t("jobs.labels.adjustmentrate")}

View File

@@ -328,13 +328,12 @@ export function BillFormComponent({
</Form.Item> </Form.Item>
{!billEdit && ( {!billEdit && (
<Form.Item label={t("bills.fields.allpartslocation")} name="location"> <Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select style={{ width: "10rem" }} disabled={disabled} allowClear> <Select
{bodyshop.md_parts_locations.map((loc, idx) => ( style={{ width: "10rem" }}
<Select.Option key={idx} value={loc}> disabled={disabled}
{loc} allowClear
</Select.Option> options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
))} />
</Select>
</Form.Item> </Form.Item>
)} )}
</LayoutFormRow> </LayoutFormRow>
@@ -373,9 +372,11 @@ export function BillFormComponent({
"local_tax_rate" "local_tax_rate"
]); ]);
let totals; let totals;
if (!!values.total && !!values.billlines && values.billlines.length > 0) if (!!values.total && !!values.billlines && values.billlines.length > 0) {
totals = CalculateBillTotal(values); totals = CalculateBillTotal(values);
if (totals) }
if (totals) {
return ( return (
// TODO: Align is not correct // TODO: Align is not correct
// eslint-disable-next-line react/no-unknown-property // eslint-disable-next-line react/no-unknown-property
@@ -414,7 +415,7 @@ export function BillFormComponent({
<Statistic <Statistic
title={t("bills.labels.discrepancy")} title={t("bills.labels.discrepancy")}
styles={{ styles={{
value: { content: {
color: totals.discrepancy.getAmount() === 0 ? "green" : "red" color: totals.discrepancy.getAmount() === 0 ? "green" : "red"
} }
}} }}
@@ -427,6 +428,7 @@ export function BillFormComponent({
) : null} ) : null}
</div> </div>
); );
}
return null; return null;
}} }}
</Form.Item> </Form.Item>

View File

@@ -1,6 +1,7 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons"; import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd"; import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
import { useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -32,6 +33,7 @@ export function BillEnterModalLinesComponent({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form; const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const firstFieldRefs = useRef({});
const CONTROL_HEIGHT = 32; const CONTROL_HEIGHT = 32;
@@ -90,6 +92,7 @@ export function BillEnterModalLinesComponent({
}); });
}; };
// Only fill actual_cost when the user forward-tabs out of Retail (actual_price)
const autofillActualCost = (index) => { const autofillActualCost = (index) => {
Promise.resolve().then(() => { Promise.resolve().then(() => {
const retailRaw = form.getFieldValue(["billlines", index, "actual_price"]); const retailRaw = form.getFieldValue(["billlines", index, "actual_price"]);
@@ -154,6 +157,9 @@ export function BillEnterModalLinesComponent({
), ),
formInput: (record, index) => ( formInput: (record, index) => (
<BillLineSearchSelect <BillLineSearchSelect
ref={(el) => {
firstFieldRefs.current[index] = el;
}}
disabled={disabled} disabled={disabled}
options={lineData} options={lineData}
style={{ style={{
@@ -164,10 +170,9 @@ export function BillEnterModalLinesComponent({
}} }}
allowRemoved={form.getFieldValue("is_credit_memo") || false} allowRemoved={form.getFieldValue("is_credit_memo") || false}
onSelect={(value, opt) => { onSelect={(value, opt) => {
const d = normalizeDiscount(discount); // IMPORTANT:
const retail = Number(opt.cost); // Do NOT autofill actual_cost here. It should only fill when the user forward-tabs
const computedActual = Number.isFinite(retail) ? round2(retail * (1 - d)) : null; // from Retail (actual_price) -> Actual Cost (actual_cost).
setFieldsValue({ setFieldsValue({
billlines: (getFieldValue("billlines") || []).map((item, idx) => { billlines: (getFieldValue("billlines") || []).map((item, idx) => {
if (idx !== index) return item; if (idx !== index) return item;
@@ -178,7 +183,7 @@ export function BillEnterModalLinesComponent({
quantity: opt.part_qty || 1, quantity: opt.part_qty || 1,
actual_price: opt.cost, actual_price: opt.cost,
original_actual_price: opt.cost, original_actual_price: opt.cost,
actual_cost: isBlank(item.actual_cost) ? computedActual : item.actual_cost, // actual_cost intentionally untouched here
cost_center: opt.part_type cost_center: opt.part_type
? bodyshopHasDmsKey(bodyshop) ? bodyshopHasDmsKey(bodyshop)
? opt.part_type !== "PAE" ? opt.part_type !== "PAE"
@@ -205,7 +210,7 @@ export function BillEnterModalLinesComponent({
label: t("billlines.fields.line_desc"), label: t("billlines.fields.line_desc"),
rules: [{ required: true }] rules: [{ required: true }]
}), }),
formInput: () => <Input.TextArea disabled={disabled} autoSize /> formInput: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} />
}, },
{ {
title: t("billlines.fields.quantity"), title: t("billlines.fields.quantity"),
@@ -234,7 +239,7 @@ export function BillEnterModalLinesComponent({
}) })
] ]
}), }),
formInput: () => <InputNumber precision={0} min={1} disabled={disabled} /> formInput: () => <InputNumber precision={0} min={1} disabled={disabled} tabIndex={0} />
}, },
{ {
title: t("billlines.fields.actual_price"), title: t("billlines.fields.actual_price"),
@@ -251,9 +256,10 @@ export function BillEnterModalLinesComponent({
<CurrencyInput <CurrencyInput
min={0} min={0}
disabled={disabled} disabled={disabled}
onBlur={() => autofillActualCost(index)} tabIndex={0}
// NOTE: Autofill should only happen on forward Tab out of Retail
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Tab") autofillActualCost(index); if (e.key === "Tab" && !e.shiftKey) autofillActualCost(index);
}} }}
/> />
), ),
@@ -328,6 +334,7 @@ export function BillEnterModalLinesComponent({
min={0} min={0}
disabled={disabled} disabled={disabled}
controls={false} controls={false}
tabIndex={0}
style={{ width: "100%", height: CONTROL_HEIGHT }} style={{ width: "100%", height: CONTROL_HEIGHT }}
onFocus={() => autofillActualCost(index)} onFocus={() => autofillActualCost(index)}
/> />
@@ -392,11 +399,17 @@ export function BillEnterModalLinesComponent({
rules: [{ required: true }] rules: [{ required: true }]
}), }),
formInput: () => ( formInput: () => (
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}> <Select
{bodyshopHasDmsKey(bodyshop) showSearch
? CiecaSelect(true, false) style={{ minWidth: "3rem" }}
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)} disabled={disabled}
</Select> tabIndex={0}
options={
bodyshopHasDmsKey(bodyshop)
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => ({ value: item.name, label: item.name }))
}
/>
) )
}, },
...(billEdit ...(billEdit
@@ -412,13 +425,11 @@ export function BillEnterModalLinesComponent({
name: [field.name, "location"] name: [field.name, "location"]
}), }),
formInput: () => ( formInput: () => (
<Select disabled={disabled}> <Select
{bodyshop.md_parts_locations.map((loc, idx) => ( disabled={disabled}
<Select.Option key={idx} value={loc}> tabIndex={0}
{loc} options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
</Select.Option> />
))}
</Select>
) )
} }
]), ]),
@@ -432,7 +443,7 @@ export function BillEnterModalLinesComponent({
key: `${field.name}deductedfromlbr`, key: `${field.name}deductedfromlbr`,
name: [field.name, "deductedfromlbr"] name: [field.name, "deductedfromlbr"]
}), }),
formInput: () => <Switch disabled={disabled} />, formInput: () => <Switch disabled={disabled} tabIndex={0} />,
additional: (record, index) => ( additional: (record, index) => (
<Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}> <Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}>
{() => { {() => {
@@ -459,22 +470,10 @@ export function BillEnterModalLinesComponent({
rules={[{ required: true }]} rules={[{ required: true }]}
name={[record.name, "lbr_adjustment", "mod_lbr_ty"]} name={[record.name, "lbr_adjustment", "mod_lbr_ty"]}
> >
<Select allowClear> <Select
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option> allowClear
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option> options={CiecaSelect(false, true)}
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option> />
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
</Select>
</Form.Item> </Form.Item>
{Enhanced_Payroll.treatment === "on" ? ( {Enhanced_Payroll.treatment === "on" ? (
@@ -517,9 +516,13 @@ export function BillEnterModalLinesComponent({
formItemProps: (field) => ({ formItemProps: (field) => ({
key: `${field.name}fedtax`, key: `${field.name}fedtax`,
valuePropName: "checked", valuePropName: "checked",
name: [field.name, "applicable_taxes", "federal"] name: [field.name, "applicable_taxes", "federal"],
initialValue: InstanceRenderManager({
imex: true,
rome: false
})
}), }),
formInput: () => <Switch disabled={disabled} /> formInput: () => <Switch disabled={disabled} tabIndex={0} />
} }
] ]
}), }),
@@ -534,7 +537,7 @@ export function BillEnterModalLinesComponent({
valuePropName: "checked", valuePropName: "checked",
name: [field.name, "applicable_taxes", "state"] name: [field.name, "applicable_taxes", "state"]
}), }),
formInput: () => <Switch disabled={disabled} /> formInput: () => <Switch disabled={disabled} tabIndex={0} />
}, },
...InstanceRenderManager({ ...InstanceRenderManager({
@@ -550,7 +553,7 @@ export function BillEnterModalLinesComponent({
valuePropName: "checked", valuePropName: "checked",
name: [field.name, "applicable_taxes", "local"] name: [field.name, "applicable_taxes", "local"]
}), }),
formInput: () => <Switch disabled={disabled} /> formInput: () => <Switch disabled={disabled} tabIndex={0} />
} }
] ]
}), }),
@@ -570,6 +573,7 @@ export function BillEnterModalLinesComponent({
icon={<DeleteFilled />} icon={<DeleteFilled />}
disabled={disabled || invLen > 0} disabled={disabled || invLen > 0}
onClick={() => remove(record.name)} onClick={() => remove(record.name)}
tabIndex={0}
/> />
{Simple_Inventory.treatment === "on" && ( {Simple_Inventory.treatment === "on" && (
@@ -641,12 +645,19 @@ export function BillEnterModalLinesComponent({
<Button <Button
disabled={disabled} disabled={disabled}
onClick={() => { onClick={() => {
const newIndex = fields.length;
add( add(
InstanceRenderManager({ InstanceRenderManager({
imex: { applicable_taxes: { federal: true } }, imex: { applicable_taxes: { federal: true } },
rome: { applicable_taxes: { federal: false } } rome: { applicable_taxes: { federal: false } }
}) })
); );
setTimeout(() => {
const firstField = firstFieldRefs.current[newIndex];
if (firstField?.focus) {
firstField.focus();
}
}, 100);
}} }}
style={{ width: "100%" }} style={{ width: "100%" }}
> >

View File

@@ -57,7 +57,6 @@ const CardPaymentModalComponent = ({
QUERY_RO_AND_OWNER_BY_JOB_PKS, QUERY_RO_AND_OWNER_BY_JOB_PKS,
{ {
fetchPolicy: "network-only", fetchPolicy: "network-only",
notifyOnNetworkStatusChange: true
} }
); );

View File

@@ -47,7 +47,6 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
const [getConversations, { loading, data, refetch, called }] = useLazyQuery(CONVERSATION_LIST_QUERY, { const [getConversations, { loading, data, refetch, called }] = useLazyQuery(CONVERSATION_LIST_QUERY, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
notifyOnNetworkStatusChange: true,
...(pollInterval > 0 ? { pollInterval } : {}) ...(pollInterval > 0 ? { pollInterval } : {})
}); });

View File

@@ -19,13 +19,12 @@ export default function ChatTagRoComponent({ roOptions, loading, handleSearch, h
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
onSelect={handleInsertTag} onSelect={handleInsertTag}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />} notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
> options={roOptions.map((item, idx) => ({
{roOptions.map((item, idx) => ( key: item.id || idx,
<Select.Option key={item.id || idx}> value: item.id || idx,
{` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`} label: ` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`
</Select.Option> }))}
))} />
</Select>
</div> </div>
{loading ? <LoadingOutlined /> : null} {loading ? <LoadingOutlined /> : null}

View File

@@ -309,13 +309,13 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
} }
]} ]}
> >
<Select> <Select
{bodyshop.md_ins_cos.map((s) => ( options={bodyshop.md_ins_cos.map((s) => ({
<Select.Option key={s.name} value={s.name}> key: s.name,
{s.name} value: s.name,
</Select.Option> label: s.name
))} }))}
</Select> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={"class"} name={"class"}
@@ -327,13 +327,13 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
} }
]} ]}
> >
<Select> <Select
{bodyshop.md_classes.map((s) => ( options={bodyshop.md_classes.map((s) => ({
<Select.Option key={s} value={s}> key: s,
{s} value: s,
</Select.Option> label: s
))} }))}
</Select> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("contracts.labels.convertform.applycleanupcharge")} label={t("contracts.labels.convertform.applycleanupcharge")}

View File

@@ -2,8 +2,6 @@ import { useEffect, useState } from "react";
import { Select } from "antd"; import { Select } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const { Option } = Select;
const ContractStatusComponent = ({ value, onChange, ref }) => { const ContractStatusComponent = ({ value, onChange, ref }) => {
const [option, setOption] = useState(value); const [option, setOption] = useState(value);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -15,11 +13,17 @@ const ContractStatusComponent = ({ value, onChange, ref }) => {
}, [value, option, onChange]); }, [value, option, onChange]);
return ( return (
<Select ref={ref} value={option} style={{ width: 100 }} onChange={setOption}> <Select
<Option value="contracts.status.new">{t("contracts.status.new")}</Option> ref={ref}
<Option value="contracts.status.out">{t("contracts.status.out")}</Option> value={option}
<Option value="contracts.status.returned">{t("contracts.status.out")}</Option> style={{ width: 100 }}
</Select> onChange={setOption}
options={[
{ value: "contracts.status.new", label: t("contracts.status.new") },
{ value: "contracts.status.out", label: t("contracts.status.out") },
{ value: "contracts.status.returned", label: t("contracts.status.out") }
]}
/>
); );
}; };

View File

@@ -2,8 +2,6 @@ import { Select } from "antd";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const { Option } = Select;
const CourtesyCarReadinessComponent = ({ value, onChange, ref }) => { const CourtesyCarReadinessComponent = ({ value, onChange, ref }) => {
const [option, setOption] = useState(value); const [option, setOption] = useState(value);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -23,10 +21,11 @@ const CourtesyCarReadinessComponent = ({ value, onChange, ref }) => {
width: 100 width: 100
}} }}
onChange={setOption} onChange={setOption}
> options={[
<Option value="courtesycars.readiness.ready">{t("courtesycars.readiness.ready")}</Option> { value: "courtesycars.readiness.ready", label: t("courtesycars.readiness.ready") },
<Option value="courtesycars.readiness.notready">{t("courtesycars.readiness.notready")}</Option> { value: "courtesycars.readiness.notready", label: t("courtesycars.readiness.notready") }
</Select> ]}
/>
); );
}; };
export default CourtesyCarReadinessComponent; export default CourtesyCarReadinessComponent;

View File

@@ -2,8 +2,6 @@ import { useEffect, useState } from "react";
import { Select } from "antd"; import { Select } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const { Option } = Select;
const CourtesyCarStatusComponent = ({ value, onChange, ref }) => { const CourtesyCarStatusComponent = ({ value, onChange, ref }) => {
const [option, setOption] = useState(value); const [option, setOption] = useState(value);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -22,14 +20,15 @@ const CourtesyCarStatusComponent = ({ value, onChange, ref }) => {
width: 100 width: 100
}} }}
onChange={setOption} onChange={setOption}
> options={[
<Option value="courtesycars.status.in">{t("courtesycars.status.in")}</Option> { value: "courtesycars.status.in", label: t("courtesycars.status.in") },
<Option value="courtesycars.status.inservice">{t("courtesycars.status.inservice")}</Option> { value: "courtesycars.status.inservice", label: t("courtesycars.status.inservice") },
<Option value="courtesycars.status.out">{t("courtesycars.status.out")}</Option> { value: "courtesycars.status.out", label: t("courtesycars.status.out") },
<Option value="courtesycars.status.sold">{t("courtesycars.status.sold")}</Option> { value: "courtesycars.status.sold", label: t("courtesycars.status.sold") },
<Option value="courtesycars.status.leasereturn">{t("courtesycars.status.leasereturn")}</Option> { value: "courtesycars.status.leasereturn", label: t("courtesycars.status.leasereturn") },
<Option value="courtesycars.status.unavailable">{t("courtesycars.status.unavailable")}</Option> { value: "courtesycars.status.unavailable", label: t("courtesycars.status.unavailable") }
</Select> ]}
/>
); );
}; };
export default CourtesyCarStatusComponent; export default CourtesyCarStatusComponent;

View File

@@ -36,7 +36,7 @@ export function DashboardTotalProductionHours({ bodyshop, data, ...cardProps })
<Statistic <Statistic
title={t("dashboard.labels.prodhrs")} title={t("dashboard.labels.prodhrs")}
value={hours.total.toFixed(1)} value={hours.total.toFixed(1)}
styles={{ value: { color: aboveTargetHours ? "green" : "red" } }} styles={{ content: { color: aboveTargetHours ? "green" : "red" } }}
/> />
</Space> </Space>
</Card> </Card>

View File

@@ -23,13 +23,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(DmsCustomerSelector)
* @constructor * @constructor
*/ */
export function DmsCustomerSelector(props) { export function DmsCustomerSelector(props) {
const { bodyshop, jobid, socket, rrOptions = {} } = props; const { bodyshop, jobid, job, socket, rrOptions = {} } = props;
// Centralized "mode" (provider + transport) // Centralized "mode" (provider + transport)
const mode = props.mode; const mode = props.mode;
// Stable base props for children // Stable base props for children
const base = useMemo(() => ({ bodyshop, jobid, socket }), [bodyshop, jobid, socket]); const base = useMemo(() => ({ bodyshop, jobid, job, socket }), [bodyshop, jobid, job, socket]);
switch (mode) { switch (mode) {
case DMS_MAP.reynolds: { case DMS_MAP.reynolds: {

View File

@@ -1,4 +1,4 @@
import { Alert, Button, Checkbox, Col, message, Space, Table } from "antd"; import { Alert, Button, Checkbox, message, Modal, Space, Table } from "antd";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
@@ -47,6 +47,7 @@ const rrAddressToString = (addr) => {
export default function RRCustomerSelector({ export default function RRCustomerSelector({
jobid, jobid,
socket, socket,
job,
rrOpenRoLimit = false, rrOpenRoLimit = false,
onRrOpenRoFinished, onRrOpenRoFinished,
rrValidationPending = false, rrValidationPending = false,
@@ -59,15 +60,26 @@ export default function RRCustomerSelector({
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
// Show dialog automatically when validation is pending // Show dialog automatically when validation is pending
// BUT: skip this for early RO flow (job already has dms_id)
useEffect(() => { useEffect(() => {
if (rrValidationPending) setOpen(true); if (rrValidationPending && !job?.dms_id) {
}, [rrValidationPending]); setOpen(true);
}
}, [rrValidationPending, job?.dms_id]);
// Listen for RR customer selection list // Listen for RR customer selection list
useEffect(() => { useEffect(() => {
if (!socket) return; if (!socket) return;
const handleRrSelectCustomer = (list) => { const handleRrSelectCustomer = (list) => {
const normalized = normalizeRrList(list); const normalized = normalizeRrList(list);
// If list is empty, it means early RO exists and customer selection should be skipped
// Don't open the modal in this case
if (normalized.length === 0) {
setRefreshing(false);
return;
}
setOpen(true); setOpen(true);
setCustomerList(normalized); setCustomerList(normalized);
const firstOwner = normalized.find((r) => r.vinOwner)?.custNo; const firstOwner = normalized.find((r) => r.vinOwner)?.custNo;
@@ -127,6 +139,10 @@ export default function RRCustomerSelector({
}); });
}; };
const handleClose = () => {
setOpen(false);
};
const refreshRrSearch = () => { const refreshRrSearch = () => {
setRefreshing(true); setRefreshing(true);
const to = setTimeout(() => setRefreshing(false), 12000); const to = setTimeout(() => setRefreshing(false), 12000);
@@ -141,8 +157,6 @@ export default function RRCustomerSelector({
socket.emit("rr-export-job", { jobId: jobid }); socket.emit("rr-export-job", { jobId: jobid });
}; };
if (!open) return null;
const columns = [ const columns = [
{ title: t("jobs.fields.dms.id"), dataIndex: "custNo", key: "custNo" }, { title: t("jobs.fields.dms.id"), dataIndex: "custNo", key: "custNo" },
{ {
@@ -169,8 +183,45 @@ export default function RRCustomerSelector({
return !rrOwnerSet.has(String(record.custNo)); return !rrOwnerSet.has(String(record.custNo));
}; };
// For early RO flow: show validation banner even when modal is closed
if (!open) {
if (rrValidationPending && job?.dms_id) {
return (
<div style={{ marginBottom: 16 }}>
<Alert
type="info"
showIcon
title="Complete Validation in Reynolds"
description={
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div>
We created the Repair Order. Please validate the totals and taxes in the DMS system. When done,
click <strong>Finished</strong> to finalize and mark this export as complete.
</div>
<div>
<Space>
<Button type="primary" onClick={onValidationFinished}>
Finished
</Button>
</Space>
</div>
</div>
}
/>
</div>
);
}
return null;
}
return ( return (
<Col span={24}> <Modal
open={open}
onCancel={handleClose}
footer={null}
width={800}
title={t("dms.selectCustomer")}
>
<Table <Table
title={() => ( title={() => (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}> <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
@@ -196,8 +247,8 @@ export default function RRCustomerSelector({
/> />
)} )}
{/* Validation step banner */} {/* Validation step banner - only show for NON-early RO flow (legacy) */}
{rrValidationPending && ( {rrValidationPending && !job?.dms_id && (
<Alert <Alert
type="info" type="info"
showIcon showIcon
@@ -262,6 +313,6 @@ export default function RRCustomerSelector({
getCheckboxProps: (record) => ({ disabled: rrDisableRow(record) }) getCheckboxProps: (record) => ({ disabled: rrDisableRow(record) })
}} }}
/> />
</Col> </Modal>
); );
} }

View File

@@ -69,7 +69,7 @@ export function DmsLogEvents({
return { return {
key: idx, key: idx,
color: logLevelColor(level), color: logLevelColor(level),
children: ( content: (
<Space orientation="vertical" size={4} style={{ display: "flex" }}> <Space orientation="vertical" size={4} style={{ display: "flex" }}>
{/* Row 1: summary + inline "Details" toggle */} {/* Row 1: summary + inline "Details" toggle */}
<Space wrap align="start"> <Space wrap align="start">
@@ -113,7 +113,7 @@ export function DmsLogEvents({
[logs, openSet, colorizeJson, isDarkMode, showDetails] [logs, openSet, colorizeJson, isDarkMode, showDetails]
); );
return <Timeline pending reverse items={items} />; return <Timeline reverse items={items} />;
} }
/** /**

View File

@@ -272,11 +272,19 @@ export default function CdkLikePostForm({ bodyshop, socket, job, logsRef, mode,
name={[field.name, "name"]} name={[field.name, "name"]}
rules={[{ required: true }]} rules={[{ required: true }]}
> >
<Select style={{ width: "100%" }} onSelect={(value) => handlePayerSelect(value, index)}> <Select
{bodyshop.cdk_configuration?.payers?.map((payer) => ( showSearch={{
<Select.Option key={payer.name}>{payer.name}</Select.Option> optionFilterProp: "label",
))} filterOption: (input, option) => option.label.toLowerCase().includes(input.toLowerCase())
</Select> }}
style={{ width: "100%" }}
onSelect={(value) => handlePayerSelect(value, index)}
options={bodyshop.cdk_configuration?.payers?.map((payer) => ({
key: payer.name,
value: payer.name,
label: payer.name
}))}
/>
</Form.Item> </Form.Item>
</Col> </Col>
@@ -404,7 +412,7 @@ export default function CdkLikePostForm({ bodyshop, socket, job, logsRef, mode,
<Typography.Title>=</Typography.Title> <Typography.Title>=</Typography.Title>
<Statistic <Statistic
title={t("jobs.labels.dms.notallocated")} title={t("jobs.labels.dms.notallocated")}
styles={{ value: { color: discrep.getAmount() === 0 ? "green" : "red" } }} styles={{ content: { color: discrep.getAmount() === 0 ? "green" : "red" } }}
value={discrep.toFormat()} value={discrep.toFormat()}
/> />
<Button disabled={disablePost} htmlType="submit"> <Button disabled={disablePost} htmlType="submit">

View File

@@ -208,8 +208,18 @@ export default function RRPostForm({
}); });
}; };
// Check if early RO was created (job has all early RO fields)
const hasEarlyRO = !!(job?.dms_id && job?.dms_customer_id && job?.dms_advisor_id);
return ( return (
<Card title={t("jobs.labels.dms.postingform")}> <Card title={t("jobs.labels.dms.postingform")}>
{hasEarlyRO && (
<Typography.Paragraph type="success" strong style={{ marginBottom: 16 }}>
{t("jobs.labels.dms.earlyro.created")} {job.dms_id}
<br />
<Typography.Text type="secondary">{t("jobs.labels.dms.earlyro.willupdate")}</Typography.Text>
</Typography.Paragraph>
)}
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
@@ -218,96 +228,96 @@ export default function RRPostForm({
initialValues={initialValues} initialValues={initialValues}
> >
<Row gutter={[16, 12]} align="bottom"> <Row gutter={[16, 12]} align="bottom">
{/* Advisor + inline Refresh */} {/* Advisor + inline Refresh - Only show if no early RO */}
<Col xs={24} sm={24} md={12} lg={8}> {!hasEarlyRO && (
<Form.Item label={t("jobs.fields.dms.advisor")} required> <Col xs={24} sm={24} md={12} lg={8}>
<Space.Compact block> <Form.Item label={t("jobs.fields.dms.advisor")} required>
<Form.Item <Space.Compact block>
name="advisorNo" <Form.Item
noStyle name="advisorNo"
rules={[{ required: true, message: t("general.validation.required") }]} noStyle
> rules={[{ required: true, message: t("general.validation.required") }]}
<Select >
style={{ flex: 1 }} <Select
loading={advLoading} style={{ flex: 1 }}
allowClear loading={advLoading}
placeholder={t("general.actions.select", "Select...")} allowClear
popupMatchSelectWidth placeholder={t("general.actions.select", "Select...")}
options={advisors popupMatchSelectWidth
.map((a) => { options={advisors
const value = getAdvisorNumber(a); .map((a) => {
if (value == null) return null; const value = getAdvisorNumber(a);
return { value: String(value), label: getAdvisorLabel(a) || String(value) }; if (value == null) return null;
}) return { value: String(value), label: getAdvisorLabel(a) || String(value) };
.filter(Boolean)} })
notFoundContent={advLoading ? t("general.labels.loading") : t("general.labels.none")} .filter(Boolean)}
/> notFoundContent={advLoading ? t("general.labels.loading") : t("general.labels.none")}
</Form.Item> />
<Tooltip title={t("general.actions.refresh")}> </Form.Item>
<Button <Tooltip title={t("general.actions.refresh")}>
aria-label={t("general.actions.refresh")}
icon={<ReloadOutlined />}
onClick={() => fetchRrAdvisors(true)}
loading={advLoading}
/>
</Tooltip>
</Space.Compact>
</Form.Item>
</Col>
{/* RR OpCode (prefix / base / suffix) */}
<Col xs={24} sm={12} md={12} lg={8}>
<Form.Item
required
label={
<Space size="small" align="center">
{t("jobs.fields.dms.rr_opcode", "RR OpCode")}
{isCustomOpCode && (
<Button <Button
type="link" aria-label={t("general.actions.refresh")}
size="small" icon={<ReloadOutlined />}
icon={<RollbackOutlined />} onClick={() => fetchRrAdvisors(true)}
onClick={handleResetOpCode} loading={advLoading}
style={{ padding: 0 }} />
> </Tooltip>
{t("jobs.fields.dms.rr_opcode_reset", "Reset")} </Space.Compact>
</Button> </Form.Item>
)} </Col>
</Space> )}
}
> {/* RR OpCode (prefix / base / suffix) - Only show if no early RO */}
<Space.Compact block> {!hasEarlyRO && (
<Form.Item name="opPrefix" noStyle> <Col xs={24} sm={12} md={12} lg={8}>
<Input <Form.Item
allowClear required
maxLength={4} label={
style={{ width: "30%" }} <Space size="small" align="center">
placeholder={t("jobs.fields.dms.rr_opcode_prefix", "Prefix")} {t("jobs.fields.dms.rr_opcode", "RR OpCode")}
/> {isCustomOpCode && (
</Form.Item> <Button
<Form.Item type="link"
name="opBase" size="small"
noStyle icon={<RollbackOutlined />}
rules={[{ required: true, message: t("general.validation.required") }]} onClick={handleResetOpCode}
> style={{ padding: 0 }}
<Input >
allowClear {t("jobs.fields.dms.rr_opcode_reset", "Reset")}
maxLength={10} </Button>
style={{ width: "40%" }} )}
placeholder={t("jobs.fields.dms.rr_opcode_base", "Base")} </Space>
/> }
</Form.Item> >
<Form.Item name="opSuffix" noStyle> <Space.Compact block>
<Input <Form.Item name="opPrefix" noStyle>
allowClear <Input
maxLength={4} allowClear
style={{ width: "30%" }} maxLength={4}
placeholder={t("jobs.fields.dms.rr_opcode_suffix", "Suffix")} style={{ width: "30%" }}
/> placeholder={t("jobs.fields.dms.rr_opcode_prefix", "Prefix")}
</Form.Item> />
</Space.Compact> </Form.Item>
</Form.Item> <Form.Item name="opBase" noStyle rules={[{ required: true }]}>
</Col> <Input
allowClear
maxLength={10}
style={{ width: "40%" }}
placeholder={t("jobs.fields.dms.rr_opcode_base", "Base")}
/>
</Form.Item>
<Form.Item name="opSuffix" noStyle>
<Input
allowClear
maxLength={4}
style={{ width: "30%" }}
placeholder={t("jobs.fields.dms.rr_opcode_suffix", "Suffix")}
/>
</Form.Item>
</Space.Compact>
</Form.Item>
</Col>
)}
<Col xs={12} sm={8} md={6} lg={4}> <Col xs={12} sm={8} md={6} lg={4}>
<Form.Item name="kmin" label={t("jobs.fields.kmin")} initialValue={job?.kmin} rules={[{ required: true }]}> <Form.Item name="kmin" label={t("jobs.fields.kmin")} initialValue={job?.kmin} rules={[{ required: true }]}>
@@ -355,13 +365,14 @@ export default function RRPostForm({
{/* Validation */} {/* Validation */}
<Form.Item shouldUpdate> <Form.Item shouldUpdate>
{() => { {() => {
const advisorOk = !!form.getFieldValue("advisorNo"); // When early RO exists, advisor is already set, so we don't need to validate it
const advisorOk = hasEarlyRO ? true : !!form.getFieldValue("advisorNo");
return ( return (
<Space size="large" wrap align="center"> <Space size="large" wrap align="center">
<Statistic title={t("jobs.labels.subtotal")} value={totals.totalSale.toFormat()} /> <Statistic title={t("jobs.labels.subtotal")} value={totals.totalSale.toFormat()} />
<Typography.Title>=</Typography.Title> <Typography.Title>=</Typography.Title>
<Button disabled={!advisorOk} htmlType="submit"> <Button disabled={!advisorOk} htmlType="submit" type={hasEarlyRO ? "default" : "primary"}>
{t("jobs.actions.dms.post")} {hasEarlyRO ? t("jobs.actions.dms.update_ro") : t("jobs.actions.dms.post")}
</Button> </Button>
</Space> </Space>
); );

View File

@@ -0,0 +1,367 @@
import { ReloadOutlined } from "@ant-design/icons";
import { Alert, Button, Form, Input, InputNumber, Modal, Radio, Select, Space, Table, Typography } from "antd";
import { useEffect, useMemo, useState } from "react";
// Simple customer selector table
function CustomerSelectorTable({ customers, onSelect, isSubmitting }) {
const [selectedCustNo, setSelectedCustNo] = useState(null);
const columns = [
{
title: "Select",
key: "select",
width: 80,
render: (_, record) => (
<Radio checked={selectedCustNo === record.custNo} onChange={() => setSelectedCustNo(record.custNo)} />
)
},
{ title: "Customer ID", dataIndex: "custNo", key: "custNo" },
{ title: "Name", dataIndex: "name", key: "name" },
{
title: "VIN Owner",
key: "vinOwner",
render: (_, record) => (record.vinOwner || record.isVehicleOwner ? "Yes" : "No")
}
];
return (
<div>
<Table columns={columns} dataSource={customers} rowKey="custNo" pagination={false} size="small" />
<div style={{ marginTop: 16, display: "flex", gap: 8 }}>
<Button
type="primary"
onClick={() => onSelect(selectedCustNo, false)}
disabled={!selectedCustNo || isSubmitting}
loading={isSubmitting}
>
Use Selected Customer
</Button>
<Button onClick={() => onSelect(null, true)} disabled={isSubmitting} loading={isSubmitting}>
Create New Customer
</Button>
</div>
</div>
);
}
/**
* RR Early RO Creation Form
* Used from convert button or admin page to create minimal RO before full export
* @param bodyshop
* @param socket
* @param job
* @param onSuccess - callback when RO is created successfully
* @param onCancel - callback to close modal
* @param showCancelButton - whether to show cancel button
* @returns {JSX.Element}
* @constructor
*/
export default function RREarlyROForm({ bodyshop, socket, job, onSuccess, onCancel, showCancelButton = true }) {
const [form] = Form.useForm();
// Advisors
const [advisors, setAdvisors] = useState([]);
const [advLoading, setAdvLoading] = useState(false);
// Customer selection
const [customerCandidates, setCustomerCandidates] = useState([]);
const [showCustomerSelector, setShowCustomerSelector] = useState(false);
// Loading and success states
const [isSubmitting, setIsSubmitting] = useState(false);
const [earlyRoCreated, setEarlyRoCreated] = useState(!!job?.dms_id);
const [createdRoNumber, setCreatedRoNumber] = useState(job?.dms_id || null);
// Derive default OpCode parts from bodyshop config (matching dms.container.jsx logic)
const initialValues = useMemo(() => {
const cfg = bodyshop?.rr_configuration || {};
const defaults =
cfg.opCodeDefault ||
cfg.op_code_default ||
cfg.op_codes?.default ||
cfg.defaults?.opCode ||
cfg.defaults ||
cfg.default ||
{};
const prefix = defaults.prefix ?? defaults.opCodePrefix ?? "";
const base = defaults.base ?? defaults.opCodeBase ?? "";
const suffix = defaults.suffix ?? defaults.opCodeSuffix ?? "";
return {
kmin: job?.kmin || 0,
opPrefix: prefix,
opBase: base,
opSuffix: suffix
};
}, [bodyshop, job]);
const getAdvisorNumber = (a) => a?.advisorId;
const getAdvisorLabel = (a) => `${a?.firstName || ""} ${a?.lastName || ""}`.trim();
const fetchRrAdvisors = (refresh = false) => {
if (!socket) return;
setAdvLoading(true);
const onResult = (payload) => {
try {
const list = payload?.result ?? payload ?? [];
setAdvisors(Array.isArray(list) ? list : []);
} finally {
setAdvLoading(false);
socket.off("rr-get-advisors:result", onResult);
}
};
socket.once("rr-get-advisors:result", onResult);
socket.emit("rr-get-advisors", { departmentType: "B", refresh }, (ack) => {
if (ack?.ok) {
const list = ack.result ?? [];
setAdvisors(Array.isArray(list) ? list : []);
} else if (ack) {
console.error("Error fetching RR Advisors:", ack.error);
}
setAdvLoading(false);
socket.off("rr-get-advisors:result", onResult);
});
};
useEffect(() => {
fetchRrAdvisors(false);
}, [bodyshop?.id, socket]);
const handleStartEarlyRO = async (values) => {
if (!socket) {
console.error("Socket not available");
return;
}
setIsSubmitting(true);
const txEnvelope = {
advisorNo: values.advisorNo,
story: values.story || "",
kmin: values.kmin || job?.kmin || 0,
opPrefix: values.opPrefix || "",
opBase: values.opBase || "",
opSuffix: values.opSuffix || ""
};
// Emit the early RO creation request
socket.emit("rr-create-early-ro", {
jobId: job.id,
txEnvelope
});
// Wait for customer selection
const customerListener = (candidates) => {
console.log("Received rr-select-customer event with candidates:", candidates);
setCustomerCandidates(candidates || []);
setShowCustomerSelector(true);
setIsSubmitting(false);
socket.off("rr-select-customer", customerListener);
};
socket.once("rr-select-customer", customerListener);
// Handle failures
const failureListener = (payload) => {
if (payload?.jobId === job.id) {
console.error("Early RO creation failed:", payload.error);
alert(`Failed to create early RO: ${payload.error}`);
setIsSubmitting(false);
setShowCustomerSelector(false);
socket.off("export-failed", failureListener);
socket.off("rr-select-customer", customerListener);
}
};
socket.once("export-failed", failureListener);
};
const handleCustomerSelected = (custNo, createNew = false) => {
if (!socket) return;
console.log("handleCustomerSelected called:", { custNo, createNew, custNoType: typeof custNo });
setIsSubmitting(true);
setShowCustomerSelector(false);
const payload = {
jobId: job.id,
custNo: createNew ? null : custNo,
create: createNew
};
console.log("Emitting rr-early-customer-selected:", payload);
// Emit customer selection
socket.emit("rr-early-customer-selected", payload, (ack) => {
console.log("Received ack from rr-early-customer-selected:", ack);
setIsSubmitting(false);
if (ack?.ok) {
const roNumber = ack.dmsRoNo || ack.outsdRoNo;
setEarlyRoCreated(true);
setCreatedRoNumber(roNumber);
onSuccess?.({ roNumber, ...ack });
} else {
alert(`Failed to create early RO: ${ack?.error || "Unknown error"}`);
}
});
// Also listen for socket events
const successListener = (payload) => {
if (payload?.jobId === job.id) {
const roNumber = payload.dmsRoNo || payload.outsdRoNo;
console.log("Early RO created:", roNumber);
socket.off("rr-early-ro-created", successListener);
socket.off("export-failed", failureListener);
}
};
const failureListener = (payload) => {
if (payload?.jobId === job.id) {
console.error("Early RO creation failed:", payload.error);
setIsSubmitting(false);
setEarlyRoCreated(false);
socket.off("rr-early-ro-created", successListener);
socket.off("export-failed", failureListener);
}
};
socket.once("rr-early-ro-created", successListener);
socket.once("export-failed", failureListener);
};
// If early RO already created, show success message
if (earlyRoCreated) {
return (
<Alert
title="Early Reynolds RO Created"
description={`RO Number: ${createdRoNumber || "N/A"} - You can now convert the job.`}
type="success"
showIcon
style={{ marginBottom: 16 }}
/>
);
}
// If showing customer selector, render modal
if (showCustomerSelector) {
return (
<>
<Typography.Title level={5}>Create Early Reynolds RO</Typography.Title>
<Typography.Paragraph type="secondary">Waiting for customer selection...</Typography.Paragraph>
<Modal
title="Select Customer for Early RO"
open={true}
width={800}
footer={null}
onCancel={() => {
setShowCustomerSelector(false);
setIsSubmitting(false);
}}
>
<CustomerSelectorTable
customers={customerCandidates}
onSelect={handleCustomerSelected}
isSubmitting={isSubmitting}
/>
</Modal>
</>
);
}
// Handle manual submit (since we can't nest forms)
const handleManualSubmit = async () => {
try {
const values = await form.validateFields();
handleStartEarlyRO(values);
} catch (error) {
console.error("Validation failed:", error);
}
};
// Show the form
return (
<div style={{ marginTop: 16 }}>
<Typography.Title level={5}>Create Early Reynolds RO</Typography.Title>
<Typography.Paragraph type="secondary" style={{ fontSize: "12px" }}>
Complete this section to create a minimal RO in Reynolds before converting the job.
</Typography.Paragraph>
<Form form={form} layout="vertical" component={false} initialValues={initialValues}>
<Form.Item name="advisorNo" label="Advisor" rules={[{ required: true, message: "Please select an advisor" }]}>
<Select
showSearch={{
optionFilterProp: "children",
filterOption: (input, option) => (option?.children?.toLowerCase() ?? "").includes(input.toLowerCase())
}}
loading={advLoading}
placeholder="Select advisor..."
popupRender={(menu) => (
<>
{menu}
<Button
type="link"
icon={<ReloadOutlined />}
onClick={() => fetchRrAdvisors(true)}
style={{ width: "100%", textAlign: "left" }}
>
Refresh Advisors
</Button>
</>
)}
>
{advisors.map((adv) => (
<Select.Option key={getAdvisorNumber(adv)} value={getAdvisorNumber(adv)}>
{getAdvisorLabel(adv)}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="kmin"
label="Mileage In"
rules={[
{ required: true, message: "Please enter initial mileage" },
{ type: "number", min: 1, message: "Mileage must be greater than 0" }
]}
>
<InputNumber min={1} style={{ width: "100%" }} />
</Form.Item>
{/* RR OpCode (prefix / base / suffix) */}
<Form.Item required label="RR OpCode">
<Space.Compact block>
<Form.Item name="opPrefix" noStyle>
<Input allowClear maxLength={4} style={{ width: "30%" }} placeholder="Prefix" />
</Form.Item>
<Form.Item name="opBase" noStyle rules={[{ required: true, message: "Base Required" }]}>
<Input allowClear maxLength={10} style={{ width: "40%" }} placeholder="Base" />
</Form.Item>
<Form.Item name="opSuffix" noStyle>
<Input allowClear maxLength={4} style={{ width: "30%" }} placeholder="Suffix" />
</Form.Item>
</Space.Compact>
</Form.Item>
<Form.Item name="story" label="Comments / Story (Optional)">
<Input.TextArea rows={2} maxLength={240} showCount placeholder="Enter comments or story..." />
</Form.Item>
<div style={{ marginTop: 16 }}>
<Space>
<Button type="primary" onClick={handleManualSubmit} loading={isSubmitting} disabled={advLoading}>
Create Early RO
</Button>
{showCancelButton && <Button onClick={onCancel}>Cancel</Button>}
</Space>
</div>
</Form>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import { Modal } from "antd";
import RREarlyROForm from "./rr-early-ro-form";
/**
* Modal wrapper for RR Early RO Creation Form
* @param open - boolean to control modal visibility
* @param onClose - callback when modal is closed
* @param onSuccess - callback when RO is created successfully
* @param bodyshop - bodyshop object
* @param socket - socket.io connection
* @param job - job object
* @returns {JSX.Element}
* @constructor
*/
export default function RREarlyROModal({ open, onClose, onSuccess, bodyshop, socket, job }) {
const handleSuccess = (result) => {
onSuccess?.(result);
onClose?.();
};
return (
<Modal
open={open}
onCancel={onClose}
footer={null}
width={700}
destroyOnHidden
title="Create Reynolds Repair Order"
>
<RREarlyROForm bodyshop={bodyshop} socket={socket} job={job} onSuccess={handleSuccess} onCancel={onClose} />
</Modal>
);
}

View File

@@ -86,11 +86,13 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b
} }
]} ]}
> >
<Select> <Select
<Select.Option key={currentUser.email}>{currentUser.email}</Select.Option> options={[
<Select.Option key={bodyshop.email}>{bodyshop.email}</Select.Option> { key: currentUser.email, value: currentUser.email, label: currentUser.email },
{bodyshop.md_from_emails && bodyshop.md_from_emails.map((e) => <Select.Option key={e}>{e}</Select.Option>)} { key: bodyshop.email, value: bodyshop.email, label: bodyshop.email },
</Select> ...(bodyshop.md_from_emails ? bodyshop.md_from_emails.map((e) => ({ key: e, value: e, label: e })) : [])
]}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={ label={

View File

@@ -1,7 +1,6 @@
import { Select, Space, Tag } from "antd"; import { Select, Space, Tag } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const { Option } = Select;
//To be used as a form element only. //To be used as a form element only.
const EmployeeSearchSelectEmail = ({ options, ...props }) => { const EmployeeSearchSelectEmail = ({ options, ...props }) => {
@@ -12,26 +11,24 @@ const EmployeeSearchSelectEmail = ({ options, ...props }) => {
showSearch={{ showSearch={{
optionFilterProp: "search" optionFilterProp: "search"
}} }}
// value={option}
style={{ style={{
width: 400 width: 400
}} }}
options={options?.map((o) => ({
key: o.id,
value: o.user_email,
search: `${o.employee_number} ${o.first_name} ${o.last_name}`,
label: (
<Space>
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
<Tag color="green">
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
</Tag>
</Space>
)
}))}
{...props} {...props}
> />
{options
? options.map((o) => (
<Option key={o.id} value={o.user_email} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
<Space>
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
<Tag color="green">
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
</Tag>
</Space>
</Option>
))
: null}
</Select>
); );
}; };
export default EmployeeSearchSelectEmail; export default EmployeeSearchSelectEmail;

View File

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

View File

@@ -14,8 +14,11 @@ export default function GlobalSearch() {
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY); const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
const navigate = useNavigate(); const navigate = useNavigate();
const executeSearch = (v) => { const executeSearch = (variables) => {
if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(v); if (variables?.search !== "" && variables?.search?.length >= 3)
callSearch({
variables
});
}; };
const debouncedExecuteSearch = _.debounce(executeSearch, 750); const debouncedExecuteSearch = _.debounce(executeSearch, 750);
@@ -157,7 +160,9 @@ export default function GlobalSearch() {
return ( return (
<AutoComplete <AutoComplete
options={options} options={options}
onSearch={handleSearch} showSearch={{
onSearch: handleSearch
}}
defaultActiveFirstOption defaultActiveFirstOption
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key !== "Enter") return; if (e.key !== "Enter") return;

View File

@@ -67,16 +67,19 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
); );
}; };
const handleInsSelect = (value, option) => { const handleInsSelect = (value) => {
form.setFieldsValue({ const selectedVendor = bodyshop.md_ins_cos.find(s => s.name === value);
addr1: option.obj.name, if (selectedVendor) {
addr2: option.obj.street1, form.setFieldsValue({
addr3: option.obj.street2, addr1: selectedVendor.name,
city: option.obj.city, addr2: selectedVendor.street1,
state: option.obj.state, addr3: selectedVendor.street2,
zip: option.obj.zip, city: selectedVendor.city,
vendorid: null state: selectedVendor.state,
}); zip: selectedVendor.zip,
vendorid: null
});
}
}; };
const handleVendorSelect = (vendorid) => { const handleVendorSelect = (vendorid) => {
@@ -97,19 +100,19 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
return ( return (
<> <>
<Button onClick={showModal}>{t("printcenter.jobs.3rdpartypayer")}</Button> <Button onClick={showModal}>{t("printcenter.jobs.3rdpartypayer")}</Button>
<Modal open={isModalVisible} onOk={handleOk} onCancel={handleCancel}> <Modal open={isModalVisible} onOk={handleOk} onCancel={handleCancel} getContainer={() => document.body}>
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}> <Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
<Form.Item label={t("bills.fields.vendor")} name="vendorid"> <Form.Item label={t("bills.fields.vendor")} name="vendorid">
<VendorSearchSelect options={VendorAutoCompleteData?.vendors} onSelect={handleVendorSelect} /> <VendorSearchSelect options={VendorAutoCompleteData?.vendors} onSelect={handleVendorSelect} />
</Form.Item> </Form.Item>
<Form.Item label={t("bodyshop.fields.md_ins_co.name")} name="ins_co_id"> <Form.Item label={t("bodyshop.fields.md_ins_co.name")} name="ins_co_id">
<Select onSelect={handleInsSelect}> <Select
{bodyshop.md_ins_cos.map((s) => ( onSelect={handleInsSelect}
<Select.Option key={s.name} obj={s} value={s.name}> options={bodyshop.md_ins_cos.map((s) => ({
{s.name} value: s.name,
</Select.Option> label: s.name
))} }))}
</Select> />
</Form.Item> </Form.Item>
<LayoutFormRow grow> <LayoutFormRow grow>
<Form.Item label={t("printcenter.jobs.3rdpartyfields.addr1")} name="addr1"> <Form.Item label={t("printcenter.jobs.3rdpartyfields.addr1")} name="addr1">

View File

@@ -88,17 +88,15 @@ export function JoblineBulkAssign({ setSelectedLines, selectedLines, insertAudit
> >
<Select <Select
showSearch={{ showSearch={{
optionFilterProp: "children", optionFilterProp: "label",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}} }}
style={{ width: 200 }} style={{ width: 200 }}
> options={bodyshop.employee_teams.map((team) => ({
{bodyshop.employee_teams.map((team) => ( value: team.id,
<Select.Option value={team.id} key={team.id} name={team.name}> label: team.name
{team.name} }))}
</Select.Option> />
))}
</Select>
</Form.Item> </Form.Item>
<Space wrap> <Space wrap>

View File

@@ -122,22 +122,26 @@ export function JobLineConvertToLabor({
} }
]} ]}
> >
<Select allowClear showSearch={{ optionFilterProp: "children" }}> <Select
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option> allowClear
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option> showSearch
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option> options={[
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option> { value: "LAA", label: t("joblines.fields.lbr_types.LAA") },
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option> { value: "LAB", label: t("joblines.fields.lbr_types.LAB") },
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option> { value: "LAD", label: t("joblines.fields.lbr_types.LAD") },
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option> { value: "LAE", label: t("joblines.fields.lbr_types.LAE") },
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option> { value: "LAF", label: t("joblines.fields.lbr_types.LAF") },
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option> { value: "LAG", label: t("joblines.fields.lbr_types.LAG") },
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option> { value: "LAM", label: t("joblines.fields.lbr_types.LAM") },
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option> { value: "LAR", label: t("joblines.fields.lbr_types.LAR") },
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option> { value: "LAS", label: t("joblines.fields.lbr_types.LAS") },
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option> { value: "LAU", label: t("joblines.fields.lbr_types.LAU") },
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option> { value: "LA1", label: t("joblines.fields.lbr_types.LA1") },
</Select> { value: "LA2", label: t("joblines.fields.lbr_types.LA2") },
{ value: "LA3", label: t("joblines.fields.lbr_types.LA3") },
{ value: "LA4", label: t("joblines.fields.lbr_types.LA4") }
]}
/>
</Form.Item> </Form.Item>
<Form.Item shouldUpdate> <Form.Item shouldUpdate>

View File

@@ -115,19 +115,18 @@ export function JobLineDispatchButton({
> >
<Select <Select
showSearch={{ showSearch={{
optionFilterProp: "children", optionFilterProp: "label",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}} }}
style={{ width: 200 }} style={{ width: 200 }}
> options={bodyshop.employees
{bodyshop.employees
.filter((emp) => emp.active) .filter((emp) => emp.active)
.map((emp) => ( .map((emp) => ({
<Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}> value: emp.id,
{`${emp.first_name} ${emp.last_name}`} key: emp.id,
</Select.Option> label: `${emp.first_name} ${emp.last_name}`
))} }))}
</Select> />
</Form.Item> </Form.Item>
<Space wrap> <Space wrap>

View File

@@ -64,13 +64,12 @@ export function JobLineStatusPopup({ bodyshop, jobline, disabled }) {
onSelect={handleChange} onSelect={handleChange}
onBlur={handleSave} onBlur={handleSave}
onClear={() => handleChange(null)} onClear={() => handleChange(null)}
> options={Object.values(bodyshop.md_order_statuses).map((s, idx) => ({
{Object.values(bodyshop.md_order_statuses).map((s, idx) => ( key: idx,
<Select.Option key={idx} value={s}> value: s,
{s} label: s
</Select.Option> }))}
))} />
</Select>
</LoadingSpinner> </LoadingSpinner>
</div> </div>
); );

View File

@@ -75,13 +75,12 @@ export function JoblineTeamAssignment({ bodyshop, jobline, disabled, jobId, inse
onSelect={handleChange} onSelect={handleChange}
onBlur={handleSave} onBlur={handleSave}
onClear={() => handleChange(null)} onClear={() => handleChange(null)}
> options={Object.values(bodyshop.employee_teams).map((s) => ({
{Object.values(bodyshop.employee_teams).map((s, idx) => ( key: s.id,
<Select.Option key={idx} value={s.id}> value: s.id,
{s.name} label: s.name
</Select.Option> }))}
))} />
</Select>
</LoadingSpinner> </LoadingSpinner>
</div> </div>
); );

View File

@@ -67,22 +67,22 @@ export function JobLinesUpsertModalComponent({ bodyshop, open, jobLine, handleCa
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow> <LayoutFormRow grow>
<Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty"> <Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty">
<Select allowClear> <Select allowClear options={[
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option> { value: "LAA", label: t("joblines.fields.lbr_types.LAA") },
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option> { value: "LAB", label: t("joblines.fields.lbr_types.LAB") },
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option> { value: "LAD", label: t("joblines.fields.lbr_types.LAD") },
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option> { value: "LAE", label: t("joblines.fields.lbr_types.LAE") },
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option> { value: "LAF", label: t("joblines.fields.lbr_types.LAF") },
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option> { value: "LAG", label: t("joblines.fields.lbr_types.LAG") },
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option> { value: "LAM", label: t("joblines.fields.lbr_types.LAM") },
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option> { value: "LAR", label: t("joblines.fields.lbr_types.LAR") },
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option> { value: "LAS", label: t("joblines.fields.lbr_types.LAS") },
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option> { value: "LAU", label: t("joblines.fields.lbr_types.LAU") },
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option> { value: "LA1", label: t("joblines.fields.lbr_types.LA1") },
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option> { value: "LA2", label: t("joblines.fields.lbr_types.LA2") },
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option> { value: "LA3", label: t("joblines.fields.lbr_types.LA3") },
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option> { value: "LA4", label: t("joblines.fields.lbr_types.LA4") }
</Select> ]} />
</Form.Item> </Form.Item>
<Form.Item label={t("joblines.fields.op_code_desc")} name="op_code_desc"> <Form.Item label={t("joblines.fields.op_code_desc")} name="op_code_desc">
<Input /> <Input />
@@ -128,17 +128,17 @@ export function JobLinesUpsertModalComponent({ bodyshop, open, jobLine, handleCa
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow> <LayoutFormRow>
<Form.Item label={t("joblines.fields.part_type")} name="part_type"> <Form.Item label={t("joblines.fields.part_type")} name="part_type">
<Select allowClear> <Select allowClear options={[
<Select.Option value="PAA">{t("joblines.fields.part_types.PAA")}</Select.Option> { value: "PAA", label: t("joblines.fields.part_types.PAA") },
<Select.Option value="PAC">{t("joblines.fields.part_types.PAC")}</Select.Option> { value: "PAC", label: t("joblines.fields.part_types.PAC") },
<Select.Option value="PAE">{t("joblines.fields.part_types.PAE")}</Select.Option> { value: "PAE", label: t("joblines.fields.part_types.PAE") },
<Select.Option value="PAL">{t("joblines.fields.part_types.PAL")}</Select.Option> { value: "PAL", label: t("joblines.fields.part_types.PAL") },
<Select.Option value="PAM">{t("joblines.fields.part_types.PAM")}</Select.Option> { value: "PAM", label: t("joblines.fields.part_types.PAM") },
<Select.Option value="PAN">{t("joblines.fields.part_types.PAN")}</Select.Option> { value: "PAN", label: t("joblines.fields.part_types.PAN") },
<Select.Option value="PAO">{t("joblines.fields.part_types.PAO")}</Select.Option> { value: "PAO", label: t("joblines.fields.part_types.PAO") },
<Select.Option value="PAR">{t("joblines.fields.part_types.PAR")}</Select.Option> { value: "PAR", label: t("joblines.fields.part_types.PAR") },
<Select.Option value="PAS">{t("joblines.fields.part_types.PAS")}</Select.Option> { value: "PAS", label: t("joblines.fields.part_types.PAS") }
</Select> ]} />
</Form.Item> </Form.Item>
<Form.Item label={t("joblines.fields.oem_partno")} name="oem_partno"> <Form.Item label={t("joblines.fields.oem_partno")} name="oem_partno">
<Input /> <Input />

View File

@@ -1,29 +1,65 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { Tag, Tooltip } from "antd"; import { Tooltip } from "antd";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = () => ({ const mapDispatchToProps = () => ({});
//setUserLanguage: language => dispatch(setUserLanguage(language))
}); const colorMap = {
gray: { bg: "#fafafa", border: "#d9d9d9", text: "#000000" },
gold: { bg: "#fffbe6", border: "#ffe58f", text: "#d48806" },
red: { bg: "#fff1f0", border: "#ffccc7", text: "#cf1322" },
blue: { bg: "#e6f7ff", border: "#91d5ff", text: "#0958d9" },
green: { bg: "#f6ffed", border: "#b7eb8f", text: "#389e0d" },
orange: { bg: "#fff7e6", border: "#ffd591", text: "#d46b08" }
};
function CompactTag({ color = "gray", children, tooltip = "" }) {
const colors = colorMap[color] || colorMap.gray;
return (
<span
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
padding: "0 2px",
fontSize: "12px",
lineHeight: "20px",
backgroundColor: colors.bg,
border: `1px solid ${colors.border}`,
borderRadius: "2px",
color: colors.text,
minWidth: "24px",
textAlign: "center"
}}
>
<Tooltip title={tooltip}>{children}</Tooltip>
</span>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount); export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount);
export function JobPartsQueueCount({ bodyshop, parts }) { export function JobPartsQueueCount({ bodyshop, parts }) {
const { t } = useTranslation();
const partsStatus = useMemo(() => { const partsStatus = useMemo(() => {
if (!parts) return null; if (!parts) return null;
const statusKeys = ["default_bo", "default_ordered", "default_received", "default_returned"]; const statusKeys = ["default_bo", "default_ordered", "default_received", "default_returned"];
return parts.reduce( return parts.reduce(
(acc, val) => { (acc, val) => {
if (val.part_type === "PAS" || val.part_type === "PASL") return acc; if (val.part_type === "PAS" || val.part_type === "PASL") return acc;
acc.total = acc.total + val.count;
acc[val.status] = acc[val.status] + val.count; acc.total += val.count;
// NOTE: if val.status is null, object key becomes "null"
acc[val.status] = (acc[val.status] ?? 0) + val.count;
return acc; return acc;
}, },
{ {
@@ -34,45 +70,38 @@ export function JobPartsQueueCount({ bodyshop, parts }) {
); );
}, [bodyshop, parts]); }, [bodyshop, parts]);
if (!parts) return null; if (!parts || !partsStatus) return null;
return ( return (
<div <div
style={{ style={{
display: "grid", display: "inline-flex", // ✅ shrink-wraps, fixes the “extra box” to the right
gridTemplateColumns: "repeat(auto-fit, minmax(40px, 1fr))", gap: 2,
gap: "8px", alignItems: "center",
width: "100%", whiteSpace: "nowrap"
justifyItems: "start"
}} }}
> >
<Tooltip title="Total"> <CompactTag tooltip="Total" color="gray">
<Tag style={{ minWidth: "40px", textAlign: "center" }}>{partsStatus.total}</Tag> {partsStatus.total}
</Tooltip> </CompactTag>
<Tooltip title={t("dashboard.errors.status_normal")}>
<Tag color="gold" style={{ minWidth: "40px", textAlign: "center" }}> <CompactTag tooltip="No Status" color="gold">
{partsStatus["null"]} {partsStatus["null"]}
</Tag> </CompactTag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_bo}> <CompactTag tooltip={bodyshop.md_order_statuses.default_bo} color="red">
<Tag color="red" style={{ minWidth: "40px", textAlign: "center" }}> {partsStatus[bodyshop.md_order_statuses.default_bo]}
{partsStatus[bodyshop.md_order_statuses.default_bo]} </CompactTag>
</Tag>
</Tooltip> <CompactTag tooltip={bodyshop.md_order_statuses.default_ordered} color="blue">
<Tooltip title={bodyshop.md_order_statuses.default_ordered}> {partsStatus[bodyshop.md_order_statuses.default_ordered]}
<Tag color="blue" style={{ minWidth: "40px", textAlign: "center" }}> </CompactTag>
{partsStatus[bodyshop.md_order_statuses.default_ordered]} <CompactTag tooltip={bodyshop.md_order_statuses.default_received} color="green">
</Tag> {partsStatus[bodyshop.md_order_statuses.default_received]}
</Tooltip> </CompactTag>
<Tooltip title={bodyshop.md_order_statuses.default_received}> <CompactTag tooltip={bodyshop.md_order_statuses.default_returned} color="orange">
<Tag color="green" style={{ minWidth: "40px", textAlign: "center" }}> {partsStatus[bodyshop.md_order_statuses.default_returned]}
{partsStatus[bodyshop.md_order_statuses.default_received]} </CompactTag>
</Tag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
<Tag color="orange" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus[bodyshop.md_order_statuses.default_returned]}
</Tag>
</Tooltip>
</div> </div>
); );
} }

View File

@@ -18,10 +18,17 @@ const mapStateToProps = createStructuredSelector({
* @param parts * @param parts
* @param displayMode * @param displayMode
* @param popoverPlacement * @param popoverPlacement
* @param countsOnly
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
export function JobPartsReceived({ bodyshop, parts, displayMode = "full", popoverPlacement = "top" }) { export function JobPartsReceived({
bodyshop,
parts,
displayMode = "full",
popoverPlacement = "top",
countsOnly = false
}) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -61,6 +68,8 @@ export function JobPartsReceived({ bodyshop, parts, displayMode = "full", popove
[canOpen] [canOpen]
); );
if (countsOnly) return <JobPartsQueueCount parts={parts} />;
const displayText = const displayText =
displayMode === "compact" ? summary.percentLabel : `${summary.percentLabel} (${summary.received}/${summary.total})`; displayMode === "compact" ? summary.percentLabel : `${summary.percentLabel} (${summary.received}/${summary.total})`;
@@ -74,7 +83,7 @@ export function JobPartsReceived({ bodyshop, parts, displayMode = "full", popove
trigger={["click"]} trigger={["click"]}
placement={popoverPlacement} placement={popoverPlacement}
content={ content={
<div onClick={stop} style={{ minWidth: 260 }}> <div onClick={stop}>
<JobPartsQueueCount parts={parts} /> <JobPartsQueueCount parts={parts} />
</div> </div>
} }
@@ -99,7 +108,8 @@ JobPartsReceived.propTypes = {
bodyshop: PropTypes.object, bodyshop: PropTypes.object,
parts: PropTypes.array, parts: PropTypes.array,
displayMode: PropTypes.oneOf(["full", "compact"]), displayMode: PropTypes.oneOf(["full", "compact"]),
popoverPlacement: PropTypes.string popoverPlacement: PropTypes.string,
countsOnly: PropTypes.bool
}; };
export default connect(mapStateToProps)(JobPartsReceived); export default connect(mapStateToProps)(JobPartsReceived);

View File

@@ -8,8 +8,6 @@ import { SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_JOBS_FOR_AUTOCOMPLETE } from
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
const { Option } = Select;
const JobSearchSelect = ({ const JobSearchSelect = ({
disabled, disabled,
convertedOnly = false, convertedOnly = false,
@@ -87,24 +85,24 @@ const JobSearchSelect = ({
style={{ width: "100%" }} style={{ width: "100%" }}
suffixIcon={(loading || idLoading) && <Spin />} // matches OLD spinner semantics suffixIcon={(loading || idLoading) && <Spin />} // matches OLD spinner semantics
notFoundContent={loading ? <LoadingOutlined /> : null} // matches OLD (loading only) notFoundContent={loading ? <LoadingOutlined /> : null} // matches OLD (loading only)
> options={theOptions?.map((o) => ({
{theOptions key: o.id,
? theOptions.map((o) => ( value: o.id,
<Option key={o.id} value={o.id} status={o.status}> status: o.status,
<Space align="center"> label: (
<span> <Space align="center">
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${o.ro_number || t("general.labels.na")} | ${OwnerNameDisplayFunction( <span>
o {`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${o.ro_number || t("general.labels.na")} | ${OwnerNameDisplayFunction(
)} | ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${o.v_model_desc || ""}`} o
</span> )} | ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${o.v_model_desc || ""}`}
<Tag> </span>
<strong>{o.status}</strong> <Tag>
</Tag> <strong>{o.status}</strong>
</Space> </Tag>
</Option> </Space>
)) )
: null} }))}
</Select> />
{error ? <AlertComponent title={error.message} type="error" /> : null} {error ? <AlertComponent title={error.message} type="error" /> : null}
{idError ? <AlertComponent title={idError.message} type="error" /> : null} {idError ? <AlertComponent title={idError.message} type="error" /> : null}

View File

@@ -59,13 +59,12 @@ export function JobsAdminClass({ bodyshop, job }) {
} }
]} ]}
> >
<Select> <Select
{bodyshop.md_classes.map((s) => ( options={bodyshop.md_classes.map((s) => ({
<Select.Option key={s} value={s}> value: s,
{s} label: s
</Select.Option> }))}
))} />
</Select>
</Form.Item> </Form.Item>
</Form> </Form>

View File

@@ -42,11 +42,11 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
<tbody> <tbody>
{fields.map((field, index) => ( {fields.map((field, index) => (
<tr key={field.key}> <tr key={field.key}>
{/* Hidden field to preserve jobline ID */}
<Form.Item hidden name={[field.name, "id"]}>
<input />
</Form.Item>
<td> <td>
{/* Hidden field to preserve jobline ID without injecting a div under <tr> */}
<Form.Item noStyle name={[field.name, "id"]}>
<input type="hidden" />
</Form.Item>
<Form.Item <Form.Item
// label={t("joblines.fields.line_desc")} // label={t("joblines.fields.line_desc")}
key={`${index}line_desc`} key={`${index}line_desc`}
@@ -141,13 +141,11 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}} }}
disabled={jobRO} disabled={jobRO}
> options={bodyshop.md_responsibility_centers.profits.map((p) => ({
{bodyshop.md_responsibility_centers.profits.map((p) => ( value: p.name,
<Select.Option key={p.name} value={p.name}> label: p.name
{p.name} }))}
</Select.Option> />
))}
</Select>
</Form.Item> </Form.Item>
</td> </td>
<td> <td>
@@ -171,13 +169,11 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}} }}
disabled={jobRO} disabled={jobRO}
> options={bodyshop.md_responsibility_centers.profits.map((p) => ({
{bodyshop.md_responsibility_centers.profits.map((p) => ( value: p.name,
<Select.Option key={p.name} value={p.name}> label: p.name
{p.name} }))}
</Select.Option> />
))}
</Select>
</Form.Item> </Form.Item>
</td> </td>
</tr> </tr>

View File

@@ -1,24 +1,28 @@
import { useMutation } from "@apollo/client/react"; import { useMutation } from "@apollo/client/react";
import { Button, Form, Input, Popover, Select, Space, Switch } from "antd"; import { Button, Divider, Form, Input, Modal, Select, Space, Switch } from "antd";
import axios from "axios"; import axios from "axios";
import { some } from "lodash"; import { some } from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries"; import { CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { DMS_MAP, getDmsMode } from "../../utils/dmsUtils";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import RREarlyROForm from "../dms-post-form/rr-early-ro-form";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
jobRO: selectJobReadOnly jobRO: selectJobReadOnly
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch( dispatch(
@@ -33,18 +37,83 @@ const mapDispatchToProps = (dispatch) => ({
export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTrail, parentFormIsFieldsTouched }) { export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTrail, parentFormIsFieldsTouched }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [earlyRoCreated, setEarlyRoCreated] = useState(!!job?.dms_id);
const [earlyRoCreatedThisSession, setEarlyRoCreatedThisSession] = useState(false);
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO); const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const notification = useNotification(); const notification = useNotification();
const allFormValues = Form.useWatch([], form); const allFormValues = Form.useWatch([], form);
const { socket } = useSocket();
const {
treatments: { Fortellis }
} = useTreatmentsWithConfig({
attributes: {},
names: ["Fortellis"],
splitKey: bodyshop?.imexshopid
});
const dmsMode = getDmsMode(bodyshop, Fortellis.treatment);
const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
const insuranceOptions = useMemo(
() =>
(bodyshop?.md_ins_cos ?? []).map((s) => ({
value: s.name,
label: s.name
})),
[bodyshop?.md_ins_cos]
);
const classOptions = useMemo(
() =>
(bodyshop?.md_classes ?? []).map((s) => ({
value: s,
label: s
})),
[bodyshop?.md_classes]
);
const referralOptions = useMemo(
() =>
(bodyshop?.md_referral_sources ?? []).map((s) => ({
value: s,
label: s
})),
[bodyshop?.md_referral_sources]
);
const csrOptions = useMemo(
() =>
(bodyshop?.employees ?? [])
.filter((emp) => emp.active)
.map((emp) => ({
value: emp.id,
label: `${emp.first_name} ${emp.last_name}`
})),
[bodyshop?.employees]
);
const categoryOptions = useMemo(
() =>
(bodyshop?.md_categories ?? []).map((s) => ({
value: s,
label: s
})),
[bodyshop?.md_categories]
);
const handleConvert = async ({ employee_csr, category, ...values }) => { const handleConvert = async ({ employee_csr, category, ...values }) => {
if (parentFormIsFieldsTouched()) { if (parentFormIsFieldsTouched()) {
alert(t("jobs.labels.savebeforeconversion")); alert(t("jobs.labels.savebeforeconversion"));
return; return;
} }
setLoading(true); setLoading(true);
const res = await mutationConvertJob({ const res = await mutationConvertJob({
variables: { variables: {
jobId: job.id, jobId: job.id,
@@ -58,13 +127,11 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
}); });
if (values.ca_gst_registrant) { if (values.ca_gst_registrant) {
await axios.post("/job/totalsssu", { await axios.post("/job/totalsssu", { id: job.id });
id: job.id
});
} }
if (!res.errors) { if (!res.errors) {
refetch(); refetch?.();
notification.success({ notification.success({
title: t("jobs.successes.converted") title: t("jobs.successes.converted")
}); });
@@ -77,182 +144,183 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
setOpen(false); setOpen(false);
} }
setLoading(false); setLoading(false);
}; };
const submitDisabled = useCallback(() => some(allFormValues, (v) => v === undefined), [allFormValues]); const submitDisabled = useCallback(() => some(allFormValues, (v) => v === undefined), [allFormValues]);
const popMenu = ( const handleEarlyROSuccess = (result) => {
<div> setEarlyRoCreated(true);
<Form setEarlyRoCreatedThisSession(true);
layout="vertical" notification.success({
form={form} title: t("jobs.successes.early_ro_created"),
onFinish={handleConvert} description: `RO Number: ${result.roNumber || "N/A"}`
initialValues={{ });
driveable: true,
towin: job.towin, setTimeout(() => {
ca_gst_registrant: job.ca_gst_registrant, refetch?.();
employee_csr: job.employee_csr, }, 2000);
category: job.category, };
referral_source: job.referral_source,
referral_source_extra: job.referral_source_extra ?? "" const handleModalClose = () => {
}} setOpen(false);
> };
<Form.Item
name={["ins_co_nm"]}
label={t("jobs.fields.ins_co_nm")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Select showSearch>
{bodyshop.md_ins_cos.map((s, i) => (
<Select.Option key={i} value={s.name}>
{s.name}
</Select.Option>
))}
</Select>
</Form.Item>
{bodyshop.enforce_class && (
<Form.Item
name={"class"}
label={t("jobs.fields.class")}
rules={[
{
required: bodyshop.enforce_class
//message: t("general.validation.required"),
}
]}
>
<Select>
{bodyshop.md_classes.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
)}
{bodyshop.enforce_referral && (
<>
<Form.Item
name={"referral_source"}
label={t("jobs.fields.referralsource")}
rules={[
{
required: bodyshop.enforce_referral
//message: t("general.validation.required"),
}
]}
>
<Select>
{bodyshop.md_referral_sources.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
<Input />
</Form.Item>
</>
)}
{bodyshop.enforce_conversion_csr && (
<Form.Item
name={"employee_csr"}
label={t(
InstanceRenderManager({
imex: "jobs.fields.employee_csr",
rome: "jobs.fields.employee_csr_writer"
})
)}
rules={[
{
required: bodyshop.enforce_conversion_csr
//message: t("general.validation.required"),
}
]}
>
<Select
showSearch={{
optionFilterProp: "children",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
style={{ width: 200 }}
>
{bodyshop.employees
.filter((emp) => emp.active)
.map((emp) => (
<Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
</Form.Item>
)}
{bodyshop.enforce_conversion_category && (
<Form.Item
name={"category"}
label={t("jobs.fields.category")}
rules={[
{
required: bodyshop.enforce_conversion_category
//message: t("general.validation.required"),
}
]}
>
<Select allowClear>
{bodyshop.md_categories.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
)}
{bodyshop.region_config.toLowerCase().startsWith("ca") && (
<Form.Item label={t("jobs.fields.ca_gst_registrant")} name="ca_gst_registrant" valuePropName="checked">
<Switch />
</Form.Item>
)}
<Form.Item label={t("jobs.fields.driveable")} name="driveable" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label={t("jobs.fields.towin")} name="towin" valuePropName="checked">
<Switch />
</Form.Item>
<Space wrap>
<Button disabled={submitDisabled()} type="primary" danger onClick={() => form.submit()} loading={loading}>
{t("jobs.actions.convert")}
</Button>
<Button onClick={() => setOpen(false)}>{t("general.actions.close")}</Button>
</Space>
</Form>
</div>
);
if (job.converted) return <></>; if (job.converted) return <></>;
return ( return (
<Popover open={open} content={popMenu}> <>
<Button <Button
key="convert" key="convert"
type="primary" type="primary"
danger danger
// style={{ display: job.converted ? "none" : "" }}
disabled={job.converted || jobRO} disabled={job.converted || jobRO}
loading={loading} loading={loading}
onClick={() => { onClick={() => {
setEarlyRoCreated(!!job?.dms_id);
setEarlyRoCreatedThisSession(false);
setOpen(true); setOpen(true);
}} }}
> >
{t("jobs.actions.convert")} {t("jobs.actions.convert")}
</Button> </Button>
</Popover>
<Modal
open={open}
onCancel={handleModalClose}
closable={!(earlyRoCreatedThisSession && !job.converted)}
maskClosable={!(earlyRoCreatedThisSession && !job.converted)}
title={t("jobs.actions.convert")}
footer={null}
width={700}
destroyOnHidden
>
<Form
layout="vertical"
form={form}
preserve={false}
onFinish={handleConvert}
initialValues={{
driveable: true,
towin: job.towin,
ca_gst_registrant: job.ca_gst_registrant,
employee_csr: job.employee_csr,
category: job.category,
referral_source: job.referral_source,
referral_source_extra: job.referral_source_extra ?? ""
}}
>
{isReynoldsMode && !job.dms_id && !earlyRoCreated && (
<>
<RREarlyROForm
bodyshop={bodyshop}
socket={socket}
job={job}
onSuccess={handleEarlyROSuccess}
showCancelButton={false}
/>
<Divider />
</>
)}
<Form.Item
name={["ins_co_nm"]}
label={t("jobs.fields.ins_co_nm")}
rules={[{ required: true }]}
>
<Select
showSearch={{
optionFilterProp:'label'
}}
options={insuranceOptions}
/>
</Form.Item>
{bodyshop.enforce_class && (
<Form.Item name="class" label={t("jobs.fields.class")} rules={[{ required: bodyshop.enforce_class }]}>
<Select options={classOptions} />
</Form.Item>
)}
{bodyshop.enforce_referral && (
<>
<Form.Item
name="referral_source"
label={t("jobs.fields.referralsource")}
rules={[{ required: bodyshop.enforce_referral }]}
>
<Select options={referralOptions} />
</Form.Item>
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
<Input />
</Form.Item>
</>
)}
{bodyshop.enforce_conversion_csr && (
<Form.Item
name="employee_csr"
label={t(
InstanceRenderManager({
imex: "jobs.fields.employee_csr",
rome: "jobs.fields.employee_csr_writer"
})
)}
rules={[{ required: bodyshop.enforce_conversion_csr }]}
>
<Select
showSearch={{
optionFilterProp: 'label',
filterOption: (input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}}
style={{ width: 200 }}
options={csrOptions}
/>
</Form.Item>
)}
{bodyshop.enforce_conversion_category && (
<Form.Item name="category" label={t("jobs.fields.category")} rules={[{ required: bodyshop.enforce_conversion_category }]}>
<Select allowClear options={categoryOptions} />
</Form.Item>
)}
{bodyshop.region_config.toLowerCase().startsWith("ca") && (
<Form.Item label={t("jobs.fields.ca_gst_registrant")} name="ca_gst_registrant" valuePropName="checked">
<Switch />
</Form.Item>
)}
<Form.Item label={t("jobs.fields.driveable")} name="driveable" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label={t("jobs.fields.towin")} name="towin" valuePropName="checked">
<Switch />
</Form.Item>
<Space wrap style={{ marginTop: 16 }}>
<Button
disabled={submitDisabled() || (isReynoldsMode && !job.dms_id && !earlyRoCreated)}
type="primary"
danger
onClick={() => form.submit()}
loading={loading}
>
{t("jobs.actions.convert")}
</Button>
<Button onClick={handleModalClose} disabled={earlyRoCreatedThisSession && !job.converted}>
{t("general.actions.close")}
</Button>
</Space>
</Form>
</Modal>
</>
); );
} }

View File

@@ -60,13 +60,13 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm"> <Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
<Select onChange={handleInsCoChange}> <Select
{bodyshop.md_ins_cos.map((s) => ( onChange={handleInsCoChange}
<Select.Option key={s.name} value={s.name}> options={bodyshop.md_ins_cos.map((s) => ({
{s.name} value: s.name,
</Select.Option> label: s.name
))} }))}
</Select> />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1"> <Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
<Input /> <Input />
@@ -192,13 +192,12 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.referralsource")} name="referral_source"> <Form.Item label={t("jobs.fields.referralsource")} name="referral_source">
<Select> <Select
{bodyshop.md_referral_sources.map((s) => ( options={bodyshop.md_referral_sources.map((s) => ({
<Select.Option key={s} value={s}> value: s,
{s} label: s
</Select.Option> }))}
))} />
</Select>
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra"> <Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
<Input /> <Input />
@@ -221,10 +220,13 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<CurrencyInput min={0} /> <CurrencyInput min={0} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status"> <Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
<Select allowClear> <Select
<Select.Option value="W">{t("jobs.labels.deductible.waived")}</Select.Option> allowClear
<Select.Option value="Y">{t("jobs.labels.deductible.stands")}</Select.Option> options={[
</Select> { value: "W", label: t("jobs.labels.deductible.waived") },
{ value: "Y", label: t("jobs.labels.deductible.stands") }
]}
/>
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.depreciation_taxes")} name="depreciation_taxes"> <Form.Item label={t("jobs.fields.depreciation_taxes")} name="depreciation_taxes">
<CurrencyInput /> <CurrencyInput />

View File

@@ -43,20 +43,19 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
<Input disabled={jobRO} /> <Input disabled={jobRO} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status"> <Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
<Select disabled={jobRO}> <Select disabled={jobRO} options={[
<Select.Option value="W">{t("jobs.labels.deductible.waived")}</Select.Option> { value: "W", label: t("jobs.labels.deductible.waived") },
<Select.Option value="Y">{t("jobs.labels.deductible.stands")}</Select.Option> { value: "Y", label: t("jobs.labels.deductible.stands") }
</Select> ]} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt"> <Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
<CurrencyInput disabled={jobRO} min={0} /> <CurrencyInput disabled={jobRO} min={0} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.ded_note")} name="ded_note"> <Form.Item label={t("jobs.fields.ded_note")} name="ded_note">
<Select disabled={jobRO}> <Select disabled={jobRO} options={bodyshop.md_ded_notes.map((n) => ({
{bodyshop.md_ded_notes.map((n, index) => ( value: n,
<Select.Option key={index}>{n}</Select.Option> label: n
))} }))} />
</Select>
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no"> <Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
<Input disabled={jobRO} /> <Input disabled={jobRO} />
@@ -66,13 +65,10 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm"> <Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
<Select disabled={jobRO} onChange={handleInsCoChange}> <Select disabled={jobRO} onChange={handleInsCoChange} options={bodyshop.md_ins_cos.map((s) => ({
{bodyshop.md_ins_cos.map((s) => ( value: s.name,
<Select.Option key={s.name} value={s.name}> label: s.name
{s.name} }))} />
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1"> <Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
<Input disabled={jobRO} /> <Input disabled={jobRO} />
@@ -123,25 +119,19 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
} }
]} ]}
> >
<Select disabled={jobRO} allowClear> <Select disabled={jobRO} allowClear options={bodyshop.md_referral_sources.map((s) => ({
{bodyshop.md_referral_sources.map((s) => ( value: s,
<Select.Option key={s} value={s}> label: s
{s} }))} />
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra"> <Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
<Input disabled={jobRO} /> <Input disabled={jobRO} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.alt_transport")} name="alt_transport"> <Form.Item label={t("jobs.fields.alt_transport")} name="alt_transport">
<Select disabled={jobRO} allowClear> <Select disabled={jobRO} allowClear options={bodyshop.appt_alt_transport.map((s) => ({
{bodyshop.appt_alt_transport.map((s) => ( value: s,
<Select.Option key={s} value={s}> label: s
{s} }))} />
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
</FormRow> </FormRow>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
@@ -243,15 +233,11 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
</FormRow> </FormRow>
<FormRow header={t("jobs.forms.other")}> <FormRow header={t("jobs.forms.other")}>
<Form.Item label={t("jobs.fields.category")} name="category"> <Form.Item label={t("jobs.fields.category")} name="category">
<Select disabled={jobRO} allowClear> <Select disabled={jobRO} allowClear options={bodyshop.md_categories.map((s) => ({
{bodyshop.md_categories.map((s) => ( value: s,
<Select.Option key={s} value={s}> label: s
{s} }))} />
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.selling_dealer")} name="selling_dealer"> <Form.Item label={t("jobs.fields.selling_dealer")} name="selling_dealer">
<Input disabled={jobRO} /> <Input disabled={jobRO} />
</Form.Item> </Form.Item>
@@ -267,6 +253,21 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
<Form.Item label={t("jobs.fields.lost_sale_reason")} name="lost_sale_reason"> <Form.Item label={t("jobs.fields.lost_sale_reason")} name="lost_sale_reason">
<Input disabled={jobRO} allowClear /> <Input disabled={jobRO} allowClear />
</Form.Item> </Form.Item>
{bodyshop.rr_dealerid && (
<Form.Item label={t("jobs.fields.dms.id")} name="dms_id">
<Input disabled />
</Form.Item>
)}
{bodyshop.rr_dealerid && (
<Form.Item label={t("jobs.fields.dms.advisor")} name="dms_advisor_id">
<Input disabled />
</Form.Item>
)}
{bodyshop.rr_dealerid && (
<Form.Item label={t("jobs.fields.dms.customer")} name="dms_customer_id">
<Input disabled />
</Form.Item>
)}
</FormRow> </FormRow>
</Card> </Card>
); );

View File

@@ -157,7 +157,6 @@ export function JobsDetailHeaderActions({
variables: watcherVars, variables: watcherVars,
skip: !jobId, skip: !jobId,
fetchPolicy: "cache-first", fetchPolicy: "cache-first",
notifyOnNetworkStatusChange: true
}); });
const jobWatchersCount = jobWatchersData?.job_watchers?.length ?? job?.job_watchers?.length ?? 0; const jobWatchersCount = jobWatchersData?.job_watchers?.length ?? job?.job_watchers?.length ?? 0;
@@ -715,13 +714,12 @@ export function JobsDetailHeaderActions({
<FormDateTimePickerComponent /> <FormDateTimePickerComponent />
</Form.Item> </Form.Item>
<Form.Item label={t("appointments.fields.color")} name="color"> <Form.Item label={t("appointments.fields.color")} name="color">
<Select> <Select
{bodyshop.appt_colors.map((col, idx) => ( options={bodyshop.appt_colors.map((col) => ({
<Select.Option key={idx} value={col.color.hex}> value: col.color.hex,
{col.label} label: col.label
</Select.Option> }))}
))} />
</Select>
</Form.Item> </Form.Item>
<Space wrap> <Space wrap>

View File

@@ -81,17 +81,16 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
const employeeData = bodyshop.associations.find((a) => a.useremail === job.admin_clerk)?.user?.employee ?? null; const employeeData = bodyshop.associations.find((a) => a.useremail === job.admin_clerk)?.user?.employee ?? null;
// Handle checkbox changes // Handle checkbox changes
const handleCheckboxChange = async (field, e) => { const handleCheckboxChange = async (field, checked) => {
e.preventDefault();
e.stopPropagation();
const checked = e.target.checked;
const value = checked ? dayjs().toISOString() : null; const value = checked ? dayjs().toISOString() : null;
try { try {
const ret = await updateJob({ const ret = await updateJob({
variables: { variables: {
jobId: job.id, jobId: job.id,
job: { [field]: value } job: { [field]: value }
} },
refetchQueries: ["GET_JOB_BY_PK"],
awaitRefetchQueries: true
}); });
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
@@ -183,7 +182,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
<Space> <Space>
<Checkbox <Checkbox
checked={!!job.estimate_sent_approval} checked={!!job.estimate_sent_approval}
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e)} onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
disabled={disabled || isPartsEntry} disabled={disabled || isPartsEntry}
> >
{job.estimate_sent_approval && ( {job.estimate_sent_approval && (
@@ -198,7 +197,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
<Space> <Space>
<Checkbox <Checkbox
checked={!!job.estimate_approved} checked={!!job.estimate_approved}
onChange={(e) => handleCheckboxChange("estimate_approved", e)} onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
disabled={disabled || isPartsEntry} disabled={disabled || isPartsEntry}
> >
{job.estimate_approved && ( {job.estimate_approved && (

View File

@@ -94,22 +94,26 @@ export function LaborAllocationsAdjustmentEdit({
} }
]} ]}
> >
<Select allowClear disabled={!!mod_lbr_ty}> <Select
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option> allowClear
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option> disabled={!!mod_lbr_ty}
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option> options={[
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option> { value: "LAA", label: t("joblines.fields.lbr_types.LAA") },
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option> { value: "LAB", label: t("joblines.fields.lbr_types.LAB") },
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option> { value: "LAD", label: t("joblines.fields.lbr_types.LAD") },
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option> { value: "LAE", label: t("joblines.fields.lbr_types.LAE") },
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option> { value: "LAF", label: t("joblines.fields.lbr_types.LAF") },
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option> { value: "LAG", label: t("joblines.fields.lbr_types.LAG") },
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option> { value: "LAM", label: t("joblines.fields.lbr_types.LAM") },
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option> { value: "LAR", label: t("joblines.fields.lbr_types.LAR") },
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option> { value: "LAS", label: t("joblines.fields.lbr_types.LAS") },
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option> { value: "LAU", label: t("joblines.fields.lbr_types.LAU") },
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option> { value: "LA1", label: t("joblines.fields.lbr_types.LA1") },
</Select> { value: "LA2", label: t("joblines.fields.lbr_types.LA2") },
{ value: "LA3", label: t("joblines.fields.lbr_types.LA3") },
{ value: "LA4", label: t("joblines.fields.lbr_types.LA4") }
]}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("jobs.fields.adjustmenthours")} label={t("jobs.fields.adjustmenthours")}
@@ -132,7 +136,13 @@ export function LaborAllocationsAdjustmentEdit({
); );
return ( return (
<Popover open={open} onOpenChange={(vis) => setOpen(vis)} content={overlay} trigger="click"> <Popover
getPopupContainer={(trigger) => trigger?.parentElement || document.body}
open={open}
onOpenChange={(vis) => setOpen(vis)}
content={overlay}
trigger="click"
>
{children} {children}
</Popover> </Popover>
); );

View File

@@ -56,7 +56,6 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount,
where: whereClause where: whereClause
}, },
fetchPolicy: "cache-and-network", fetchPolicy: "cache-and-network",
notifyOnNetworkStatusChange: true,
errorPolicy: "all", errorPolicy: "all",
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(), pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
skip: skipQuery skip: skipQuery

View File

@@ -2,6 +2,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { store } from "../../redux/store"; import { store } from "../../redux/store";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { Tooltip } from "antd";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -11,15 +12,27 @@ const mapDispatchToProps = () => ({
}); });
export default connect(mapStateToProps, mapDispatchToProps)(OwnerNameDisplay); export default connect(mapStateToProps, mapDispatchToProps)(OwnerNameDisplay);
export function OwnerNameDisplay({ bodyshop, ownerObject }) { export function OwnerNameDisplay({ bodyshop, ownerObject, withToolTip = false }) {
const emptyTest = ownerObject?.ownr_fn + ownerObject?.ownr_ln + ownerObject?.ownr_co_nm; const emptyTest = ownerObject?.ownr_fn + ownerObject?.ownr_ln + ownerObject?.ownr_co_nm;
if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "") return "N/A"; if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "") return "N/A";
if (bodyshop.last_name_first) let returnString;
return `${ownerObject?.ownr_ln || ""}, ${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_co_nm || ""}`.trim(); if (bodyshop.last_name_first) {
returnString =
return `${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_ln || ""} ${ownerObject.ownr_co_nm || ""}`.trim(); `${ownerObject?.ownr_ln || ""}, ${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_co_nm || ""}`.trim();
} else {
returnString = `${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_ln || ""} ${ownerObject.ownr_co_nm || ""}`.trim();
}
if (withToolTip) {
return (
<Tooltip title={returnString} mouseEnterDelay={0.5}>
{returnString}
</Tooltip>
);
} else {
return returnString;
}
} }
export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) { export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) {

View File

@@ -7,8 +7,6 @@ import { SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_OWNERS_FOR_AUTOCOMPLETE }
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
const { Option } = Select;
const OwnerSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => { const OwnerSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_OWNERS_FOR_AUTOCOMPLETE); const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_OWNERS_FOR_AUTOCOMPLETE);
@@ -16,9 +14,10 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE
); );
const executeSearch = (v) => { const executeSearch = (variables) => {
if (v && v.variables?.search !== "" && v.variables.search.length >= 2) callSearch({ variables: v.variables }); if (variables?.search !== "" && variables?.search?.length >= 2) callSearch({ variables });
}; };
const debouncedExecuteSearch = _.debounce(executeSearch, 500); const debouncedExecuteSearch = _.debounce(executeSearch, 500);
const handleSearch = (value) => { const handleSearch = (value) => {
@@ -70,15 +69,12 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
onSelect={handleSelect} onSelect={handleSelect}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />} notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
onBlur={onBlur} onBlur={onBlur}
> options={theOptions?.map((o) => ({
{theOptions key: o.id,
? theOptions.map((o) => ( value: o.id,
<Option key={o.id} value={o.id}> label: `${OwnerNameDisplayFunction(o)} | ${o.ownr_addr1 || ""} `
{`${OwnerNameDisplayFunction(o)} | ${o.ownr_addr1 || ""} `} }))}
</Option> />
))
: null}
</Select>
{idLoading || loading ? <LoadingOutlined /> : null} {idLoading || loading ? <LoadingOutlined /> : null}
{error ? <AlertComponent title={error.message} type="error" /> : null} {error ? <AlertComponent title={error.message} type="error" /> : null}
{idError ? <AlertComponent title={idError.message} type="error" /> : null} {idError ? <AlertComponent title={idError.message} type="error" /> : null}

View File

@@ -1,94 +1,121 @@
import { DownOutlined } from "@ant-design/icons"; import { DownOutlined } from "@ant-design/icons";
import { Dropdown, InputNumber, Space } from "antd"; import { Button, Divider, Dropdown, InputNumber, Space, theme } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
const DISCOUNT_PRESETS = [5, 10, 15, 20, 25, 40];
export default function PartsOrderModalPriceChange({ form, field }) { export default function PartsOrderModalPriceChange({ form, field }) {
const { t } = useTranslation(); const { t } = useTranslation();
const menu = { const { token } = theme.useToken();
items: [
{
key: "5",
label: t("parts_orders.labels.discount", { percent: "5%" })
},
{
key: "10",
label: t("parts_orders.labels.discount", { percent: "10%" })
},
{
key: "15",
label: t("parts_orders.labels.discount", { percent: "15%" })
},
{
key: "20",
label: t("parts_orders.labels.discount", { percent: "20%" })
},
{
key: "25",
label: t("parts_orders.labels.discount", { percent: "25%" })
},
{
key: "40",
label: t("parts_orders.labels.discount", { percent: "40%" })
},
{
key: "custom",
label: (
<Space.Compact>
<InputNumber
onClick={(e) => e.stopPropagation()}
onKeyUp={(e) => {
if (e.key === "Enter") {
const values = form.getFieldsValue();
const { parts_order_lines } = values;
form.setFieldsValue({ const [open, setOpen] = useState(false);
parts_order_lines: { const [customPercent, setCustomPercent] = useState(0);
data: parts_order_lines.data.map((p, idx) => {
if (idx !== field.name) return p; const applyDiscountPercent = (percent) => {
console.log(p, e.target.value, (p.act_price || 0) * ((100 - (e.target.value || 0)) / 100)); const pct = Number(percent) || 0;
return {
...p, const values = form.getFieldsValue();
act_price: (p.act_price || 0) * ((100 - (e.target.value || 0)) / 100) const parts_order_lines = values?.parts_order_lines;
}; const data = Array.isArray(parts_order_lines?.data) ? parts_order_lines.data : [];
}) if (!data.length) return;
}
}); form.setFieldsValue({
e.target.value = 0; parts_order_lines: {
} data: data.map((p, idx) => {
}} if (idx !== field.name) return p;
min={0} return {
max={100} ...p,
/> act_price: (p.act_price || 0) * ((100 - pct) / 100)
<span style={{ padding: "0 11px", backgroundColor: "#fafafa", border: "1px solid #d9d9d9", borderLeft: 0 }}>%</span> };
</Space.Compact> })
)
} }
], });
};
const applyCustom = () => {
logImEXEvent("parts_order_manual_discount", {});
applyDiscountPercent(customPercent);
setCustomPercent(0);
setOpen(false);
};
const menu = {
// Kill the menu “card” styling so our wrapper becomes the single card.
style: {
background: "transparent",
boxShadow: "none"
},
items: DISCOUNT_PRESETS.map((pct) => ({
key: String(pct),
label: t("parts_orders.labels.discount", { percent: `${pct}%` })
})),
onClick: ({ key }) => { onClick: ({ key }) => {
logImEXEvent("parts_order_manual_discount", {}); logImEXEvent("parts_order_manual_discount", {});
if (key === "custom") return; applyDiscountPercent(key);
const values = form.getFieldsValue(); setOpen(false);
const { parts_order_lines } = values;
form.setFieldsValue({
parts_order_lines: {
data: parts_order_lines.data.map((p, idx) => {
if (idx !== field.name) return p;
return {
...p,
act_price: (p.act_price || 0) * ((100 - key) / 100)
};
})
}
});
} }
}; };
return ( return (
<Dropdown menu={menu} trigger="click"> <Dropdown
menu={menu}
trigger={["click"]}
open={open}
onOpenChange={(nextOpen) => setOpen(nextOpen)}
getPopupContainer={(triggerNode) => triggerNode?.parentElement ?? document.body}
popupRender={(menus) => (
<div
// This makes the whole dropdown (menu + footer) look like one panel in both light/dark.
style={{
background: token.colorBgElevated,
borderRadius: token.borderRadiusLG,
boxShadow: token.boxShadowSecondary,
overflow: "hidden",
minWidth: 180
}}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
{menus}
<Divider style={{ margin: 0 }} />
<div style={{ padding: token.paddingXS }}>
<Space.Compact style={{ width: "100%" }}>
<InputNumber
value={customPercent}
min={0}
max={100}
precision={0}
controls={false}
style={{ width: "100%" }}
formatter={(v) => (v === null || v === undefined ? "" : `${v}%`)}
parser={(v) =>
String(v ?? "")
.replace("%", "")
.trim()
}
onChange={(v) => setCustomPercent(v ?? 0)}
onKeyDown={(e) => {
e.stopPropagation();
if (e.key === "Enter") {
e.preventDefault();
applyCustom();
}
}}
/>
<Button type="primary" onClick={applyCustom}>
{t("general.labels.apply")}
</Button>
</Space.Compact>
</div>
</div>
)}
>
<Space> <Space>
% % <DownOutlined />
<DownOutlined />
</Space> </Space>
</Dropdown> </Dropdown>
); );

View File

@@ -158,19 +158,21 @@ export function PartsOrderModalComponent({
key={`${index}part_type`} key={`${index}part_type`}
name={[field.name, "part_type"]} name={[field.name, "part_type"]}
> >
<Select disabled={!(sendType === "oec" && OEConnection_PriceChange.treatment === "on")}> <Select
<Select.Option value="PAA">{t("joblines.fields.part_types.PAA")}</Select.Option> disabled={!(sendType === "oec" && OEConnection_PriceChange.treatment === "on")}
<Select.Option value="PAC">{t("joblines.fields.part_types.PAC")}</Select.Option> options={[
{ value: "PAA", label: t("joblines.fields.part_types.PAA") },
<Select.Option value="PAL">{t("joblines.fields.part_types.PAL")}</Select.Option> { value: "PAC", label: t("joblines.fields.part_types.PAC") },
<Select.Option value="PAG">{t("joblines.fields.part_types.PAG")}</Select.Option> { value: "PAL", label: t("joblines.fields.part_types.PAL") },
<Select.Option value="PAM">{t("joblines.fields.part_types.PAM")}</Select.Option> { value: "PAG", label: t("joblines.fields.part_types.PAG") },
<Select.Option value="PAP">{t("joblines.fields.part_types.PAP")}</Select.Option> { value: "PAM", label: t("joblines.fields.part_types.PAM") },
<Select.Option value="PAN">{t("joblines.fields.part_types.PAN")}</Select.Option> { value: "PAP", label: t("joblines.fields.part_types.PAP") },
<Select.Option value="PAO">{t("joblines.fields.part_types.PAO")}</Select.Option> { value: "PAN", label: t("joblines.fields.part_types.PAN") },
<Select.Option value="PAR">{t("joblines.fields.part_types.PAR")}</Select.Option> { value: "PAO", label: t("joblines.fields.part_types.PAO") },
<Select.Option value="PAS">{t("joblines.fields.part_types.PAS")}</Select.Option> { value: "PAR", label: t("joblines.fields.part_types.PAR") },
</Select> { value: "PAS", label: t("joblines.fields.part_types.PAS") }
]}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("parts_orders.fields.oem_partno")} label={t("parts_orders.fields.oem_partno")}

View File

@@ -17,7 +17,6 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import PartsOrderModalComponent from "./parts-order-modal.component"; import PartsOrderModalComponent from "./parts-order-modal.component";
import axios from "axios"; import axios from "axios";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
@@ -66,7 +65,7 @@ export function PartsOrderModalContainer({
const sendTypeState = useState("e"); const sendTypeState = useState("e");
const sendType = sendTypeState[0]; const sendType = sendTypeState[0];
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, { const { error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, {
skip: !open, skip: !open,
variables: { jobId: jobId }, variables: { jobId: jobId },
fetchPolicy: "network-only", fetchPolicy: "network-only",
@@ -94,16 +93,6 @@ export function PartsOrderModalContainer({
}; };
}); });
const missingIdx = forcedLines.findIndex((l) => !l?.job_line_id);
if (missingIdx !== -1) {
notification.error({
title: t("parts_orders.errors.creating"),
description: `Missing job_line_id for parts line #${missingIdx + 1}`
});
setSaving(false);
return;
}
let insertResult; let insertResult;
try { try {
insertResult = await insertPartOrder({ insertResult = await insertPartOrder({
@@ -372,6 +361,7 @@ export function PartsOrderModalContainer({
} }
}, [open, linesToOrder, form]); }, [open, linesToOrder, form]);
//This used to have a loading component spinner for the vendor data. With Apollo 4, the NetworkState isn't emitting correctly, so loading just gets set to true the second time, and no longer works as expected.
return ( return (
<Modal <Modal
open={open} open={open}
@@ -390,18 +380,14 @@ export function PartsOrderModalContainer({
> >
{error ? <AlertComponent title={error.message} type="error" /> : null} {error ? <AlertComponent title={error.message} type="error" /> : null}
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish} initialValues={initialValues}> <Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish} initialValues={initialValues}>
{loading ? ( <PartsOrderModalComponent
<LoadingSpinner /> form={form}
) : ( vendorList={data?.vendors || []}
<PartsOrderModalComponent sendTypeState={sendTypeState}
form={form} isReturn={isReturn}
vendorList={data?.vendors || []} preferredMake={data && data.jobs[0] && data.jobs[0].v_make_desc}
sendTypeState={sendTypeState} job={job}
isReturn={isReturn} />
preferredMake={data && data.jobs[0] && data.jobs[0].v_make_desc}
job={job}
/>
)}
</Form> </Form>
</Modal> </Modal>
); );

View File

@@ -1,6 +1,6 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client/react"; import { useQuery } from "@apollo/client/react";
import { Button, Card, Input, Space, Table } from "antd"; import { Button, Card, Checkbox, Input, Space, Table } from "antd";
import _ from "lodash"; import _ from "lodash";
import queryString from "query-string"; import queryString from "query-string";
import { useState } from "react"; import { useState } from "react";
@@ -31,6 +31,8 @@ export function PartsQueueListComponent({ bodyshop }) {
const { selected, sortcolumn, sortorder, statusFilters } = searchParams; const { selected, sortcolumn, sortorder, statusFilters } = searchParams;
const history = useNavigate(); const history = useNavigate();
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null); const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
const [viewTimeStamp, setViewTimeStamp] = useLocalStorage("parts_queue_timestamps", false);
const [countsOnly, setCountsOnly] = useLocalStorage("parts_queue_counts_only", false);
const { loading, error, data, refetch } = useQuery(QUERY_PARTS_QUEUE, { const { loading, error, data, refetch } = useQuery(QUERY_PARTS_QUEUE, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
@@ -92,6 +94,7 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
width: "110px",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder, sortOrder: sortcolumn === "ro_number" && sortorder,
@@ -103,16 +106,20 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
dataIndex: "ownr_ln", dataIndex: "ownr_ln",
key: "ownr_ln", key: "ownr_ln",
width: "8%",
ellipsis: {
showTitle: true
},
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortcolumn === "ownr_ln" && sortorder, sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
<Link to={"/manage/owners/" + record.ownerid}> <Link to={"/manage/owners/" + record.ownerid}>
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record} withToolTip />
</Link> </Link>
) : ( ) : (
<span> <span>
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record} withToolTip />
</span> </span>
); );
} }
@@ -187,7 +194,7 @@ export function PartsQueueListComponent({ bodyshop }) {
ellipsis: true, ellipsis: true,
sorter: (a, b) => dateSort(a.scheduled_in, b.scheduled_in), sorter: (a, b) => dateSort(a.scheduled_in, b.scheduled_in),
sortOrder: sortcolumn === "scheduled_in" && sortorder, sortOrder: sortcolumn === "scheduled_in" && sortorder,
render: (text, record) => <DateTimeFormatter>{record.scheduled_in}</DateTimeFormatter> render: (text, record) => <DateTimeFormatter hideTime={!viewTimeStamp}>{record.scheduled_in}</DateTimeFormatter>
}, },
{ {
title: t("jobs.fields.scheduled_completion"), title: t("jobs.fields.scheduled_completion"),
@@ -196,7 +203,9 @@ export function PartsQueueListComponent({ bodyshop }) {
ellipsis: true, ellipsis: true,
sorter: (a, b) => dateSort(a.scheduled_completion, b.scheduled_completion), sorter: (a, b) => dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder: sortcolumn === "scheduled_completion" && sortorder, sortOrder: sortcolumn === "scheduled_completion" && sortorder,
render: (text, record) => <DateTimeFormatter>{record.scheduled_completion}</DateTimeFormatter> render: (text, record) => (
<DateTimeFormatter hideTime={!viewTimeStamp}>{record.scheduled_completion}</DateTimeFormatter>
)
}, },
// { // {
// title: t("vehicles.fields.plate_no"), // title: t("vehicles.fields.plate_no"),
@@ -227,16 +236,23 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.updated_at"), title: t("jobs.fields.updated_at"),
dataIndex: "updated_at", dataIndex: "updated_at",
key: "updated_at", key: "updated_at",
width: "110px",
sorter: (a, b) => dateSort(a.updated_at, b.updated_at), sorter: (a, b) => dateSort(a.updated_at, b.updated_at),
sortOrder: sortcolumn === "updated_at" && sortorder, sortOrder: sortcolumn === "updated_at" && sortorder,
render: (text, record) => <TimeAgoFormatter>{record.updated_at}</TimeAgoFormatter> render: (text, record) => <TimeAgoFormatter removeAgoString>{record.updated_at}</TimeAgoFormatter>
}, },
{ {
title: t("jobs.fields.partsstatus"), title: t("jobs.fields.partsstatus"),
dataIndex: "partsstatus", dataIndex: "partsstatus",
key: "partsstatus", key: "partsstatus",
width: countsOnly ? "180px" : "110px",
render: (text, record) => ( render: (text, record) => (
<JobPartsReceived parts={record.joblines_status} displayMode="full" popoverPlacement="topLeft" /> <JobPartsReceived
parts={record.joblines_status}
displayMode="full"
popoverPlacement="middle"
countsOnly={countsOnly}
/>
) )
}, },
{ {
@@ -249,6 +265,7 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.queued_for_parts"), title: t("jobs.fields.queued_for_parts"),
dataIndex: "queued_for_parts", dataIndex: "queued_for_parts",
key: "queued_for_parts", key: "queued_for_parts",
width: "120px",
sorter: (a, b) => a.queued_for_parts - b.queued_for_parts, sorter: (a, b) => a.queued_for_parts - b.queued_for_parts,
sortOrder: sortcolumn === "queued_for_parts" && sortorder, sortOrder: sortcolumn === "queued_for_parts" && sortorder,
filteredValue: filter?.queued_for_parts || null, filteredValue: filter?.queued_for_parts || null,
@@ -275,6 +292,12 @@ export function PartsQueueListComponent({ bodyshop }) {
<Button onClick={() => refetch()}> <Button onClick={() => refetch()}>
<SyncOutlined /> <SyncOutlined />
</Button> </Button>
<Checkbox checked={countsOnly} onChange={(e) => setCountsOnly(e.target.checked)}>
{t("parts.labels.view_counts_only")}
</Checkbox>
<Checkbox checked={viewTimeStamp} onChange={(e) => setViewTimeStamp(e.target.checked)}>
{t("parts.labels.view_timestamps")}
</Checkbox>
<Input.Search <Input.Search
className="imex-table-header__search" className="imex-table-header__search"
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
@@ -299,7 +322,7 @@ export function PartsQueueListComponent({ bodyshop }) {
rowKey="id" rowKey="id"
dataSource={jobs} dataSource={jobs}
style={{ height: "100%" }} style={{ height: "100%" }}
scroll={{ x: true }} //scroll={{ x: true }}
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={{ rowSelection={{
onSelect: (record) => { onSelect: (record) => {

View File

@@ -29,13 +29,12 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
}) })
}); });
}} }}
> options={bodyshop.md_parts_locations.map((loc, idx) => ({
{bodyshop.md_parts_locations.map((loc, idx) => ( key: idx,
<Select.Option key={idx} value={loc}> value: loc,
{loc} label: loc
</Select.Option> }))}
))} />
</Select>
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<Typography.Title level={4}>{t("parts_orders.labels.inthisorder")}</Typography.Title> <Typography.Title level={4}>{t("parts_orders.labels.inthisorder")}</Typography.Title>
@@ -85,13 +84,14 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
key={`${index}location`} key={`${index}location`}
name={[field.name, "location"]} name={[field.name, "location"]}
> >
<Select style={{ width: "10rem" }}> <Select
{bodyshop.md_parts_locations.map((loc, idx) => ( style={{ width: "10rem" }}
<Select.Option key={idx} value={loc}> options={bodyshop.md_parts_locations.map((loc, idx) => ({
{loc} key: idx,
</Select.Option> value: loc,
))} label: loc
</Select> }))}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("parts_orders.fields.quantity")} label={t("parts_orders.fields.quantity")}

View File

@@ -2,8 +2,6 @@ import { Button, Card, Divider, Form, Input, Select, Space } from "antd";
import { DeleteOutlined, PlusOutlined } from "@ant-design/icons"; import { DeleteOutlined, PlusOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const { Option } = Select;
export default function PartsShopInfoEmailPresets() { export default function PartsShopInfoEmailPresets() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -26,13 +24,7 @@ export default function PartsShopInfoEmailPresets() {
label={t("bodyshop.labels.email_type")} label={t("bodyshop.labels.email_type")}
rules={[{ required: true, message: t("bodyshop.errors.email_type_required") }]} rules={[{ required: true, message: t("bodyshop.errors.email_type_required") }]}
> >
<Select placeholder={t("bodyshop.placeholders.select_email_type")}> <Select placeholder={t("bodyshop.placeholders.select_email_type")} options={emailTypes} />
{emailTypes.map((type) => (
<Option key={type.value} value={type.value}>
{type.label}
</Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
{...restField} {...restField}

View File

@@ -91,20 +91,25 @@ export function PaymentFormComponent({ form, bodyshop, disabled }) {
} }
]} ]}
> >
<Select disabled={disabled}> <Select disabled={disabled}
<Select.Option value={t("payments.labels.customer")}>{t("payments.labels.customer")}</Select.Option> options={Qb_Multi_Ar.treatment === "on"
{Qb_Multi_Ar.treatment === "on" ? ( ? [
<Select.OptGroup label={t("payments.labels.external")}> { value: t("payments.labels.customer"), label: t("payments.labels.customer") },
{bodyshop.md_ins_cos.map((i, idx) => ( {
<Select.Option key={idx} value={i.name}> label: t("payments.labels.external"),
{i.name} options: bodyshop.md_ins_cos.map((i, idx) => ({
</Select.Option> key: idx,
))} value: i.name,
</Select.OptGroup> label: i.name
) : ( }))
<Select.Option value={t("payments.labels.insurance")}>{t("payments.labels.insurance")}</Select.Option> }
)} ]
</Select> : [
{ value: t("payments.labels.customer"), label: t("payments.labels.customer") },
{ value: t("payments.labels.insurance"), label: t("payments.labels.insurance") }
]
}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -117,13 +122,13 @@ export function PaymentFormComponent({ form, bodyshop, disabled }) {
} }
]} ]}
> >
<Select disabled={disabled}> <Select disabled={disabled}
{bodyshop.md_payment_types.map((v, idx) => ( options={bodyshop.md_payment_types.map((v, idx) => ({
<Select.Option key={idx} value={v}> key: idx,
{v} value: v,
</Select.Option> label: v
))} }))}
</Select> />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow> <LayoutFormRow grow>

View File

@@ -33,7 +33,7 @@ export default function PaymentFormTotalPayments({ jobid }) {
{balance && ( {balance && (
<Statistic <Statistic
title={t("payments.labels.balance")} title={t("payments.labels.balance")}
styles={{ value: { color: balance.getAmount() !== 0 ? "red" : "green" } }} styles={{ content: { color: balance.getAmount() !== 0 ? "red" : "green" } }}
value={(balance && balance.toFormat()) || ""} value={(balance && balance.toFormat()) || ""}
/> />
)} )}

View File

@@ -108,7 +108,7 @@ export function PrintCenterJobsLabels({ jobId }) {
</Card> </Card>
); );
return ( return (
<Popover content={content} open={isModalVisible}> <Popover content={content} open={isModalVisible} getPopupContainer={(trigger) => trigger.parentElement}>
<Button onClick={() => setIsModalVisible(true)}>{t("printcenter.jobs.labels.labels")}</Button> <Button onClick={() => setIsModalVisible(true)}>{t("printcenter.jobs.labels.labels")}</Button>
</Popover> </Popover>
); );

View File

@@ -35,8 +35,6 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
const result = await updateJob({ const result = await updateJob({
variables: { jobId: record.id, job: { [empAssignment]: employeeid } } variables: { jobId: record.id, job: { [empAssignment]: employeeid } }
// awaitRefetchQueries: true,
}); });
insertAuditTrail({ insertAuditTrail({
@@ -55,6 +53,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
await refetch(); await refetch();
setAssignment({ operation: null, employeeid: null });
setLoading(false); setLoading(false);
}; };
const handleRemove = async (operation) => { const handleRemove = async (operation) => {
@@ -84,6 +83,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
await refetch(); await refetch();
setAssignment({ operation: null, employeeid: null });
setLoading(false); setLoading(false);
}; };
@@ -94,29 +94,31 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
const [visibility, setVisibility] = useState(false); const [visibility, setVisibility] = useState(false);
const onChange = (e, option) => { const onChange = (e, option) => {
setAssignment({ ...assignment, employeeid: e, name: option.name }); setAssignment({ ...assignment, employeeid: e, name: option.label });
}; };
const employeeOptions = bodyshop.employees
.filter((emp) => emp.active)
.map((emp) => ({
value: emp.id,
label: `${emp.first_name} ${emp.last_name}`,
name: `${emp.first_name} ${emp.last_name}`
}));
const popContent = ( const popContent = (
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={24}> <Col span={24}>
<Select <Select
id="employeeSelector" id="employeeSelector"
showSearch={{ showSearch={{
optionFilterProp: "children", optionFilterProp: "label",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}} }}
style={{ width: 200 }} style={{ width: 200 }}
value={assignment.employeeid}
onChange={onChange} onChange={onChange}
> options={employeeOptions}
{bodyshop.employees />
.filter((emp) => emp.active)
.map((emp) => (
<Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
</Col> </Col>
<Col span={24}> <Col span={24}>
<Space wrap> <Space wrap>
@@ -141,25 +143,25 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]); if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]);
return ( return (
<Popover destroyOnHidden content={popContent} open={visibility}> <Spin spinning={loading}>
<Spin spinning={loading}> {record[type] ? (
{record[type] ? ( <div style={{ cursor: "pointer" }}>
<div style={{ cursor: "pointer" }}> <span>{`${theEmployee?.first_name || ""} ${theEmployee?.last_name || ""}`}</span>
<span>{`${theEmployee?.first_name || ""} ${theEmployee?.last_name || ""}`}</span> <DeleteFilled style={iconStyle} onClick={() => handleRemove(type)} />
<DeleteFilled style={iconStyle} onClick={() => handleRemove(type)} /> </div>
</div> ) : (
) : ( <Popover destroyOnHidden content={popContent} open={visibility} trigger="click">
<PlusCircleFilled <PlusCircleFilled
style={{ ...iconStyle, cursor: "pointer" }} style={{ ...iconStyle, cursor: "pointer" }}
className="muted-button" className="muted-button"
onClick={() => { onClick={() => {
setAssignment({ operation: type }); setAssignment({ operation: type, employeeid: null });
setVisibility(true); setVisibility(true);
}} }}
/> />
)} </Popover>
</Spin> )}
</Popover> </Spin>
); );
} }

View File

@@ -453,10 +453,10 @@ export function ProductionListConfigManager({
}} }}
onSelect={handleSelect} onSelect={handleSelect}
placeholder={t("production.labels.selectview")} placeholder={t("production.labels.selectview")}
optionLabelProp="label"
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
value={activeView} value={activeView}
disabled={open || isAddingNewProfile} // Disable the Select box when the popover is open or adding a new profile disabled={open || isAddingNewProfile} // Disable the Select box when the popover is open or adding a new profile
optionLabelProp="label"
> >
{bodyshop?.production_config && {bodyshop?.production_config &&
bodyshop.production_config bodyshop.production_config

View File

@@ -1,15 +1,19 @@
import { SyncOutlined } from "@ant-design/icons"; import { HolderOutlined, SyncOutlined } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout"; import { PageHeader } from "@ant-design/pro-layout";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd"; import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
import _ from "lodash"; import _ from "lodash";
import { useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import ReactDragListView from "react-drag-listview"; import { closestCenter, DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { arrayMove, horizontalListSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { selectDarkMode } from "../../redux/application/application.selectors.js";
import Prompt from "../../utils/prompt.js"; import Prompt from "../../utils/prompt.js";
import AlertComponent from "../alert/alert.component.jsx"; import AlertComponent from "../alert/alert.component.jsx";
import ProductionListColumnsAdd from "../production-list-columns/production-list-columns.add.component"; import ProductionListColumnsAdd from "../production-list-columns/production-list-columns.add.component";
@@ -23,12 +27,81 @@ import { logImEXEvent } from "../../firebase/firebase.utils.js";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
technician: selectTechnician, technician: selectTechnician,
currentUser: selectCurrentUser currentUser: selectCurrentUser,
isDarkMode: selectDarkMode
}); });
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser }) { // Draggable header cell component - combines drag and resize
function DraggableHeaderCell(props) {
const { children, columnKey, onResize, width, ...restProps } = props;
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: columnKey,
disabled: !columnKey
});
const style = {
...restProps.style,
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.3 : 1,
userSelect: "none",
textAlign: "left"
};
// If no columnKey, render as regular header
if (!columnKey) {
return <ResizeableTitle {...props} />;
}
// Only apply drag listeners to elements with data-drag-handle attribute
const filteredListeners = listeners
? {
onPointerDown: (e) => {
// Only trigger drag if clicking on the drag handle
if (e.target.closest('[data-drag-handle="true"]')) {
listeners.onPointerDown?.(e);
}
}
}
: {};
// Combine drag functionality with resize
return (
<ResizeableTitle
{...restProps}
ref={setNodeRef}
style={style}
onResize={onResize}
width={width}
dragAttributes={attributes}
dragListeners={filteredListeners}
>
{children}
</ResizeableTitle>
);
}
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser, isDarkMode }) {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
// NEW: smoother resize
const [isResizing, setIsResizing] = useState(false);
const resizeRafRef = useRef(null);
const pendingResizeRef = useRef(null);
const [activeId, setActiveId] = useState(null);
const MIN_COL_WIDTH = 20;
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 1
}
})
);
const { const {
treatments: { Production_List_Status_Colors, Enhanced_Payroll } treatments: { Production_List_Status_Colors, Enhanced_Payroll }
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
@@ -36,8 +109,10 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
names: ["Production_List_Status_Colors", "Enhanced_Payroll"], names: ["Production_List_Status_Colors", "Enhanced_Payroll"],
splitKey: bodyshop.imexshopid splitKey: bodyshop.imexshopid
}); });
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email); const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
const defaultView = assoc?.default_prod_list_view; const defaultView = assoc?.default_prod_list_view;
const initialStateRef = useRef( const initialStateRef = useRef(
(bodyshop.production_config && (bodyshop.production_config &&
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) || bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
@@ -46,6 +121,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
filteredInfo: { text: "" } filteredInfo: { text: "" }
} }
); );
const initialColumnsRef = useRef( const initialColumnsRef = useRef(
(initialStateRef.current && (initialStateRef.current &&
bodyshop?.production_config bodyshop?.production_config
@@ -66,14 +142,36 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
})) || })) ||
[] []
); );
const [state, setState] = useState(initialStateRef.current); const [state, setState] = useState(initialStateRef.current);
const [columns, setColumns] = useState(initialColumnsRef.current); const [columns, setColumns] = useState(initialColumnsRef.current);
const scrollX = useMemo(() => {
// keep scroll width aligned with the actual column widths so AntD doesn't clamp at a fixed floor
const sum = columns.reduce((acc, c) => acc + (c.width ?? 100), 0);
return Math.max(sum, 1);
}, [columns]);
const { t } = useTranslation(); const { t } = useTranslation();
const matchingColumnConfig = useMemo(() => { const matchingColumnConfig = useMemo(() => {
return bodyshop?.production_config?.find((p) => p.name === defaultView); return bodyshop?.production_config?.find((p) => p.name === defaultView);
}, [bodyshop.production_config, defaultView]); }, [bodyshop.production_config, defaultView]);
// NEW: cleanup RAF on unmount
useEffect(() => { useEffect(() => {
return () => {
if (resizeRafRef.current) cancelAnimationFrame(resizeRafRef.current);
};
}, []);
useEffect(() => {
// NEW: while resizing, dont regenerate columns
if (isResizing) return;
// NEW: bail early BEFORE expensive ProductionListColumns(...) call
if (!_.isEqual(initialColumnsRef.current, columns)) return;
const newColumns = const newColumns =
matchingColumnConfig?.columns.columnKeys.map((k) => { matchingColumnConfig?.columns.columnKeys.map((k) => {
return { return {
@@ -89,10 +187,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
width: k.width ?? 100 width: k.width ?? 100
}; };
}) || []; }) || [];
// Only update columns if they haven't been manually changed by the user
if (_.isEqual(initialColumnsRef.current, columns)) { setColumns(newColumns);
setColumns(newColumns);
}
}, [ }, [
matchingColumnConfig, matchingColumnConfig,
bodyshop, bodyshop,
@@ -102,7 +198,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
Production_List_Status_Colors, Production_List_Status_Colors,
refetch, refetch,
state, state,
columns columns,
isResizing
]); ]);
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
@@ -118,17 +215,30 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
logImEXEvent("production_list_sort_filter", { pagination, filters, sorter }); logImEXEvent("production_list_sort_filter", { pagination, filters, sorter });
}; };
const onDragEnd = (fromIndex, toIndex) => { const onDragStart = ({ active }) => {
if (fromIndex === toIndex) return; setActiveId(active.id);
const columnsCopy = [...columns]; };
const [movedItem] = columnsCopy.splice(fromIndex, 1);
columnsCopy.splice(toIndex, 0, movedItem); const onDragEnd = ({ active, over }) => {
if (!_.isEqual(columnsCopy, columns)) { setActiveId(null);
setColumns(columnsCopy); if (!over || active.id === over.id) return;
setHasUnsavedChanges(true);
const oldIndex = columns.findIndex((col) => col.key === active.id);
const newIndex = columns.findIndex((col) => col.key === over.id);
if (oldIndex !== -1 && newIndex !== -1) {
const newColumns = arrayMove(columns, oldIndex, newIndex);
if (!_.isEqual(newColumns, columns)) {
setColumns(newColumns);
setHasUnsavedChanges(true);
}
} }
}; };
const onDragCancel = () => {
setActiveId(null);
};
const removeColumn = (e) => { const removeColumn = (e) => {
const { key } = e; const { key } = e;
const newColumns = columns.filter((i) => i.key !== key); const newColumns = columns.filter((i) => i.key !== key);
@@ -139,19 +249,55 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
logImEXEvent("production_list_remove_column", { key }); logImEXEvent("production_list_remove_column", { key });
}; };
const handleResize = // NEW: commit widths via rAF (less jank)
(index) => const applyColumnWidth = useCallback((columnKey, width) => {
(e, { size }) => { const nextWidth = Math.max(MIN_COL_WIDTH, Math.round(width));
const nextColumns = [...columns]; setColumns((prev) => {
nextColumns[index] = { const idx = prev.findIndex((c) => c.key === columnKey);
...nextColumns[index], if (idx === -1) return prev;
width: size.width
}; const currentWidth = prev[idx].width ?? 100;
if (!_.isEqual(nextColumns, columns)) { if (currentWidth === nextWidth) return prev;
setColumns(nextColumns);
const next = prev.slice();
next[idx] = { ...next[idx], width: nextWidth };
return next;
});
}, []);
const handleResize = useCallback(
(columnKey) =>
(e, { size }) => {
pendingResizeRef.current = { columnKey, width: size.width };
if (resizeRafRef.current) return;
resizeRafRef.current = requestAnimationFrame(() => {
resizeRafRef.current = null;
const pending = pendingResizeRef.current;
if (!pending) return;
applyColumnWidth(pending.columnKey, pending.width);
});
},
[applyColumnWidth]
);
const handleResizeStart = useCallback(() => {
setIsResizing(true);
}, []);
const handleResizeStop = useCallback(
(columnKey) =>
(e, { size }) => {
setIsResizing(false);
// Ensure final width is committed
applyColumnWidth(columnKey, size.width);
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
} logImEXEvent("production_list_resize_column", { key: columnKey, width: size.width });
}; },
[applyColumnWidth]
);
const addColumn = (newColumn) => { const addColumn = (newColumn) => {
const updatedColumns = [...columns, newColumn]; const updatedColumns = [...columns, newColumn];
@@ -163,19 +309,53 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
}; };
const headerItem = (col) => { const headerItem = (col) => {
const menu = { const menu = { onClick: removeColumn, items: [{ key: col.key, label: t("production.actions.removecolumn") }] };
onClick: removeColumn,
items: [
{
key: col.key,
label: t("production.actions.removecolumn")
}
]
};
return ( return (
<Dropdown className="prod-header-dropdown" menu={menu} trigger={["contextMenu"]}> <div
<span>{col.title}</span> style={{
</Dropdown> display: "flex",
alignItems: "left",
width: "100%",
userSelect: "none",
minWidth: 0 // critical: allow the flex row to shrink
}}
>
<span
className="drag-handle-trigger"
data-drag-handle="true"
style={{
marginRight: 8,
color: "#999",
cursor: "grab",
padding: 4,
display: "inline-flex",
alignItems: "left",
userSelect: "none",
flex: "0 0 auto"
}}
title="Drag to reorder column"
>
<HolderOutlined />
</span>
<Dropdown className="prod-header-dropdown" menu={menu} trigger={["contextMenu"]}>
<span
style={{
flex: "1 1 auto",
minWidth: 0, // critical: allow text to shrink
overflow: "hidden", // clip
textOverflow: "ellipsis", // show …
whiteSpace: "nowrap", // keep single line
cursor: "default",
userSelect: "none",
display: "block"
}}
>
{col.title}
</span>
</Dropdown>
</div>
); );
}; };
@@ -274,6 +454,9 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
onSave={() => { onSave={() => {
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
initialStateRef.current = state; initialStateRef.current = state;
// NEW: after saving, treat current columns as the baseline
initialColumnsRef.current = columns;
}} }}
/> />
<Input <Input
@@ -286,60 +469,104 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
} }
/> />
<ProductionListDetail jobs={dataSource} /> <ProductionListDetail jobs={dataSource} />
<ReactDragListView.DragColumn onDragEnd={onDragEnd} nodeSelector="th" handleSelector=".prod-header-dropdown"> <DndContext
<Table sensors={sensors}
sticky onDragStart={onDragStart}
pagination={false} onDragEnd={onDragEnd}
size="small" onDragCancel={onDragCancel}
{...(Production_List_Status_Colors.treatment === "on" && { collisionDetection={closestCenter}
onRow: (record, index) => { modifiers={[restrictToHorizontalAxis]}
if (!bodyshop.md_ro_statuses.production_colors) return null; >
const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status); <SortableContext items={columns.map((col) => col.key)} strategy={horizontalListSortingStrategy}>
if (!color) { <Table
if (index % 2 === 0) sticky
tableLayout="fixed"
className="prod-list-table"
pagination={false}
size="small"
{...(Production_List_Status_Colors.treatment === "on" &&
!isResizing && {
onRow: (record, index) => {
if (!bodyshop.md_ro_statuses.production_colors) return null;
const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status);
if (!color) {
if (index % 2 === 0)
return {
style: {
backgroundColor: "var(--table-row-even-bg)"
}
};
return null;
}
return { return {
className: "rowWithColor",
style: { style: {
backgroundColor: "var(--table-row-even-bg)" "--bgColor": color.color
? `rgba(${color.color.r},${color.color.g},${color.color.b},${color.color.a || 1})`
: "var(--status-row-bg-fallback)"
} }
}; };
return null;
}
return {
className: "rowWithColor",
style: {
"--bgColor": color.color
? `rgba(${color.color.r},${color.color.g},${color.color.b},${color.color.a || 1})`
: "var(--status-row-bg-fallback)"
} }
})}
components={{
header: {
cell: DraggableHeaderCell
}
}}
columns={columns.map((c) => {
return {
...c,
filteredValue: state.filteredInfo[c.key] || null,
sortOrder: state.sortedInfo.columnKey === c.key && state.sortedInfo.order,
title: headerItem(c),
ellipsis: true,
width: c.width ?? 100,
onHeaderCell: (column) => ({
columnKey: column.key,
width: column.width,
onResize: handleResize(column.key),
onResizeStart: handleResizeStart,
onResizeStop: handleResizeStop(column.key)
})
}; };
} })}
})} rowKey="id"
components={{ loading={loading}
header: { dataSource={dataSource}
cell: ResizeableTitle scroll={{ x: scrollX }}
} onChange={handleTableChange}
}} />
columns={columns.map((c, index) => { </SortableContext>
return {
...c, <DragOverlay dropAnimation={null}>
filteredValue: state.filteredInfo[c.key] || null, {activeId ? (
sortOrder: state.sortedInfo.columnKey === c.key && state.sortedInfo.order, <div
title: headerItem(c), style={{
ellipsis: true, backgroundColor: isDarkMode ? "#141414" : "white",
width: c.width ?? 100, color: isDarkMode ? "white" : "#000",
onHeaderCell: (column) => ({ border: `2px solid ${isDarkMode ? "#177ddc" : "#1890ff"}`,
width: column.width, borderRadius: "4px",
onResize: handleResize(index) padding: "12px 16px",
}) boxShadow: "0 4px 12px rgba(0, 0, 0, 0.25)",
}; cursor: "grabbing",
})} display: "flex",
rowKey="id" alignItems: "center",
loading={loading} fontWeight: 500,
dataSource={dataSource} minWidth: "120px"
scroll={{ x: 1000 }} }}
onChange={handleTableChange} >
/> <HolderOutlined style={{ marginRight: "8px", color: isDarkMode ? "white" : "#000", fontSize: "16px" }} />
</ReactDragListView.DragColumn> <span>
{(() => {
const col = columns.find((c) => c.key === activeId);
const title = typeof col?.title === "string" ? col.title : col?.dataIndex || col?.key || "Column";
return title;
})()}
</span>
</div>
) : null}
</DragOverlay>
</DndContext>
</div> </div>
); );
} }

View File

@@ -1,28 +1,37 @@
import { forwardRef } from "react";
import { Resizable } from "react-resizable"; import { Resizable } from "react-resizable";
import "react-resizable/css/styles.css";
export default function ResizableComponent(props) { const ResizableComponent = forwardRef((props, ref) => {
const { onResize, width, ...restProps } = props; const { onResize, onResizeStart, onResizeStop, width, dragAttributes, dragListeners, ...restProps } = props;
if (!width) { if (!width) {
return <th {...restProps} />; return <th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} />;
} }
return ( return (
<Resizable <Resizable
width={width || 200} width={width}
height={0} height={0}
onResize={onResize} onResize={onResize}
onResizeStart={onResizeStart}
onResizeStop={onResizeStop}
draggableOpts={{ enableUserSelectHack: false }} draggableOpts={{ enableUserSelectHack: false }}
handle={ resizeHandles={["e"]}
axis="x"
handle={(axis, handleRef) => (
<span <span
className="react-resizable-handle" ref={handleRef}
onClick={(e) => { className={`react-resizable-handle react-resizable-handle-${axis}`}
e.stopPropagation(); onClick={(e) => e.stopPropagation()}
}}
/> />
} )}
> >
<th {...restProps} /> <th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} />
</Resizable> </Resizable>
); );
} });
ResizableComponent.displayName = "ResizableComponent";
export default ResizableComponent;

View File

@@ -158,20 +158,28 @@ export function ScheduleJobModalComponent({
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow> <LayoutFormRow grow>
<Form.Item name="color" label={t("appointments.fields.color")}> <Form.Item name="color" label={t("appointments.fields.color")}>
<Select allowClear> <Select
{bodyshop.appt_colors && allowClear
bodyshop.appt_colors.map((color) => ( options={
<Select.Option style={{ color: color.color.hex }} key={color.color.hex} value={color.color.hex}> bodyshop.appt_colors &&
{color.label} bodyshop.appt_colors.map((color) => ({
</Select.Option> value: color.color.hex,
))} label: color.label
</Select> }))
}
/>
</Form.Item> </Form.Item>
<Form.Item name={"alt_transport"} label={t("jobs.fields.alt_transport")}> <Form.Item name={"alt_transport"} label={t("jobs.fields.alt_transport")}>
<Select allowClear> <Select
{bodyshop.appt_alt_transport && allowClear
bodyshop.appt_alt_transport.map((alt) => <Select.Option key={alt}>{alt}</Select.Option>)} options={
</Select> bodyshop.appt_alt_transport &&
bodyshop.appt_alt_transport.map((alt) => ({
value: alt,
label: alt
}))
}
/>
</Form.Item> </Form.Item>
<Form.Item name={"note"} label={t("appointments.fields.note")}> <Form.Item name={"note"} label={t("appointments.fields.note")}>
<Input /> <Input />

View File

@@ -120,13 +120,12 @@ export function ScheduleManualEvent({ bodyshop, event }) {
<FormDateTimePickerComponent /> <FormDateTimePickerComponent />
</Form.Item> </Form.Item>
<Form.Item label={t("appointments.fields.color")} name="color"> <Form.Item label={t("appointments.fields.color")} name="color">
<Select> <Select
{bodyshop.appt_colors.map((col, idx) => ( options={bodyshop.appt_colors.map((col) => ({
<Select.Option key={idx} value={col.color.hex}> value: col.color.hex,
{col.label} label: col.label
</Select.Option> }))}
))} />
</Select>
</Form.Item> </Form.Item>
<Space wrap> <Space wrap>

View File

@@ -325,22 +325,20 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
} }
]} ]}
> >
<Select> <Select
<Select.Option key={"shift"} value="timetickets.labels.shift"> options={[
{t("timetickets.labels.shift")} { value: "timetickets.labels.shift", label: t("timetickets.labels.shift") },
</Select.Option> ...(bodyshop.cdk_dealerid ||
bodyshop.pbs_serialnumber ||
{bodyshop.cdk_dealerid || bodyshop.rr_dealerid ||
bodyshop.pbs_serialnumber || Enhanced_Payroll.treatment === "on"
bodyshop.rr_dealerid || ? CiecaSelect(false, true)
Enhanced_Payroll.treatment === "on" : bodyshop.md_responsibility_centers.costs.map((c) => ({
? CiecaSelect(false, true) value: c.name,
: bodyshop.md_responsibility_centers.costs.map((c) => ( label: c.name
<Select.Option key={c.name} value={c.name}> })))
{c.name} ]}
</Select.Option> />
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("employees.fields.rate")} label={t("employees.fields.rate")}

View File

@@ -1039,22 +1039,25 @@ export function ShopInfoGeneral({ form }) {
key={`${index}mod_lbr_ty`} key={`${index}mod_lbr_ty`}
name={[field.name, "mod_lbr_ty"]} name={[field.name, "mod_lbr_ty"]}
> >
<Select allowClear> <Select
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option> allowClear
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option> options={[
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option> { value: "LAA", label: t("joblines.fields.lbr_types.LAA") },
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option> { value: "LAB", label: t("joblines.fields.lbr_types.LAB") },
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option> { value: "LAD", label: t("joblines.fields.lbr_types.LAD") },
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option> { value: "LAE", label: t("joblines.fields.lbr_types.LAE") },
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option> { value: "LAF", label: t("joblines.fields.lbr_types.LAF") },
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option> { value: "LAG", label: t("joblines.fields.lbr_types.LAG") },
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option> { value: "LAM", label: t("joblines.fields.lbr_types.LAM") },
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option> { value: "LAR", label: t("joblines.fields.lbr_types.LAR") },
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option> { value: "LAS", label: t("joblines.fields.lbr_types.LAS") },
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option> { value: "LAU", label: t("joblines.fields.lbr_types.LAU") },
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option> { value: "LA1", label: t("joblines.fields.lbr_types.LA1") },
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option> { value: "LA2", label: t("joblines.fields.lbr_types.LA2") },
</Select> { value: "LA3", label: t("joblines.fields.lbr_types.LA3") },
{ value: "LA4", label: t("joblines.fields.lbr_types.LA4") }
]}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("joblines.fields.mod_lb_hrs")} label={t("joblines.fields.mod_lb_hrs")}
@@ -1068,17 +1071,20 @@ export function ShopInfoGeneral({ form }) {
key={`${index}part_type`} key={`${index}part_type`}
name={[field.name, "part_type"]} name={[field.name, "part_type"]}
> >
<Select allowClear> <Select
<Select.Option value="PAA">{t("joblines.fields.part_types.PAA")}</Select.Option> allowClear
<Select.Option value="PAC">{t("joblines.fields.part_types.PAC")}</Select.Option> options={[
<Select.Option value="PAE">{t("joblines.fields.part_types.PAE")}</Select.Option> { value: "PAA", label: t("joblines.fields.part_types.PAA") },
<Select.Option value="PAL">{t("joblines.fields.part_types.PAL")}</Select.Option> { value: "PAC", label: t("joblines.fields.part_types.PAC") },
<Select.Option value="PAM">{t("joblines.fields.part_types.PAM")}</Select.Option> { value: "PAE", label: t("joblines.fields.part_types.PAE") },
<Select.Option value="PAN">{t("joblines.fields.part_types.PAN")}</Select.Option> { value: "PAL", label: t("joblines.fields.part_types.PAL") },
<Select.Option value="PAO">{t("joblines.fields.part_types.PAO")}</Select.Option> { value: "PAM", label: t("joblines.fields.part_types.PAM") },
<Select.Option value="PAR">{t("joblines.fields.part_types.PAR")}</Select.Option> { value: "PAN", label: t("joblines.fields.part_types.PAN") },
<Select.Option value="PAS">{t("joblines.fields.part_types.PAS")}</Select.Option> { value: "PAO", label: t("joblines.fields.part_types.PAO") },
</Select> { value: "PAR", label: t("joblines.fields.part_types.PAR") },
{ value: "PAS", label: t("joblines.fields.part_types.PAS") }
]}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("joblines.fields.oem_partno")} label={t("joblines.fields.oem_partno")}

View File

@@ -51,13 +51,7 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
} }
]} ]}
> >
<Select> <Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} />
{Object.keys(ConfigFormTypes).map((i) => (
<Select.Option key={i} value={i}>
{i}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("jobs.fields.intake.label")} label={t("jobs.fields.intake.label")}
@@ -156,13 +150,13 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
} }
]} ]}
> >
<Select mode="multiple"> <Select
{Object.keys(TemplateListGenerated).map((i) => ( mode="multiple"
<Select.Option key={TemplateListGenerated[i].key} value={TemplateListGenerated[i].key}> options={Object.keys(TemplateListGenerated).map((i) => ({
{TemplateListGenerated[i].title} value: TemplateListGenerated[i].key,
</Select.Option> label: TemplateListGenerated[i].title
))} }))}
</Select> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={["intakechecklist", "next_contact_hours"]} name={["intakechecklist", "next_contact_hours"]}
@@ -205,13 +199,7 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
} }
]} ]}
> >
<Select> <Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} />
{Object.keys(ConfigFormTypes).map((i) => (
<Select.Option key={i} value={i}>
{i}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -310,13 +298,13 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
} }
]} ]}
> >
<Select mode="multiple"> <Select
{Object.keys(TemplateListGenerated).map((i) => ( mode="multiple"
<Select.Option key={TemplateListGenerated[i].key} value={TemplateListGenerated[i].key}> options={Object.keys(TemplateListGenerated).map((i) => ({
{TemplateListGenerated[i].title} value: TemplateListGenerated[i].key,
</Select.Option> label: TemplateListGenerated[i].title
))} }))}
</Select> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={["deliverchecklist", "actual_delivery"]} name={["deliverchecklist", "actual_delivery"]}

View File

@@ -80,13 +80,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
} }
]} ]}
> >
<Select mode="multiple"> <Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={["md_ro_statuses", "pre_production_statuses"]} name={["md_ro_statuses", "pre_production_statuses"]}
@@ -99,13 +93,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
} }
]} ]}
> >
<Select mode="multiple"> <Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={["md_ro_statuses", "production_statuses"]} name={["md_ro_statuses", "production_statuses"]}
@@ -118,13 +106,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
} }
]} ]}
> >
<Select mode="multiple"> <Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={["md_ro_statuses", "post_production_statuses"]} name={["md_ro_statuses", "post_production_statuses"]}
@@ -137,13 +119,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
} }
]} ]}
> >
<Select mode="multiple"> <Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={["md_ro_statuses", "ready_statuses"]} name={["md_ro_statuses", "ready_statuses"]}
@@ -156,13 +132,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
} }
]} ]}
> >
<Select mode="multiple"> <Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={["md_ro_statuses", "additional_board_statuses"]} name={["md_ro_statuses", "additional_board_statuses"]}
@@ -175,13 +145,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
} }
]} ]}
> >
<Select mode="multiple"> <Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<LayoutFormRow noDivider> <LayoutFormRow noDivider>
<Form.Item <Form.Item
@@ -194,13 +158,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]} ]}
name={["md_ro_statuses", "default_scheduled"]} name={["md_ro_statuses", "default_scheduled"]}
> >
<Select> <Select options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.statuses.default_arrived")} label={t("bodyshop.fields.statuses.default_arrived")}
@@ -212,13 +170,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]} ]}
name={["md_ro_statuses", "default_arrived"]} name={["md_ro_statuses", "default_arrived"]}
> >
<Select> <Select options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.statuses.default_exported")} label={t("bodyshop.fields.statuses.default_exported")}
@@ -230,13 +182,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]} ]}
name={["md_ro_statuses", "default_exported"]} name={["md_ro_statuses", "default_exported"]}
> >
<Select> <Select options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.statuses.default_imported")} label={t("bodyshop.fields.statuses.default_imported")}
@@ -248,13 +194,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]} ]}
name={["md_ro_statuses", "default_imported"]} name={["md_ro_statuses", "default_imported"]}
> >
<Select> <Select options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.statuses.default_invoiced")} label={t("bodyshop.fields.statuses.default_invoiced")}
@@ -266,13 +206,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]} ]}
name={["md_ro_statuses", "default_invoiced"]} name={["md_ro_statuses", "default_invoiced"]}
> >
<Select> <Select options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.statuses.default_completed")} label={t("bodyshop.fields.statuses.default_completed")}
@@ -284,13 +218,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]} ]}
name={["md_ro_statuses", "default_completed"]} name={["md_ro_statuses", "default_completed"]}
> >
<Select> <Select options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.statuses.default_delivered")} label={t("bodyshop.fields.statuses.default_delivered")}
@@ -302,13 +230,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]} ]}
name={["md_ro_statuses", "default_delivered"]} name={["md_ro_statuses", "default_delivered"]}
> >
<Select> <Select options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.statuses.default_void")} label={t("bodyshop.fields.statuses.default_void")}
@@ -320,13 +242,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]} ]}
name={["md_ro_statuses", "default_void"]} name={["md_ro_statuses", "default_void"]}
> >
<Select> <Select options={options.map((item) => ({ value: item, label: item }))} />
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
{Production_List_Status_Colors.treatment === "on" && ( {Production_List_Status_Colors.treatment === "on" && (
@@ -352,13 +268,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
} }
]} ]}
> >
<Select> <Select options={productionStatus.map((item) => ({ value: item, label: item }))} />
{productionStatus.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<DeleteFilled <DeleteFilled
onClick={() => { onClick={() => {

View File

@@ -60,13 +60,13 @@ export default function ShopInfoSpeedPrint() {
} }
]} ]}
> >
<Select mode="multiple"> <Select
{Object.keys(TemplateListGenerated).map((key, idx) => ( mode="multiple"
<Select.Option key={idx} value={TemplateListGenerated[key].key}> options={Object.keys(TemplateListGenerated).map((key) => ({
{TemplateListGenerated[key].title} value: TemplateListGenerated[key].key,
</Select.Option> label: TemplateListGenerated[key].title
))} }))}
</Select> />
</Form.Item> </Form.Item>
<Space wrap> <Space wrap>

View File

@@ -43,85 +43,43 @@ export function ShopInfoIntellipay({ bodyshop, form }) {
label={t("bodyshop.fields.intellipay_config.payment_map.visa")} label={t("bodyshop.fields.intellipay_config.payment_map.visa")}
name={["intellipay_config", "payment_map", "visa"]} name={["intellipay_config", "payment_map", "visa"]}
> >
<Select showSearch> <Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.intellipay_config.payment_map.mast")} label={t("bodyshop.fields.intellipay_config.payment_map.mast")}
name={["intellipay_config", "payment_map", "mast"]} name={["intellipay_config", "payment_map", "mast"]}
> >
<Select showSearch> <Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.intellipay_config.payment_map.amex")} label={t("bodyshop.fields.intellipay_config.payment_map.amex")}
name={["intellipay_config", "payment_map", "amex"]} name={["intellipay_config", "payment_map", "amex"]}
> >
<Select showSearch> <Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.intellipay_config.payment_map.disc")} label={t("bodyshop.fields.intellipay_config.payment_map.disc")}
name={["intellipay_config", "payment_map", "disc"]} name={["intellipay_config", "payment_map", "disc"]}
> >
<Select showSearch> <Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.intellipay_config.payment_map.dnrs")} label={t("bodyshop.fields.intellipay_config.payment_map.dnrs")}
name={["intellipay_config", "payment_map", "dnrs"]} name={["intellipay_config", "payment_map", "dnrs"]}
> >
<Select showSearch> <Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.intellipay_config.payment_map.jcb")} label={t("bodyshop.fields.intellipay_config.payment_map.jcb")}
name={["intellipay_config", "payment_map", "jcb"]} name={["intellipay_config", "payment_map", "jcb"]}
> >
<Select showSearch> <Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.intellipay_config.payment_map.intr")} label={t("bodyshop.fields.intellipay_config.payment_map.intr")}
name={["intellipay_config", "payment_map", "intr"]} name={["intellipay_config", "payment_map", "intr"]}
> >
<Select showSearch> <Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
</> </>

View File

@@ -33,7 +33,8 @@ export function TaskListContainer({
currentUser, currentUser,
onlyMine, onlyMine,
parentJobId, parentJobId,
showRo = true showRo = true,
disableJobRefetch = false
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const notification = useNotification(); const notification = useNotification();
@@ -90,6 +91,10 @@ export function TaskListContainer({
refetchQueries: [Object.keys(query)[0]] refetchQueries: [Object.keys(query)[0]]
}; };
if (!disableJobRefetch) {
toggledTaskObject.refetchQueries.push("GET_JOB_BY_PK");
}
const toggledTask = await toggleTaskCompleted(toggledTaskObject); const toggledTask = await toggleTaskCompleted(toggledTaskObject);
if (!toggledTask.errors) { if (!toggledTask.errors) {
@@ -139,6 +144,10 @@ export function TaskListContainer({
refetchQueries: [Object.keys(query)[0]] refetchQueries: [Object.keys(query)[0]]
}; };
if (!disableJobRefetch) {
toggledTaskObject.refetchQueries.push("GET_JOB_BY_PK");
}
const toggledTask = await toggleTaskDeleted(toggledTaskObject); const toggledTask = await toggleTaskDeleted(toggledTaskObject);
if (!toggledTask.errors) { if (!toggledTask.errors) {

View File

@@ -57,21 +57,23 @@ export function TechClockInComponent({ form, bodyshop, technician }) {
} }
]} ]}
> >
<Select> <Select
{emps && options={
emps.rates.map((item) => ( emps &&
<Select.Option key={item.cost_center} value={item.cost_center}> emps.rates.map((item) => ({
{item.cost_center === "timetickets.labels.shift" value: item.cost_center,
label:
item.cost_center === "timetickets.labels.shift"
? t(item.cost_center) ? t(item.cost_center)
: bodyshop.cdk_dealerid || : bodyshop.cdk_dealerid ||
bodyshop.pbs_serialnumber || bodyshop.pbs_serialnumber ||
bodyshop.rr_dealerid || bodyshop.rr_dealerid ||
Enhanced_Payroll.treatment === "on" Enhanced_Payroll.treatment === "on"
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`) ? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
: item.cost_center} : item.cost_center
</Select.Option> }))
))} }
</Select> />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<Divider /> <Divider />

View File

@@ -201,22 +201,22 @@ export function TechClockOffButton({
} }
]} ]}
> >
<Select disabled={isShiftTicket}> <Select disabled={isShiftTicket}
{isShiftTicket ? ( options={
<Select.Option value="timetickets.labels.shift">{t("timetickets.labels.shift")}</Select.Option> isShiftTicket
) : ( ? [{ value: "timetickets.labels.shift", label: t("timetickets.labels.shift") }]
emps && : emps &&
emps.rates.map((item) => ( emps.rates.map((item) => ({
<Select.Option key={item.cost_center}> value: item.cost_center,
{item.cost_center === "timetickets.labels.shift" label:
? t(item.cost_center) item.cost_center === "timetickets.labels.shift"
: hasDmsKey ? t(item.cost_center)
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`) : hasDmsKey
: item.cost_center} ? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
</Select.Option> : item.cost_center
)) }))
)} }
</Select> />
</Form.Item> </Form.Item>
{isShiftTicket ? ( {isShiftTicket ? (
@@ -232,11 +232,12 @@ export function TechClockOffButton({
} }
]} ]}
> >
<Select> <Select
{bodyshop.md_ro_statuses.production_statuses.map((item) => ( options={bodyshop.md_ro_statuses.production_statuses.map((item) => ({
<Select.Option key={item}></Select.Option> value: item,
))} label: item
</Select> }))}
/>
</Form.Item> </Form.Item>
)} )}

View File

@@ -91,21 +91,22 @@ export function TimeTicketModalComponent({
value={value === "timetickets.labels.shift" ? t(value) : value} value={value === "timetickets.labels.shift" ? t(value) : value}
{...props} {...props}
disabled={value === "timetickets.labels.shift" || disabled} disabled={value === "timetickets.labels.shift" || disabled}
> options={
{emps && emps &&
emps.rates.map((item) => ( emps.rates.map((item) => ({
<Select.Option key={item.cost_center} value={item.cost_center}> value: item.cost_center,
{item.cost_center === "timetickets.labels.shift" label:
item.cost_center === "timetickets.labels.shift"
? t(item.cost_center) ? t(item.cost_center)
: bodyshop.cdk_dealerid || : bodyshop.cdk_dealerid ||
bodyshop.pbs_serialnumber || bodyshop.pbs_serialnumber ||
bodyshop.rr_dealerid || bodyshop.rr_dealerid ||
Enhanced_Payroll.treatment === "on" Enhanced_Payroll.treatment === "on"
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`) ? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
: item.cost_center} : item.cost_center
</Select.Option> }))
))} }
</Select> />
); );
const MemoInput = ({ value, ...props }) => ( const MemoInput = ({ value, ...props }) => (

View File

@@ -20,13 +20,15 @@ export function TimeTicketShiftFormComponent() {
} }
]} ]}
> >
<Select> <Select
<Select.Option value="timetickets.labels.amshift">{t("timetickets.labels.amshift")}</Select.Option> options={[
<Select.Option value="timetickets.labels.ambreak">{t("timetickets.labels.ambreak")}</Select.Option> { value: "timetickets.labels.amshift", label: t("timetickets.labels.amshift") },
<Select.Option value="timetickets.labels.lunch">{t("timetickets.labels.lunch")}</Select.Option> { value: "timetickets.labels.ambreak", label: t("timetickets.labels.ambreak") },
<Select.Option value="timetickets.labels.pmshift">{t("timetickets.labels.pmshift")}</Select.Option> { value: "timetickets.labels.lunch", label: t("timetickets.labels.lunch") },
<Select.Option value="timetickets.labels.pmbreak">{t("timetickets.labels.pmbreak")}</Select.Option> { value: "timetickets.labels.pmshift", label: t("timetickets.labels.pmshift") },
</Select> { value: "timetickets.labels.pmbreak", label: t("timetickets.labels.pmbreak") }
]}
/>
</Form.Item> </Form.Item>
</div> </div>
); );

View File

@@ -9,8 +9,6 @@ import {
} from "../../graphql/vehicles.queries"; } from "../../graphql/vehicles.queries";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
const { Option } = Select;
const VehicleSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => { const VehicleSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_VEHICLES_FOR_AUTOCOMPLETE); const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_VEHICLES_FOR_AUTOCOMPLETE);
@@ -18,9 +16,10 @@ const VehicleSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE
); );
const executeSearch = (v) => { const executeSearch = (variables) => {
if (v && v.variables?.search !== "" && v.variables.search.length >= 2) callSearch({ variables: v.variables }); if (variables?.search !== "" && variables?.search?.length >= 2) callSearch({ variables });
}; };
const debouncedExecuteSearch = _.debounce(executeSearch, 500); const debouncedExecuteSearch = _.debounce(executeSearch, 500);
const handleSearch = (value) => { const handleSearch = (value) => {
@@ -72,15 +71,12 @@ const VehicleSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
onSelect={handleSelect} onSelect={handleSelect}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />} notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
onBlur={onBlur} onBlur={onBlur}
> options={theOptions?.map((o) => ({
{theOptions key: o.id,
? theOptions.map((o) => ( value: o.id,
<Option key={o.id} value={o.id}> label: `${o.v_vin || ""} ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${o.v_model_desc || ""} `
{`${o.v_vin || ""} ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${o.v_model_desc || ""} `} }))}
</Option> />
))
: null}
</Select>
{idLoading || loading ? <LoadingOutlined /> : null} {idLoading || loading ? <LoadingOutlined /> : null}
{error ? <AlertComponent title={error.message} type="error" /> : null} {error ? <AlertComponent title={error.message} type="error" /> : null}
{idError ? <AlertComponent title={idError.message} type="error" /> : null} {idError ? <AlertComponent title={idError.message} type="error" /> : null}

View File

@@ -3,8 +3,6 @@ import { Select, Space, Tag } from "antd";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import PhoneNumberFormatter from "../../utils/PhoneFormatter";
const { Option } = Select;
// To be used as a form element only. // To be used as a form element only.
const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone, ref }) => { const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone, ref }) => {
@@ -21,10 +19,57 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
? options.filter((o) => o.favorite.filter((f) => f.toLowerCase() === preferredMake.toLowerCase()).length > 0) ? options.filter((o) => o.favorite.filter((f) => f.toLowerCase() === preferredMake.toLowerCase()).length > 0)
: []; : [];
const formatOption = (o, isFavorite = false) => ({
key: isFavorite ? `favorite-${o.id}` : o.id,
value: o.id,
name: o.name,
discount: o.discount,
label: (
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "nowrap",
width: "100%"
}}
>
<div
style={{
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{o.name}
</div>
<Space style={{ marginLeft: "1rem" }}>
{isFavorite && <HeartOutlined style={{ color: "red" }} />}
{!isFavorite &&
o.tags?.map((tag, idx) => (
<Tag key={idx} style={{ marginLeft: "0.5rem" }}>
{tag}
</Tag>
))}
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
</div>
)
});
const allOptions = [
...(favorites?.map((o) => formatOption(o, true)) || []),
...(options?.map((o) => formatOption(o, false)) || [])
];
return ( return (
<Select <Select
ref={ref} ref={ref}
showSearch showSearch={{
optionFilterProp: "name"
}}
value={option} value={option}
style={{ style={{
width: "100%" width: "100%"
@@ -59,76 +104,11 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
}} }}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
onChange={setOption} onChange={setOption}
optionFilterProp="name"
onSelect={onSelect} onSelect={onSelect}
disabled={disabled || false} disabled={disabled || false}
optionLabelProp="name" optionLabelProp="name"
> options={allOptions}
{favorites && />
favorites.map((o) => (
<Option key={`favorite-${o.id}`} value={o.id} name={o.name} discount={o.discount}>
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "nowrap",
width: "100%"
}}
>
<div
style={{
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{o.name}
</div>
<Space style={{ marginLeft: "1rem" }}>
<HeartOutlined style={{ color: "red" }} />
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
</div>
</Option>
))}
{options &&
options.map((o) => (
<Option key={o.id} value={o.id} name={o.name} discount={o.discount}>
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "nowrap",
width: "100%"
}}
>
<div
style={{
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{o.name}
</div>
<Space style={{ marginLeft: "1rem" }}>
{o.tags?.map((tag, idx) => (
<Tag key={idx} style={{ marginLeft: "0.5rem" }}>
{tag}
</Tag>
))}
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
</div>
</Option>
))}
</Select>
); );
}; };
export default VendorSearchSelect; export default VendorSearchSelect;

View File

@@ -470,6 +470,9 @@ export const GET_JOB_BY_PK = gql`
clm_total clm_total
comment comment
converted converted
dms_id
dms_customer_id
dms_advisor_id
csiinvites { csiinvites {
completedon completedon
id id
@@ -491,6 +494,9 @@ export const GET_JOB_BY_PK = gql`
ded_status ded_status
deliverchecklist deliverchecklist
depreciation_taxes depreciation_taxes
dms_id
dms_advisor_id
dms_customer_id
driveable driveable
employee_body employee_body
employee_body_rel { employee_body_rel {
@@ -1995,6 +2001,9 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
qb_multiple_payers qb_multiple_payers
lbr_adjustments lbr_adjustments
ownr_ea ownr_ea
dms_id
dms_customer_id
dms_advisor_id
payments { payments {
amount amount
created_at created_at
@@ -2216,6 +2225,9 @@ export const QUERY_JOB_EXPORT_DMS = gql`
plate_no plate_no
plate_st plate_st
ownr_co_nm ownr_co_nm
dms_id
dms_customer_id
dms_advisor_id
} }
} }
`; `;

View File

@@ -119,12 +119,13 @@ export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
setLogLevel(value); setLogLevel(value);
socket.emit("set-log-level", value); socket.emit("set-log-level", value);
}} }}
> options={[
<Select.Option key="DEBUG">DEBUG</Select.Option> { key: "DEBUG", value: "DEBUG", label: "DEBUG" },
<Select.Option key="INFO">INFO</Select.Option> { key: "INFO", value: "INFO", label: "INFO" },
<Select.Option key="WARN">WARN</Select.Option> { key: "WARN", value: "WARN", label: "WARN" },
<Select.Option key="ERROR">ERROR</Select.Option> { key: "ERROR", value: "ERROR", label: "ERROR" }
</Select> ]}
/>
<Button onClick={() => setLogs([])}>Clear Logs</Button> <Button onClick={() => setLogs([])}>Clear Logs</Button>
<Button <Button
onClick={() => { onClick={() => {

View File

@@ -426,6 +426,24 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
if (data.jobs_by_pk?.date_exported) return <Result status="warning" title={t("dms.errors.alreadyexported")} />; if (data.jobs_by_pk?.date_exported) return <Result status="warning" title={t("dms.errors.alreadyexported")} />;
// Check if Reynolds mode requires early RO
const hasEarlyRO = !!(data.jobs_by_pk?.dms_id && data.jobs_by_pk?.dms_customer_id && data.jobs_by_pk?.dms_advisor_id);
if (isRrMode && !hasEarlyRO) {
return (
<Result
status="warning"
title={t("dms.errors.earlyrorequired")}
subTitle={t("dms.errors.earlyrorequired.message")}
extra={
<Link to={`/manage/jobs/${jobId}/admin`}>
<Button type="primary">{t("general.actions.gotoadmin")}</Button>
</Link>
}
/>
);
}
return ( return (
<div> <div>
<AlertComponent style={{ marginBottom: 10 }} title={bannerMessage} type="warning" showIcon closable /> <AlertComponent style={{ marginBottom: 10 }} title={bannerMessage} type="warning" showIcon closable />
@@ -486,6 +504,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
<DmsCustomerSelector <DmsCustomerSelector
jobid={jobId} jobid={jobId}
job={data?.jobs_by_pk}
bodyshop={bodyshop} bodyshop={bodyshop}
socket={activeSocket} socket={activeSocket}
mode={mode} mode={mode}
@@ -522,13 +541,14 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
setLogLevel(value); setLogLevel(value);
setActiveLogLevel(value); setActiveLogLevel(value);
}} }}
> options={[
<Select.Option key="SILLY">SILLY</Select.Option> { key: "SILLY", value: "SILLY", label: "SILLY" },
<Select.Option key="DEBUG">DEBUG</Select.Option> { key: "DEBUG", value: "DEBUG", label: "DEBUG" },
<Select.Option key="INFO">INFO</Select.Option> { key: "INFO", value: "INFO", label: "INFO" },
<Select.Option key="WARN">WARN</Select.Option> { key: "WARN", value: "WARN", label: "WARN" },
<Select.Option key="ERROR">ERROR</Select.Option> { key: "ERROR", value: "ERROR", label: "ERROR" }
</Select> ]}
/>
<Button onClick={() => setLogs([])}>Clear Logs</Button> <Button onClick={() => setLogs([])}>Clear Logs</Button>
<Button <Button
onClick={() => { onClick={() => {

View File

@@ -1,9 +1,12 @@
import { useQuery } from "@apollo/client/react"; import { useMutation, useQuery } from "@apollo/client/react";
import { Card, Col, Result, Row, Space, Typography } from "antd"; import { Button, Card, Col, Form, Input, Modal, Result, Row, Select, Space, Switch, Typography } from "antd";
import { useEffect } from "react"; import { useEffect, useState, useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { some } from "lodash";
import axios from "axios";
import AlertComponent from "../../components/alert/alert.component"; import AlertComponent from "../../components/alert/alert.component";
import JobCalculateTotals from "../../components/job-calculate-totals/job-calculate-totals.component"; import JobCalculateTotals from "../../components/job-calculate-totals/job-calculate-totals.component";
import ScoreboardAddButton from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component"; import ScoreboardAddButton from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component";
@@ -19,13 +22,26 @@ import JobsAdminRemoveAR from "../../components/jobs-admin-remove-ar/jobs-admin-
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import NotFound from "../../components/not-found/not-found.component"; import NotFound from "../../components/not-found/not-found.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries"; import RREarlyROModal from "../../components/dms-post-form/rr-early-ro-modal";
import { GET_JOB_BY_PK, CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions"; import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { createStructuredSelector } from "reselect";
import { useSocket } from "../../contexts/SocketIO/useSocket";
import { useNotification } from "../../contexts/Notifications/notificationContext";
import { DMS_MAP, getDmsMode } from "../../utils/dmsUtils";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)) setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
}); });
const colSpan = { const colSpan = {
@@ -39,14 +55,36 @@ const cardStyle = {
height: "100%" height: "100%"
}; };
export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) { export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop, insertAuditTrail }) {
const { jobId } = useParams(); const { jobId } = useParams();
const { loading, error, data } = useQuery(GET_JOB_BY_PK, { const { loading, error, data, refetch } = useQuery(GET_JOB_BY_PK, {
variables: { id: jobId }, variables: { id: jobId },
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only" nextFetchPolicy: "network-only"
}); });
const { t } = useTranslation(); const { t } = useTranslation();
const { socket } = useSocket(); // Extract socket from context
const notification = useNotification();
const [showEarlyROModal, setShowEarlyROModal] = useState(false);
const [showConvertModal, setShowConvertModal] = useState(false);
const [convertLoading, setConvertLoading] = useState(false);
const [form] = Form.useForm();
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
const allFormValues = Form.useWatch([], form);
// Get Fortellis treatment for proper DMS mode detection
const {
treatments: { Fortellis }
} = useTreatmentsWithConfig({
attributes: {},
names: ["Fortellis"],
splitKey: bodyshop?.imexshopid
});
// Check if bodyshop has Reynolds integration using the proper getDmsMode function
const dmsMode = getDmsMode(bodyshop, Fortellis.treatment);
const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
const job = data?.jobs_by_pk;
useEffect(() => { useEffect(() => {
setSelectedHeader("activejobs"); setSelectedHeader("activejobs");
document.title = t("titles.jobs-admin", { document.title = t("titles.jobs-admin", {
@@ -75,6 +113,55 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) {
]); ]);
}, [setBreadcrumbs, t, jobId, data, setSelectedHeader]); }, [setBreadcrumbs, t, jobId, data, setSelectedHeader]);
const handleEarlyROSuccess = (result) => {
notification.success({
title: t("jobs.successes.early_ro_created"),
description: `RO Number: ${result.roNumber || "N/A"}`
});
setShowEarlyROModal(false);
refetch?.();
};
const handleConvert = async ({ employee_csr, category, ...values }) => {
if (!job?.id) return;
setConvertLoading(true);
const res = await mutationConvertJob({
variables: {
jobId: job.id,
job: {
converted: true,
...(bodyshop?.enforce_conversion_csr ? { employee_csr } : {}),
...(bodyshop?.enforce_conversion_category ? { category } : {}),
...values
}
}
});
if (values.ca_gst_registrant) {
await axios.post("/job/totalsssu", {
id: job.id
});
}
if (!res.errors) {
refetch();
notification.success({
title: t("jobs.successes.converted")
});
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobconverted(res.data.update_jobs.returning[0].ro_number),
type: "jobconverted"
});
setShowConvertModal(false);
}
setConvertLoading(false);
};
const submitDisabled = useCallback(() => some(allFormValues, (v) => v === undefined), [allFormValues]);
if (loading) return <LoadingSpinner />; if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent title={error.message} type="error" />; if (error) return <AlertComponent title={error.message} type="error" />;
if (!data.jobs_by_pk) return <NotFound />; if (!data.jobs_by_pk) return <NotFound />;
@@ -99,6 +186,16 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) {
<JobsAdminUnvoid job={data ? data.jobs_by_pk : {}} /> <JobsAdminUnvoid job={data ? data.jobs_by_pk : {}} />
<JobsAdminStatus job={data ? data.jobs_by_pk : {}} /> <JobsAdminStatus job={data ? data.jobs_by_pk : {}} />
<JobsAdminRemoveAR job={data ? data.jobs_by_pk : {}} /> <JobsAdminRemoveAR job={data ? data.jobs_by_pk : {}} />
{isReynoldsMode && job?.converted && !job?.dms_id && !job?.dms_customer_id && !job?.dms_advisor_id && (
<Button className="ant-btn ant-btn-default" onClick={() => setShowEarlyROModal(true)}>
{t("jobs.actions.dms.createearlyro", "Create RR RO")}
</Button>
)}
{isReynoldsMode && !job?.converted && !job?.dms_id && (
<Button type="primary" danger onClick={() => setShowConvertModal(true)}>
{t("jobs.actions.convertwithoutearlyro", "Convert without Early RO")}
</Button>
)}
</Space> </Space>
</Card> </Card>
</Col> </Col>
@@ -124,8 +221,173 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) {
</Card> </Card>
</Col> </Col>
</Row> </Row>
{/* Early RO Modal */}
<RREarlyROModal
open={showEarlyROModal}
onClose={() => setShowEarlyROModal(false)}
onSuccess={handleEarlyROSuccess}
bodyshop={bodyshop}
socket={socket}
job={job}
/>
{/* Convert without Early RO Modal */}
<Modal
open={showConvertModal}
onCancel={() => setShowConvertModal(false)}
title={t("jobs.actions.convertwithoutearlyro", "Convert without Early RO")}
footer={null}
width={700}
destroyOnHidden
>
<Form
layout="vertical"
form={form}
onFinish={handleConvert}
initialValues={{
driveable: true,
towin: job?.towin,
ca_gst_registrant: job?.ca_gst_registrant,
employee_csr: job?.employee_csr,
category: job?.category,
referral_source: job?.referral_source,
referral_source_extra: job?.referral_source_extra ?? ""
}}
>
<Form.Item
name={["ins_co_nm"]}
label={t("jobs.fields.ins_co_nm")}
rules={[
{
required: true
}
]}
>
<Select showSearch>
{bodyshop?.md_ins_cos?.map((s, i) => (
<Select.Option key={i} value={s.name}>
{s.name}
</Select.Option>
))}
</Select>
</Form.Item>
{bodyshop?.enforce_class && (
<Form.Item
name={"class"}
label={t("jobs.fields.class")}
rules={[
{
required: bodyshop.enforce_class
}
]}
>
<Select>
{bodyshop?.md_classes?.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
)}
{bodyshop?.enforce_referral && (
<>
<Form.Item
name={"referral_source"}
label={t("jobs.fields.referralsource")}
rules={[
{
required: bodyshop.enforce_referral
}
]}
>
<Select>
{bodyshop?.md_referral_sources?.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
<Input />
</Form.Item>
</>
)}
{bodyshop?.enforce_conversion_csr && (
<Form.Item
name={"employee_csr"}
label={t(
InstanceRenderManager({
imex: "jobs.fields.employee_csr",
rome: "jobs.fields.employee_csr_writer"
})
)}
rules={[
{
required: bodyshop.enforce_conversion_csr
}
]}
>
<Select
showSearch={{
optionFilterProp: "children",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
style={{ width: 200 }}
>
{bodyshop?.employees
?.filter((emp) => emp.active)
?.map((emp) => (
<Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
</Form.Item>
)}
{bodyshop?.enforce_conversion_category && (
<Form.Item
name={"category"}
label={t("jobs.fields.category")}
rules={[
{
required: bodyshop.enforce_conversion_category
}
]}
>
<Select allowClear>
{bodyshop?.md_categories?.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
)}
{bodyshop?.region_config?.toLowerCase().startsWith("ca") && (
<Form.Item label={t("jobs.fields.ca_gst_registrant")} name="ca_gst_registrant" valuePropName="checked">
<Switch />
</Form.Item>
)}
<Form.Item label={t("jobs.fields.driveable")} name="driveable" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label={t("jobs.fields.towin")} name="towin" valuePropName="checked">
<Switch />
</Form.Item>
<Space wrap style={{ marginTop: 16 }}>
<Button disabled={submitDisabled()} type="primary" danger onClick={() => form.submit()} loading={convertLoading}>
{t("jobs.actions.convert")}
</Button>
<Button onClick={() => setShowConvertModal(false)}>{t("general.actions.close")}</Button>
</Space>
</Form>
</Modal>
</RbacWrapper> </RbacWrapper>
); );
} }
export default connect(null, mapDispatchToProps)(JobsCloseContainer); export default connect(mapStateToProps, mapDispatchToProps)(JobsCloseContainer);

View File

@@ -9,6 +9,7 @@ import {
Form, Form,
Input, Input,
InputNumber, InputNumber,
Modal,
Popconfirm, Popconfirm,
Row, Row,
Select, Select,
@@ -42,7 +43,7 @@ import { setModalContext } from "../../redux/modals/modals.actions.js";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js"; import { bodyshopHasDmsKey, DMS_MAP, getDmsMode } from "../../utils/dmsUtils.js";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -71,6 +72,11 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
const notification = useNotification(); const notification = useNotification();
const hasDMSKey = bodyshopHasDmsKey(bodyshop); const hasDMSKey = bodyshopHasDmsKey(bodyshop);
const dmsMode = getDmsMode(bodyshop, "off");
const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
const hasEarlyRO = !!(job?.dms_id && job?.dms_customer_id && job?.dms_advisor_id);
const canSendToDMS = !isReynoldsMode || hasEarlyRO;
const [showEarlyROModal, setShowEarlyROModal] = useState(false);
const { const {
treatments: { Qb_Multi_Ar, ClosingPeriod } treatments: { Qb_Multi_Ar, ClosingPeriod }
@@ -82,18 +88,18 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
const handleFinish = async ({ removefromproduction, ...values }) => { const handleFinish = async ({ removefromproduction, ...values }) => {
setLoading(true); setLoading(true);
// Validate that all joblines have valid IDs // Validate that all joblines have valid IDs
const joblinesWithIds = values.joblines.filter(jl => jl && jl.id); const joblinesWithIds = values.joblines.filter((jl) => jl && jl.id);
if (joblinesWithIds.length !== values.joblines.length) { if (joblinesWithIds.length !== values.joblines.length) {
notification.error({ notification.error({
title: t("jobs.errors.invalidjoblines"), title: t("jobs.errors.invalidjoblines"),
message: t("jobs.errors.missingjoblineids") description: t("jobs.errors.missingjoblineids")
}); });
setLoading(false); setLoading(false);
return; return;
} }
const result = await client.mutate({ const result = await client.mutate({
mutation: generateJobLinesUpdatesForInvoicing(values.joblines) mutation: generateJobLinesUpdatesForInvoicing(values.joblines)
}); });
@@ -208,9 +214,17 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
</Button> </Button>
</Popconfirm> </Popconfirm>
{bodyshopHasDmsKey(bodyshop) && ( {bodyshopHasDmsKey(bodyshop) && (
<Link to={`/manage/dms?jobId=${job.id}`}> <>
<Button disabled={job.date_exported || !jobRO}>{t("jobs.actions.sendtodms")}</Button> {canSendToDMS ? (
</Link> <Link to={`/manage/dms?jobId=${job.id}`}>
<Button disabled={job.date_exported || !jobRO}>{t("jobs.actions.sendtodms")}</Button>
</Link>
) : (
<Button disabled={job.date_exported || !jobRO} onClick={() => setShowEarlyROModal(true)}>
{t("jobs.actions.sendtodms")}
</Button>
)}
</>
)} )}
<Button <Button
onClick={() => { onClick={() => {
@@ -426,13 +440,15 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
} }
]} ]}
> >
<Select style={{ minWidth: "12rem" }} disabled={jobRO}> <Select
{bodyshop.md_ins_cos.map((s) => ( style={{ minWidth: "12rem" }}
<Select.Option key={s.name} value={s.name}> disabled={jobRO}
{s.name} options={bodyshop.md_ins_cos.map((s) => ({
</Select.Option> key: s.name,
))} value: s.name,
</Select> label: s.name
}))}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -510,7 +526,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
<Statistic <Statistic
title={t("jobs.labels.pimraryamountpayable")} title={t("jobs.labels.pimraryamountpayable")}
styles={{ styles={{
value: { content: {
color: discrep.getAmount() >= 0 ? "green" : "red" color: discrep.getAmount() >= 0 ? "green" : "red"
} }
}} }}
@@ -527,6 +543,30 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
<Divider /> <Divider />
<JobsCloseLines job={job} /> <JobsCloseLines job={job} />
</Form> </Form>
{/* Early RO Required Modal */}
<Modal
open={showEarlyROModal}
onCancel={() => setShowEarlyROModal(false)}
footer={null}
title={
<Space>
<Typography.Text type="warning" style={{ fontSize: "1.2em" }}>
</Typography.Text>
<span>{t("dms.errors.earlyrorequired")}</span>
</Space>
}
>
<Space orientation="vertical" size="large" style={{ width: "100%" }}>
<Typography.Paragraph>{t("dms.errors.earlyrorequired.message")}</Typography.Paragraph>
<Link to={`/manage/jobs/${job.id}/admin`}>
<Button type="primary" block onClick={() => setShowEarlyROModal(false)}>
{t("general.actions.gotoadmin")}
</Button>
</Link>
</Space>
</Modal>
</div> </div>
); );
} }

View File

@@ -23,10 +23,17 @@ export function TasksPageComponent({ bodyshop, currentUser, type }) {
relationshipType={"assigned_to"} relationshipType={"assigned_to"}
query={{ QUERY_MY_TASKS_PAGINATED }} query={{ QUERY_MY_TASKS_PAGINATED }}
titleTranslation={"tasks.titles.my_tasks"} titleTranslation={"tasks.titles.my_tasks"}
disableJobRefetch={true}
/> />
); );
case taskPageTypes.ALL_TASKS: case taskPageTypes.ALL_TASKS:
return <TaskListContainer query={{ QUERY_ALL_TASKS_PAGINATED }} titleTranslation={"tasks.titles.all_tasks"} />; return (
<TaskListContainer
query={{ QUERY_ALL_TASKS_PAGINATED }}
titleTranslation={"tasks.titles.all_tasks"}
disableJobRefetch={true}
/>
);
default: default:
return <></>; return <></>;
} }

View File

@@ -17,7 +17,10 @@ import "./tech.page.styles.scss";
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component.jsx"; import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component.jsx";
import { lazyDev } from "../../utils/lazyWithPreload.jsx"; import { lazyDev } from "../../utils/lazyWithPreload.jsx";
const TimeTicketModalContainer = lazyDev(() => import("../../components/time-ticket-modal/time-ticket-modal.container")); const NoteUpsertModal = lazyDev(() => import("../../components/note-upsert-modal/note-upsert-modal.container.jsx"));
const TimeTicketModalContainer = lazyDev(
() => import("../../components/time-ticket-modal/time-ticket-modal.container")
);
const EmailOverlayContainer = lazyDev(() => import("../../components/email-overlay/email-overlay.container.jsx")); const EmailOverlayContainer = lazyDev(() => import("../../components/email-overlay/email-overlay.container.jsx"));
const PrintCenterModalContainer = lazyDev( const PrintCenterModalContainer = lazyDev(
() => import("../../components/print-center-modal/print-center-modal.container") () => import("../../components/print-center-modal/print-center-modal.container")
@@ -34,7 +37,9 @@ const TimeTicketModalTask = lazyDev(
const TechAssignedProdJobs = lazyDev(() => import("../tech-assigned-prod-jobs/tech-assigned-prod-jobs.component")); const TechAssignedProdJobs = lazyDev(() => import("../tech-assigned-prod-jobs/tech-assigned-prod-jobs.component"));
const TechDispatchedParts = lazyDev(() => import("../tech-dispatched-parts/tech-dispatched-parts.page")); const TechDispatchedParts = lazyDev(() => import("../tech-dispatched-parts/tech-dispatched-parts.page"));
const TaskUpsertModalContainer = lazyDev(() => import("../../components/task-upsert-modal/task-upsert-modal.container")); const TaskUpsertModalContainer = lazyDev(
() => import("../../components/task-upsert-modal/task-upsert-modal.container")
);
const { Content } = Layout; const { Content } = Layout;
@@ -70,6 +75,8 @@ export function TechPage({ technician }) {
<TechHeader /> <TechHeader />
<TaskUpsertModalContainer /> <TaskUpsertModalContainer />
<NoteUpsertModal />
<Content className="tech-content-container"> <Content className="tech-content-container">
<ErrorBoundary> <ErrorBoundary>
<Suspense <Suspense

View File

@@ -48,6 +48,7 @@
"arrivedon": "Arrived on: ", "arrivedon": "Arrived on: ",
"arrivingjobs": "Arriving Jobs", "arrivingjobs": "Arriving Jobs",
"blocked": "Blocked", "blocked": "Blocked",
"bp": "B/P",
"cancelledappointment": "Canceled appointment for: ", "cancelledappointment": "Canceled appointment for: ",
"completingjobs": "Completing Jobs", "completingjobs": "Completing Jobs",
"dataconsistency": "<0>{{ro_number}}</0> has a data consistency issue. It may have been excluded for scheduling purposes. CODE: {{code}}.", "dataconsistency": "<0>{{ro_number}}</0> has a data consistency issue. It may have been excluded for scheduling purposes. CODE: {{code}}.",
@@ -59,18 +60,17 @@
"noarrivingjobs": "No Jobs are arriving.", "noarrivingjobs": "No Jobs are arriving.",
"nocompletingjobs": "No Jobs scheduled for completion.", "nocompletingjobs": "No Jobs scheduled for completion.",
"nodateselected": "No date has been selected.", "nodateselected": "No date has been selected.",
"owner": "Owner",
"priorappointments": "Previous Appointments", "priorappointments": "Previous Appointments",
"reminder": "This is {{shopname}} reminding you about an appointment on {{date}} at {{time}}. Please let us know if you are not able to make the appointment. We look forward to seeing you soon. ", "reminder": "This is {{shopname}} reminding you about an appointment on {{date}} at {{time}}. Please let us know if you are not able to make the appointment. We look forward to seeing you soon. ",
"ro_number": "RO #",
"scheduled_completion": "Scheduled Completion",
"scheduledfor": "Scheduled appointment for: ", "scheduledfor": "Scheduled appointment for: ",
"severalerrorsfound": "Several Jobs have issues which may prevent accurate smart scheduling. Click to expand.", "severalerrorsfound": "Several Jobs have issues which may prevent accurate smart scheduling. Click to expand.",
"smartscheduling": "Smart Scheduling", "smartscheduling": "Smart Scheduling",
"smspaymentreminder": "This is {{shopname}} reminding you about your remaining balance of {{amount}}. To pay for the said balance click the link {{payment_link}}.", "smspaymentreminder": "This is {{shopname}} reminding you about your remaining balance of {{amount}}. To pay for the said balance click the link {{payment_link}}.",
"suggesteddates": "Suggested Dates", "suggesteddates": "Suggested Dates",
"ro_number": "RO #", "vehicle": "Vehicle"
"owner": "Owner",
"vehicle": "Vehicle",
"bp": "B/P",
"scheduled_completion": "Scheduled Completion"
}, },
"successes": { "successes": {
"canceled": "Appointment canceled successfully.", "canceled": "Appointment canceled successfully.",
@@ -90,6 +90,11 @@
"actions": "Actions" "actions": "Actions"
} }
}, },
"audio": {
"manager": {
"description": "Click anywhere to enable the message ding."
}
},
"audit": { "audit": {
"fields": { "fields": {
"cc": "CC", "cc": "CC",
@@ -149,11 +154,6 @@
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}" "tasks_updated": "Task '{{title}}' updated by {{updatedBy}}"
} }
}, },
"audio": {
"manager": {
"description": "Click anywhere to enable the message ding."
}
},
"billlines": { "billlines": {
"actions": { "actions": {
"newline": "New Line" "newline": "New Line"
@@ -281,9 +281,9 @@
}, },
"errors": { "errors": {
"creatingdefaultview": "Error creating default view.", "creatingdefaultview": "Error creating default view.",
"duplicate_insurance_company": "Duplicate insurance company name. Each insurance company name must be unique",
"loading": "Unable to load shop details. Please call technical support.", "loading": "Unable to load shop details. Please call technical support.",
"saving": "Error encountered while saving. {{message}}", "saving": "Error encountered while saving. {{message}}"
"duplicate_insurance_company": "Duplicate insurance company name. Each insurance company name must be unique"
}, },
"fields": { "fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}", "ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
@@ -564,21 +564,18 @@
"responsibilitycenter_tax_tier": "Tax {{typeNum}} Tier {{typeNumIterator}}", "responsibilitycenter_tax_tier": "Tax {{typeNum}} Tier {{typeNumIterator}}",
"responsibilitycenter_tax_type": "Tax {{typeNum}} Type", "responsibilitycenter_tax_type": "Tax {{typeNum}} Type",
"responsibilitycenters": { "responsibilitycenters": {
"gogcode": "GOG Code (BreakOut)",
"item_type": "Item Type",
"item_type_gog": "GOG",
"item_type_paint": "Paint Materials",
"item_type_freight": "Freight",
"taxable_flag": "Taxable?",
"taxable": "Taxable",
"nontaxable": "Non-taxable",
"ap": "Accounts Payable", "ap": "Accounts Payable",
"ar": "Accounts Receivable", "ar": "Accounts Receivable",
"ats": "ATS", "ats": "ATS",
"federal_tax": "Federal Tax", "federal_tax": "Federal Tax",
"federal_tax_itc": "Federal Tax Credit", "federal_tax_itc": "Federal Tax Credit",
"gogcode": "GOG Code (BreakOut)",
"gst_override": "GST Override Account #", "gst_override": "GST Override Account #",
"invoiceexemptcode": "QuickBooks US - Invoice Tax Exempt Code", "invoiceexemptcode": "QuickBooks US - Invoice Tax Exempt Code",
"item_type": "Item Type",
"item_type_freight": "Freight",
"item_type_gog": "GOG",
"item_type_paint": "Paint Materials",
"itemexemptcode": "QuickBooks US - Line Item Tax Exempt Code", "itemexemptcode": "QuickBooks US - Line Item Tax Exempt Code",
"la1": "LA1", "la1": "LA1",
"la2": "LA2", "la2": "LA2",
@@ -597,6 +594,7 @@
"local_tax": "Local Tax", "local_tax": "Local Tax",
"mapa": "Paint Materials", "mapa": "Paint Materials",
"mash": "Shop Materials", "mash": "Shop Materials",
"nontaxable": "Non-taxable",
"paa": "Aftermarket", "paa": "Aftermarket",
"pac": "Chrome", "pac": "Chrome",
"pag": "Glass", "pag": "Glass",
@@ -617,6 +615,8 @@
"state": "State Tax Applies" "state": "State Tax Applies"
}, },
"state_tax": "State Tax", "state_tax": "State Tax",
"taxable": "Taxable",
"taxable_flag": "Taxable?",
"tow": "Towing" "tow": "Towing"
}, },
"schedule_end_time": "Schedule Ending Time", "schedule_end_time": "Schedule Ending Time",
@@ -678,8 +678,6 @@
"zip_post": "Zip/Postal Code" "zip_post": "Zip/Postal Code"
}, },
"labels": { "labels": {
"parts_shop_management": "Shop Management",
"parts_vendor_management": "Vendor Management",
"2tiername": "Name => RO", "2tiername": "Name => RO",
"2tiersetup": "2 Tier Setup", "2tiersetup": "2 Tier Setup",
"2tiersource": "Source => RO", "2tiersource": "Source => RO",
@@ -702,11 +700,11 @@
"payers": "Payers" "payers": "Payers"
}, },
"cdk_dealerid": "CDK Dealer ID", "cdk_dealerid": "CDK Dealer ID",
"rr_dealerid": "Reynolds Store Number",
"costsmapping": "Costs Mapping", "costsmapping": "Costs Mapping",
"dms_allocations": "DMS Allocations", "dms_allocations": "DMS Allocations",
"pbs_serialnumber": "PBS Serial Number", "pbs_serialnumber": "PBS Serial Number",
"profitsmapping": "Profits Mapping", "profitsmapping": "Profits Mapping",
"rr_dealerid": "Reynolds Store Number",
"title": "DMS" "title": "DMS"
}, },
"emaillater": "Email Later", "emaillater": "Email Later",
@@ -733,6 +731,8 @@
"followers": "Notifications" "followers": "Notifications"
}, },
"orderstatuses": "Order Statuses", "orderstatuses": "Order Statuses",
"parts_shop_management": "Shop Management",
"parts_vendor_management": "Vendor Management",
"partslocations": "Parts Locations", "partslocations": "Parts Locations",
"partsscan": "Parts Scanning", "partsscan": "Parts Scanning",
"printlater": "Print Later", "printlater": "Print Later",
@@ -1047,7 +1047,9 @@
}, },
"dms": { "dms": {
"errors": { "errors": {
"alreadyexported": "This job has already been sent to the DMS. If you need to resend it, please use admin permissions to mark the job for re-export." "alreadyexported": "This job has already been sent to the DMS. If you need to resend it, please use admin permissions to mark the job for re-export.",
"earlyrorequired": "Early RO Required",
"earlyrorequired.message": "This job requires an early Repair Order to be created before posting to Reynolds. Please use the admin panel to create the early RO first."
}, },
"labels": { "labels": {
"refreshallocations": "Refresh to see DMS Allocations." "refreshallocations": "Refresh to see DMS Allocations."
@@ -1228,8 +1230,6 @@
}, },
"general": { "general": {
"actions": { "actions": {
"select": "Select",
"optional": "Optional",
"add": "Add", "add": "Add",
"autoupdate": "{{app}} will automatically update in {{time}} seconds. Please save all changes.", "autoupdate": "{{app}} will automatically update in {{time}} seconds. Please save all changes.",
"calculate": "Calculate", "calculate": "Calculate",
@@ -1246,9 +1246,11 @@
"deselectall": "Deselect All", "deselectall": "Deselect All",
"download": "Download", "download": "Download",
"edit": "Edit", "edit": "Edit",
"gotoadmin": "Go to Admin Panel",
"login": "Login", "login": "Login",
"next": "Next", "next": "Next",
"ok": "Ok", "ok": "Ok",
"optional": "Optional",
"previous": "Previous", "previous": "Previous",
"print": "Print", "print": "Print",
"refresh": "Refresh", "refresh": "Refresh",
@@ -1259,6 +1261,7 @@
"save": "Save", "save": "Save",
"saveandnew": "Save and New", "saveandnew": "Save and New",
"saveas": "Save As", "saveas": "Save As",
"select": "Select",
"selectall": "Select All", "selectall": "Select All",
"send": "Send", "send": "Send",
"sendbysms": "Send by SMS", "sendbysms": "Send by SMS",
@@ -1288,8 +1291,7 @@
"vehicle": "Vehicle" "vehicle": "Vehicle"
}, },
"labels": { "labels": {
"selected": "Selected", "apply": "Apply",
"settings": "Settings",
"actions": "Actions", "actions": "Actions",
"areyousure": "Are you sure?", "areyousure": "Are you sure?",
"barcode": "Barcode", "barcode": "Barcode",
@@ -1343,8 +1345,10 @@
"search": "Search...", "search": "Search...",
"searchresults": "Results for {{search}}", "searchresults": "Results for {{search}}",
"selectdate": "Select date...", "selectdate": "Select date...",
"selected": "Selected",
"sendagain": "Send Again", "sendagain": "Send Again",
"sendby": "Send By", "sendby": "Send By",
"settings": "Settings",
"signin": "Sign In", "signin": "Sign In",
"sms": "SMS", "sms": "SMS",
"status": "Status", "status": "Status",
@@ -1587,13 +1591,13 @@
"labels": { "labels": {
"adjustmenttobeadded": "Adjustment to be added: {{adjustment}}", "adjustmenttobeadded": "Adjustment to be added: {{adjustment}}",
"billref": "Latest Bill", "billref": "Latest Bill",
"bulk_location_help": "This will set the same location on all selected lines.",
"convertedtolabor": "This line has been converted to labor. Ensure you adjust the profit center for the amount accordingly.", "convertedtolabor": "This line has been converted to labor. Ensure you adjust the profit center for the amount accordingly.",
"edit": "Edit Line", "edit": "Edit Line",
"ioucreated": "IOU", "ioucreated": "IOU",
"new": "New Line", "new": "New Line",
"nostatus": "No Status", "nostatus": "No Status",
"presets": "Jobline Presets", "presets": "Jobline Presets"
"bulk_location_help": "This will set the same location on all selected lines."
}, },
"successes": { "successes": {
"created": "Job line created successfully.", "created": "Job line created successfully.",
@@ -1621,11 +1625,13 @@
"changestatus": "Change Status", "changestatus": "Change Status",
"changestimator": "Change Estimator", "changestimator": "Change Estimator",
"convert": "Convert", "convert": "Convert",
"convertwithoutearlyro": "Convert without Early RO",
"createiou": "Create IOU", "createiou": "Create IOU",
"deliver": "Deliver", "deliver": "Deliver",
"deliver_quick": "Quick Deliver", "deliver_quick": "Quick Deliver",
"dms": { "dms": {
"addpayer": "Add Payer", "addpayer": "Add Payer",
"createearlyro": "Create RR RO",
"createnewcustomer": "Create New Customer", "createnewcustomer": "Create New Customer",
"findmakemodelcode": "Find Make/Model Code", "findmakemodelcode": "Find Make/Model Code",
"getmakes": "Get Makes", "getmakes": "Get Makes",
@@ -1634,6 +1640,7 @@
}, },
"post": "Post", "post": "Post",
"refetchmakesmodels": "Refetch Make and Model Codes", "refetchmakesmodels": "Refetch Make and Model Codes",
"update_ro": "Update RO",
"usegeneric": "Use Generic Customer", "usegeneric": "Use Generic Customer",
"useselected": "Use Selected Customer" "useselected": "Use Selected Customer"
}, },
@@ -1701,9 +1708,9 @@
"actual_delivery": "Actual Delivery", "actual_delivery": "Actual Delivery",
"actual_in": "Actual In", "actual_in": "Actual In",
"acv_amount": "ACV Amount", "acv_amount": "ACV Amount",
"admin_clerk": "Admin Clerk",
"adjustment_bottom_line": "Adjustments", "adjustment_bottom_line": "Adjustments",
"adjustmenthours": "Adjustment Hours", "adjustmenthours": "Adjustment Hours",
"admin_clerk": "Admin Clerk",
"alt_transport": "Alt. Trans.", "alt_transport": "Alt. Trans.",
"area_of_damage_impact": { "area_of_damage_impact": {
"10": "Left Front Side", "10": "Left Front Side",
@@ -1784,9 +1791,8 @@
"ded_status": "Deductible Status", "ded_status": "Deductible Status",
"depreciation_taxes": "Betterment/Depreciation/Taxes", "depreciation_taxes": "Betterment/Depreciation/Taxes",
"dms": { "dms": {
"first_name": "First Name",
"last_name": "Last Name",
"address": "Customer Address", "address": "Customer Address",
"advisor": "Advisor #",
"amount": "Amount", "amount": "Amount",
"center": "Center", "center": "Center",
"control_type": { "control_type": {
@@ -1794,17 +1800,19 @@
}, },
"cost": "Cost", "cost": "Cost",
"cost_dms_acctnumber": "Cost DMS Acct #", "cost_dms_acctnumber": "Cost DMS Acct #",
"customer": "Customer #",
"dms_make": "DMS Make", "dms_make": "DMS Make",
"dms_model": "DMS Model", "dms_model": "DMS Model",
"dms_model_override": "Override DMS Make/Model", "dms_model_override": "Override DMS Make/Model",
"dms_unsold": "New, Unsold Vehicle", "dms_unsold": "New, Unsold Vehicle",
"dms_wip_acctnumber": "Cost WIP DMS Acct #", "dms_wip_acctnumber": "Cost WIP DMS Acct #",
"first_name": "First Name",
"id": "DMS ID", "id": "DMS ID",
"inservicedate": "In Service Date", "inservicedate": "In Service Date",
"journal": "Journal #", "journal": "Journal #",
"make_override": "Make Override", "last_name": "Last Name",
"advisor": "Advisor #",
"lines": "Posting Lines", "lines": "Posting Lines",
"make_override": "Make Override",
"name1": "Customer Name", "name1": "Customer Name",
"payer": { "payer": {
"amount": "Amount", "amount": "Amount",
@@ -1817,7 +1825,11 @@
"sale": "Sale", "sale": "Sale",
"sale_dms_acctnumber": "Sale DMS Acct #", "sale_dms_acctnumber": "Sale DMS Acct #",
"story": "Story", "story": "Story",
"vinowner": "VIN Owner" "vinowner": "VIN Owner",
"rr_opcode": "RR OpCode",
"rr_opcode_prefix": "Prefix",
"rr_opcode_suffix": "Suffix",
"rr_opcode_base": "Base"
}, },
"dms_allocation": "DMS Allocation", "dms_allocation": "DMS Allocation",
"driveable": "Driveable", "driveable": "Driveable",
@@ -1945,7 +1957,7 @@
"amount": "Amount", "amount": "Amount",
"name": "Name" "name": "Name"
}, },
"queued_for_parts": "Queued for Parts", "queued_for_parts": "Queued",
"rate_ats": "ATS Rate", "rate_ats": "ATS Rate",
"rate_ats_flat": "ATS Flat Rate", "rate_ats_flat": "ATS Flat Rate",
"rate_la1": "LA1", "rate_la1": "LA1",
@@ -2102,6 +2114,11 @@
"damageto": "Damage to $t(jobs.fields.area_of_damage_impact.{{area_of_damage}}).", "damageto": "Damage to $t(jobs.fields.area_of_damage_impact.{{area_of_damage}}).",
"defaultstory": "B/S RO: {{ro_number}}. Owner: {{ownr_nm}}. Insurance Co: {{ins_co_nm}}. Claim/PO #: {{clm_po}}", "defaultstory": "B/S RO: {{ro_number}}. Owner: {{ownr_nm}}. Insurance Co: {{ins_co_nm}}. Claim/PO #: {{clm_po}}",
"disablebillwip": "Cost and WIP for bills has been ignored per shop configuration.", "disablebillwip": "Cost and WIP for bills has been ignored per shop configuration.",
"earlyro": {
"created": "Early RO Created:",
"fields": "Required fields:",
"willupdate": "This will update the existing RO with full job data."
},
"invoicedatefuture": "Invoice date must be today or in the future for CDK posting.", "invoicedatefuture": "Invoice date must be today or in the future for CDK posting.",
"kmoutnotgreaterthankmin": "Mileage out must be greater than mileage in.", "kmoutnotgreaterthankmin": "Mileage out must be greater than mileage in.",
"logs": "Logs", "logs": "Logs",
@@ -2259,6 +2276,7 @@
"delete": "Job deleted successfully.", "delete": "Job deleted successfully.",
"deleted": "Job deleted successfully.", "deleted": "Job deleted successfully.",
"duplicated": "Job duplicated successfully. ", "duplicated": "Job duplicated successfully. ",
"early_ro_created": "Early RO Created",
"exported": "Job(s) exported successfully. ", "exported": "Job(s) exported successfully. ",
"invoiced": "Job closed and invoiced successfully.", "invoiced": "Job closed and invoiced successfully.",
"ioucreated": "IOU created successfully. Click to see.", "ioucreated": "IOU created successfully. Click to see.",
@@ -2447,6 +2465,7 @@
"labels": { "labels": {
"addlabel": "Add a label to this conversation.", "addlabel": "Add a label to this conversation.",
"archive": "Archive", "archive": "Archive",
"mark_unread": "Mark as unread",
"maxtenimages": "You can only select up to a maximum of 10 images at a time.", "maxtenimages": "You can only select up to a maximum of 10 images at a time.",
"messaging": "Messaging", "messaging": "Messaging",
"no_consent": "Opted-out", "no_consent": "Opted-out",
@@ -2459,8 +2478,7 @@
"selectmedia": "Select Media", "selectmedia": "Select Media",
"sentby": "Sent by {{by}} at {{time}}", "sentby": "Sent by {{by}} at {{time}}",
"typeamessage": "Send a message...", "typeamessage": "Send a message...",
"unarchive": "Unarchive", "unarchive": "Unarchive"
"mark_unread": "Mark as unread"
}, },
"render": { "render": {
"conversation_list": "Conversation List" "conversation_list": "Conversation List"
@@ -2614,20 +2632,20 @@
"name": "Owner Details" "name": "Owner Details"
}, },
"labels": { "labels": {
"cell": "Cell",
"create_new": "Create a new owner record.", "create_new": "Create a new owner record.",
"deleteconfirm": "Are you sure you want to delete this owner? This cannot be undone.", "deleteconfirm": "Are you sure you want to delete this owner? This cannot be undone.",
"email": "Email",
"existing_owners": "Existing Owners", "existing_owners": "Existing Owners",
"fromclaim": "Current Claim", "fromclaim": "Current Claim",
"fromowner": "Historical Owner Record", "fromowner": "Historical Owner Record",
"relatedjobs": "Related Jobs",
"updateowner": "Update Owner",
"work": "Work",
"home": "Home", "home": "Home",
"cell": "Cell",
"other": "Other", "other": "Other",
"email": "Email",
"phone": "Phone", "phone": "Phone",
"sms": "SMS" "relatedjobs": "Related Jobs",
"sms": "SMS",
"updateowner": "Update Owner",
"work": "Work"
}, },
"successes": { "successes": {
"delete": "Owner deleted successfully.", "delete": "Owner deleted successfully.",
@@ -2638,6 +2656,10 @@
"actions": { "actions": {
"order": "Order Parts", "order": "Order Parts",
"orderinhouse": "Order as In House" "orderinhouse": "Order as In House"
},
"labels": {
"view_counts_only": "View Parts Counts Only",
"view_timestamps": "Show timestamps"
} }
}, },
"parts_dispatch": { "parts_dispatch": {
@@ -2987,8 +3009,6 @@
"settings": "Error saving board settings: {{error}}" "settings": "Error saving board settings: {{error}}"
}, },
"labels": { "labels": {
"click_for_statuses": "Click to view parts statuses",
"partsreceived": "Parts Received",
"actual_in": "Actual In", "actual_in": "Actual In",
"addnewprofile": "Add New Profile", "addnewprofile": "Add New Profile",
"alert": "Alert", "alert": "Alert",
@@ -3007,6 +3027,7 @@
"card_size": "Card Size", "card_size": "Card Size",
"cardcolor": "Colored Cards", "cardcolor": "Colored Cards",
"cardsettings": "Card Settings", "cardsettings": "Card Settings",
"click_for_statuses": "Click to view parts statuses",
"clm_no": "Claim Number", "clm_no": "Claim Number",
"comment": "Comment", "comment": "Comment",
"compact": "Compact Cards", "compact": "Compact Cards",
@@ -3027,6 +3048,7 @@
"orientation": "Board Orientation", "orientation": "Board Orientation",
"ownr_nm": "Customer Name", "ownr_nm": "Customer Name",
"paintpriority": "P/P", "paintpriority": "P/P",
"partsreceived": "Parts Received",
"partsstatus": "Parts Status", "partsstatus": "Parts Status",
"production_note": "Production Note", "production_note": "Production Note",
"refinishhours": "R", "refinishhours": "R",
@@ -3573,17 +3595,12 @@
} }
}, },
"titles": { "titles": {
"parts_settings": "Parts Management Settings | {{app}}",
"simplified-parts-jobs": "Parts Management | {{app}}",
"accounting-payables": "Payables | {{app}}", "accounting-payables": "Payables | {{app}}",
"accounting-payments": "Payments | {{app}}", "accounting-payments": "Payments | {{app}}",
"accounting-receivables": "Receivables | {{app}}", "accounting-receivables": "Receivables | {{app}}",
"all_tasks": "All Tasks | {{app}}", "all_tasks": "All Tasks | {{app}}",
"app": "", "app": "",
"bc": { "bc": {
"simplified-parts-jobs": "Jobs",
"parts": "Parts",
"parts_settings": "Settings",
"accounting-payables": "Payables", "accounting-payables": "Payables",
"accounting-payments": "Payments", "accounting-payments": "Payments",
"accounting-receivables": "Receivables", "accounting-receivables": "Receivables",
@@ -3615,7 +3632,9 @@
"my_tasks": "My Tasks", "my_tasks": "My Tasks",
"owner-detail": "{{name}}", "owner-detail": "{{name}}",
"owners": "Owners", "owners": "Owners",
"parts": "Parts",
"parts-queue": "Parts Queue", "parts-queue": "Parts Queue",
"parts_settings": "Settings",
"payments-all": "All Payments", "payments-all": "All Payments",
"phonebook": "Phonebook", "phonebook": "Phonebook",
"productionboard": "Production Board - Visual", "productionboard": "Production Board - Visual",
@@ -3627,6 +3646,7 @@
"shop-csi": "CSI Responses", "shop-csi": "CSI Responses",
"shop-templates": "Shop Templates", "shop-templates": "Shop Templates",
"shop-vendors": "Vendors", "shop-vendors": "Vendors",
"simplified-parts-jobs": "Jobs",
"tasks": "Tasks", "tasks": "Tasks",
"temporarydocs": "Temporary Documents", "temporarydocs": "Temporary Documents",
"timetickets": "Time Tickets", "timetickets": "Time Tickets",
@@ -3662,7 +3682,9 @@
"my_tasks": "My Tasks | {{app}}", "my_tasks": "My Tasks | {{app}}",
"owners": "All Owners | {{app}}", "owners": "All Owners | {{app}}",
"owners-detail": "{{name}} | {{app}}", "owners-detail": "{{name}} | {{app}}",
"parts": "",
"parts-queue": "Parts Queue | {{app}}", "parts-queue": "Parts Queue | {{app}}",
"parts_settings": "Parts Management Settings | {{app}}",
"payments-all": "Payments | {{app}}", "payments-all": "Payments | {{app}}",
"phonebook": "Phonebook | {{app}}", "phonebook": "Phonebook | {{app}}",
"productionboard": "Production Board - Visual | {{app}}", "productionboard": "Production Board - Visual | {{app}}",
@@ -3678,6 +3700,7 @@
"shop-csi": "CSI Responses | {{app}}", "shop-csi": "CSI Responses | {{app}}",
"shop-templates": "Shop Templates | {{app}}", "shop-templates": "Shop Templates | {{app}}",
"shop_vendors": "Vendors | {{app}}", "shop_vendors": "Vendors | {{app}}",
"simplified-parts-jobs": "Parts Management | {{app}}",
"tasks": "Tasks", "tasks": "Tasks",
"techconsole": "Technician Console | {{app}}", "techconsole": "Technician Console | {{app}}",
"techjobclock": "Technician Job Clock | {{app}}", "techjobclock": "Technician Job Clock | {{app}}",
@@ -3838,10 +3861,10 @@
"user": { "user": {
"actions": { "actions": {
"changepassword": "Change Password", "changepassword": "Change Password",
"signout": "Sign Out", "dark_theme": "Switch to Dark Theme",
"updateprofile": "Update Profile",
"light_theme": "Switch to Light Theme", "light_theme": "Switch to Light Theme",
"dark_theme": "Switch to Dark Theme" "signout": "Sign Out",
"updateprofile": "Update Profile"
}, },
"errors": { "errors": {
"updating": "Error updating user or association {{message}}" "updating": "Error updating user or association {{message}}"
@@ -3855,14 +3878,14 @@
"labels": { "labels": {
"actions": "Actions", "actions": "Actions",
"changepassword": "Change Password", "changepassword": "Change Password",
"profileinfo": "Profile Info",
"user_settings": "User Settings",
"play_sound_for_new_messages": "Play a sound for new messages",
"notification_sound_on": "Sound is ON",
"notification_sound_off": "Sound is OFF",
"notification_sound_enabled": "Notification sound enabled",
"notification_sound_disabled": "Notification sound disabled", "notification_sound_disabled": "Notification sound disabled",
"notification_sound_help": "Toggle the ding for incoming chat messages." "notification_sound_enabled": "Notification sound enabled",
"notification_sound_help": "Toggle the ding for incoming chat messages.",
"notification_sound_off": "Sound is OFF",
"notification_sound_on": "Sound is ON",
"play_sound_for_new_messages": "Play a sound for new messages",
"profileinfo": "Profile Info",
"user_settings": "User Settings"
}, },
"successess": { "successess": {
"passwordchanged": "Password changed successfully. " "passwordchanged": "Password changed successfully. "

View File

@@ -48,6 +48,7 @@
"arrivedon": "Llegado el:", "arrivedon": "Llegado el:",
"arrivingjobs": "", "arrivingjobs": "",
"blocked": "", "blocked": "",
"bp": "",
"cancelledappointment": "Cita cancelada para:", "cancelledappointment": "Cita cancelada para:",
"completingjobs": "", "completingjobs": "",
"dataconsistency": "", "dataconsistency": "",
@@ -59,18 +60,17 @@
"noarrivingjobs": "", "noarrivingjobs": "",
"nocompletingjobs": "", "nocompletingjobs": "",
"nodateselected": "No se ha seleccionado ninguna fecha.", "nodateselected": "No se ha seleccionado ninguna fecha.",
"owner": "",
"priorappointments": "Nombramientos previos", "priorappointments": "Nombramientos previos",
"reminder": "", "reminder": "",
"ro_number": "",
"scheduled_completion": "",
"scheduledfor": "Cita programada para:", "scheduledfor": "Cita programada para:",
"severalerrorsfound": "", "severalerrorsfound": "",
"smartscheduling": "", "smartscheduling": "",
"smspaymentreminder": "", "smspaymentreminder": "",
"suggesteddates": "", "suggesteddates": "",
"ro_number": "", "vehicle": ""
"owner": "",
"vehicle": "",
"bp": "",
"scheduled_completion": ""
}, },
"successes": { "successes": {
"canceled": "Cita cancelada con éxito.", "canceled": "Cita cancelada con éxito.",
@@ -90,6 +90,11 @@
"actions": "Comportamiento" "actions": "Comportamiento"
} }
}, },
"audio": {
"manager": {
"description": ""
}
},
"audit": { "audit": {
"fields": { "fields": {
"cc": "", "cc": "",
@@ -149,11 +154,6 @@
"tasks_updated": "" "tasks_updated": ""
} }
}, },
"audio": {
"manager": {
"description": ""
}
},
"billlines": { "billlines": {
"actions": { "actions": {
"newline": "" "newline": ""
@@ -281,9 +281,9 @@
}, },
"errors": { "errors": {
"creatingdefaultview": "", "creatingdefaultview": "",
"duplicate_insurance_company": "",
"loading": "No se pueden cargar los detalles de la tienda. Por favor llame al soporte técnico.", "loading": "No se pueden cargar los detalles de la tienda. Por favor llame al soporte técnico.",
"saving": "", "saving": ""
"duplicate_insurance_company": ""
}, },
"fields": { "fields": {
"ReceivableCustomField": "", "ReceivableCustomField": "",
@@ -564,21 +564,18 @@
"responsibilitycenter_tax_tier": "", "responsibilitycenter_tax_tier": "",
"responsibilitycenter_tax_type": "", "responsibilitycenter_tax_type": "",
"responsibilitycenters": { "responsibilitycenters": {
"gogcode": "",
"item_type": "Item Type",
"item_type_gog": "",
"item_type_paint": "",
"item_type_freight": "",
"taxable_flag": "",
"taxable": "",
"nontaxable": "",
"ap": "", "ap": "",
"ar": "", "ar": "",
"ats": "", "ats": "",
"federal_tax": "", "federal_tax": "",
"federal_tax_itc": "", "federal_tax_itc": "",
"gogcode": "",
"gst_override": "", "gst_override": "",
"invoiceexemptcode": "", "invoiceexemptcode": "",
"item_type": "Item Type",
"item_type_freight": "",
"item_type_gog": "",
"item_type_paint": "",
"itemexemptcode": "", "itemexemptcode": "",
"la1": "", "la1": "",
"la2": "", "la2": "",
@@ -597,6 +594,7 @@
"local_tax": "", "local_tax": "",
"mapa": "", "mapa": "",
"mash": "", "mash": "",
"nontaxable": "",
"paa": "", "paa": "",
"pac": "", "pac": "",
"pag": "", "pag": "",
@@ -617,6 +615,8 @@
"state": "" "state": ""
}, },
"state_tax": "", "state_tax": "",
"taxable": "",
"taxable_flag": "",
"tow": "" "tow": ""
}, },
"schedule_end_time": "", "schedule_end_time": "",
@@ -678,8 +678,6 @@
"zip_post": "" "zip_post": ""
}, },
"labels": { "labels": {
"parts_shop_management": "",
"parts_vendor_management": "",
"2tiername": "", "2tiername": "",
"2tiersetup": "", "2tiersetup": "",
"2tiersource": "", "2tiersource": "",
@@ -702,11 +700,11 @@
"payers": "" "payers": ""
}, },
"cdk_dealerid": "", "cdk_dealerid": "",
"rr_dealerid": "",
"costsmapping": "", "costsmapping": "",
"dms_allocations": "", "dms_allocations": "",
"pbs_serialnumber": "", "pbs_serialnumber": "",
"profitsmapping": "", "profitsmapping": "",
"rr_dealerid": "",
"title": "" "title": ""
}, },
"emaillater": "", "emaillater": "",
@@ -733,6 +731,8 @@
"followers": "" "followers": ""
}, },
"orderstatuses": "", "orderstatuses": "",
"parts_shop_management": "",
"parts_vendor_management": "",
"partslocations": "", "partslocations": "",
"partsscan": "", "partsscan": "",
"printlater": "", "printlater": "",
@@ -1047,7 +1047,9 @@
}, },
"dms": { "dms": {
"errors": { "errors": {
"alreadyexported": "" "alreadyexported": "",
"earlyrorequired": "",
"earlyrorequired.message": ""
}, },
"labels": { "labels": {
"refreshallocations": "" "refreshallocations": ""
@@ -1244,9 +1246,11 @@
"deselectall": "", "deselectall": "",
"download": "", "download": "",
"edit": "Editar", "edit": "Editar",
"gotoadmin": "",
"login": "", "login": "",
"next": "", "next": "",
"ok": "", "ok": "",
"optional": "",
"previous": "", "previous": "",
"print": "", "print": "",
"refresh": "", "refresh": "",
@@ -1257,6 +1261,7 @@
"save": "Salvar", "save": "Salvar",
"saveandnew": "", "saveandnew": "",
"saveas": "", "saveas": "",
"select": "",
"selectall": "", "selectall": "",
"send": "", "send": "",
"sendbysms": "", "sendbysms": "",
@@ -1286,9 +1291,8 @@
"vehicle": "" "vehicle": ""
}, },
"labels": { "labels": {
"selected": "", "apply": "",
"actions": "Comportamiento", "actions": "Comportamiento",
"settings": "",
"areyousure": "", "areyousure": "",
"barcode": "código de barras", "barcode": "código de barras",
"cancel": "", "cancel": "",
@@ -1341,8 +1345,10 @@
"search": "Buscar...", "search": "Buscar...",
"searchresults": "", "searchresults": "",
"selectdate": "", "selectdate": "",
"selected": "",
"sendagain": "", "sendagain": "",
"sendby": "", "sendby": "",
"settings": "",
"signin": "", "signin": "",
"sms": "", "sms": "",
"status": "", "status": "",
@@ -1585,13 +1591,13 @@
"labels": { "labels": {
"adjustmenttobeadded": "", "adjustmenttobeadded": "",
"billref": "", "billref": "",
"bulk_location_help": "",
"convertedtolabor": "", "convertedtolabor": "",
"edit": "Línea de edición", "edit": "Línea de edición",
"ioucreated": "", "ioucreated": "",
"new": "Nueva línea", "new": "Nueva línea",
"nostatus": "", "nostatus": "",
"presets": "", "presets": ""
"bulk_location_help": ""
}, },
"successes": { "successes": {
"created": "", "created": "",
@@ -1619,11 +1625,13 @@
"changestatus": "Cambiar Estado", "changestatus": "Cambiar Estado",
"changestimator": "", "changestimator": "",
"convert": "Convertir", "convert": "Convertir",
"convertwithoutearlyro": "",
"createiou": "", "createiou": "",
"deliver": "", "deliver": "",
"deliver_quick": "", "deliver_quick": "",
"dms": { "dms": {
"addpayer": "", "addpayer": "",
"createearlyro": "",
"createnewcustomer": "", "createnewcustomer": "",
"findmakemodelcode": "", "findmakemodelcode": "",
"getmakes": "", "getmakes": "",
@@ -1632,6 +1640,7 @@
}, },
"post": "", "post": "",
"refetchmakesmodels": "", "refetchmakesmodels": "",
"update_ro": "",
"usegeneric": "", "usegeneric": "",
"useselected": "" "useselected": ""
}, },
@@ -1700,8 +1709,8 @@
"actual_in": "Real en", "actual_in": "Real en",
"acv_amount": "", "acv_amount": "",
"adjustment_bottom_line": "Ajustes", "adjustment_bottom_line": "Ajustes",
"admin_clerk": "",
"adjustmenthours": "", "adjustmenthours": "",
"admin_clerk": "",
"alt_transport": "", "alt_transport": "",
"area_of_damage_impact": { "area_of_damage_impact": {
"10": "", "10": "",
@@ -1782,9 +1791,8 @@
"ded_status": "Estado deducible", "ded_status": "Estado deducible",
"depreciation_taxes": "Depreciación / Impuestos", "depreciation_taxes": "Depreciación / Impuestos",
"dms": { "dms": {
"first_name": "",
"last_name": "",
"address": "", "address": "",
"advisor": "",
"amount": "", "amount": "",
"center": "", "center": "",
"control_type": { "control_type": {
@@ -1792,29 +1800,36 @@
}, },
"cost": "", "cost": "",
"cost_dms_acctnumber": "", "cost_dms_acctnumber": "",
"customer": "",
"dms_make": "", "dms_make": "",
"dms_model": "", "dms_model": "",
"dms_model_override": "", "dms_model_override": "",
"make_override": "",
"advisor": "",
"dms_unsold": "", "dms_unsold": "",
"dms_wip_acctnumber": "", "dms_wip_acctnumber": "",
"first_name": "",
"id": "", "id": "",
"inservicedate": "", "inservicedate": "",
"journal": "", "journal": "",
"last_name": "",
"lines": "", "lines": "",
"make_override": "",
"name1": "", "name1": "",
"payer": { "payer": {
"amount": "", "amount": "",
"control_type": "", "control_type": "",
"controlnumber": "", "controlnumber": "",
"dms_acctnumber": "", "dms_acctnumber": "",
"name": "" "name": "",
"payer_type": ""
}, },
"sale": "", "sale": "",
"sale_dms_acctnumber": "", "sale_dms_acctnumber": "",
"story": "", "story": "",
"vinowner": "" "vinowner": "",
"rr_opcode": "",
"rr_opcode_prefix": "",
"rr_opcode_suffix": "",
"rr_opcode_base": ""
}, },
"dms_allocation": "", "dms_allocation": "",
"driveable": "", "driveable": "",
@@ -2099,6 +2114,11 @@
"damageto": "", "damageto": "",
"defaultstory": "", "defaultstory": "",
"disablebillwip": "", "disablebillwip": "",
"earlyro": {
"created": "",
"fields": "",
"willupdate": ""
},
"invoicedatefuture": "", "invoicedatefuture": "",
"kmoutnotgreaterthankmin": "", "kmoutnotgreaterthankmin": "",
"logs": "", "logs": "",
@@ -2256,6 +2276,7 @@
"delete": "", "delete": "",
"deleted": "Trabajo eliminado con éxito.", "deleted": "Trabajo eliminado con éxito.",
"duplicated": "", "duplicated": "",
"early_ro_created": "",
"exported": "", "exported": "",
"invoiced": "", "invoiced": "",
"ioucreated": "", "ioucreated": "",
@@ -2444,6 +2465,7 @@
"labels": { "labels": {
"addlabel": "", "addlabel": "",
"archive": "", "archive": "",
"mark_unread": "",
"maxtenimages": "", "maxtenimages": "",
"messaging": "Mensajería", "messaging": "Mensajería",
"no_consent": "", "no_consent": "",
@@ -2456,8 +2478,7 @@
"selectmedia": "", "selectmedia": "",
"sentby": "", "sentby": "",
"typeamessage": "Enviar un mensaje...", "typeamessage": "Enviar un mensaje...",
"unarchive": "", "unarchive": ""
"mark_unread": ""
}, },
"render": { "render": {
"conversation_list": "" "conversation_list": ""
@@ -2611,20 +2632,20 @@
"name": "" "name": ""
}, },
"labels": { "labels": {
"cell": "",
"create_new": "Crea un nuevo registro de propietario.", "create_new": "Crea un nuevo registro de propietario.",
"deleteconfirm": "", "deleteconfirm": "",
"email": "",
"existing_owners": "Propietarios existentes", "existing_owners": "Propietarios existentes",
"fromclaim": "", "fromclaim": "",
"fromowner": "", "fromowner": "",
"relatedjobs": "",
"updateowner": "",
"work": "",
"home": "", "home": "",
"cell": "",
"other": "", "other": "",
"email": "",
"phone": "", "phone": "",
"sms": "" "relatedjobs": "",
"sms": "",
"updateowner": "",
"work": ""
}, },
"successes": { "successes": {
"delete": "", "delete": "",
@@ -2635,6 +2656,10 @@
"actions": { "actions": {
"order": "Pedido de piezas", "order": "Pedido de piezas",
"orderinhouse": "" "orderinhouse": ""
},
"labels": {
"view_counts_only": "",
"view_timestamps": ""
} }
}, },
"parts_dispatch": { "parts_dispatch": {
@@ -2984,8 +3009,6 @@
"settings": "" "settings": ""
}, },
"labels": { "labels": {
"click_for_statuses": "",
"partsreceived": "",
"actual_in": "", "actual_in": "",
"addnewprofile": "", "addnewprofile": "",
"alert": "", "alert": "",
@@ -3004,6 +3027,7 @@
"card_size": "", "card_size": "",
"cardcolor": "", "cardcolor": "",
"cardsettings": "", "cardsettings": "",
"click_for_statuses": "",
"clm_no": "", "clm_no": "",
"comment": "", "comment": "",
"compact": "", "compact": "",
@@ -3024,6 +3048,7 @@
"orientation": "", "orientation": "",
"ownr_nm": "", "ownr_nm": "",
"paintpriority": "", "paintpriority": "",
"partsreceived": "",
"partsstatus": "", "partsstatus": "",
"production_note": "", "production_note": "",
"refinishhours": "", "refinishhours": "",
@@ -3570,18 +3595,12 @@
} }
}, },
"titles": { "titles": {
"simplified-parts-jobs": "",
"parts": "",
"parts_settings": "",
"accounting-payables": "", "accounting-payables": "",
"accounting-payments": "", "accounting-payments": "",
"accounting-receivables": "", "accounting-receivables": "",
"all_tasks": "", "all_tasks": "",
"app": "", "app": "",
"bc": { "bc": {
"simplified-parts-jobs": "",
"parts": "",
"parts_settings": "",
"accounting-payables": "", "accounting-payables": "",
"accounting-payments": "", "accounting-payments": "",
"accounting-receivables": "", "accounting-receivables": "",
@@ -3613,7 +3632,9 @@
"my_tasks": "", "my_tasks": "",
"owner-detail": "", "owner-detail": "",
"owners": "", "owners": "",
"parts": "",
"parts-queue": "", "parts-queue": "",
"parts_settings": "",
"payments-all": "", "payments-all": "",
"phonebook": "", "phonebook": "",
"productionboard": "", "productionboard": "",
@@ -3625,6 +3646,7 @@
"shop-csi": "", "shop-csi": "",
"shop-templates": "", "shop-templates": "",
"shop-vendors": "", "shop-vendors": "",
"simplified-parts-jobs": "",
"tasks": "", "tasks": "",
"temporarydocs": "", "temporarydocs": "",
"timetickets": "", "timetickets": "",
@@ -3660,7 +3682,9 @@
"my_tasks": "", "my_tasks": "",
"owners": "Todos los propietarios | {{app}}", "owners": "Todos los propietarios | {{app}}",
"owners-detail": "", "owners-detail": "",
"parts": "",
"parts-queue": "", "parts-queue": "",
"parts_settings": "",
"payments-all": "", "payments-all": "",
"phonebook": "", "phonebook": "",
"productionboard": "", "productionboard": "",
@@ -3676,6 +3700,7 @@
"shop-csi": "", "shop-csi": "",
"shop-templates": "", "shop-templates": "",
"shop_vendors": "Vendedores | {{app}}", "shop_vendors": "Vendedores | {{app}}",
"simplified-parts-jobs": "",
"tasks": "", "tasks": "",
"techconsole": "{{app}}", "techconsole": "{{app}}",
"techjobclock": "{{app}}", "techjobclock": "{{app}}",
@@ -3836,10 +3861,10 @@
"user": { "user": {
"actions": { "actions": {
"changepassword": "", "changepassword": "",
"signout": "desconectar", "dark_theme": "",
"updateprofile": "Actualización del perfil",
"light_theme": "", "light_theme": "",
"dark_theme": "" "signout": "desconectar",
"updateprofile": "Actualización del perfil"
}, },
"errors": { "errors": {
"updating": "" "updating": ""
@@ -3853,14 +3878,14 @@
"labels": { "labels": {
"actions": "", "actions": "",
"changepassword": "", "changepassword": "",
"profileinfo": "",
"user_settings": "",
"play_sound_for_new_messages": "",
"notification_sound_on": "",
"notification_sound_off": "",
"notification_sound_enabled": "",
"notification_sound_disabled": "", "notification_sound_disabled": "",
"notification_sound_help": "" "notification_sound_enabled": "",
"notification_sound_help": "",
"notification_sound_off": "",
"notification_sound_on": "",
"play_sound_for_new_messages": "",
"profileinfo": "",
"user_settings": ""
}, },
"successess": { "successess": {
"passwordchanged": "" "passwordchanged": ""

View File

@@ -48,6 +48,7 @@
"arrivedon": "Arrivé le:", "arrivedon": "Arrivé le:",
"arrivingjobs": "", "arrivingjobs": "",
"blocked": "", "blocked": "",
"bp": "",
"cancelledappointment": "Rendez-vous annulé pour:", "cancelledappointment": "Rendez-vous annulé pour:",
"completingjobs": "", "completingjobs": "",
"dataconsistency": "", "dataconsistency": "",
@@ -59,18 +60,17 @@
"noarrivingjobs": "", "noarrivingjobs": "",
"nocompletingjobs": "", "nocompletingjobs": "",
"nodateselected": "Aucune date n'a été sélectionnée.", "nodateselected": "Aucune date n'a été sélectionnée.",
"owner": "",
"priorappointments": "Rendez-vous précédents", "priorappointments": "Rendez-vous précédents",
"reminder": "", "reminder": "",
"ro_number": "",
"scheduled_completion": "",
"scheduledfor": "Rendez-vous prévu pour:", "scheduledfor": "Rendez-vous prévu pour:",
"severalerrorsfound": "", "severalerrorsfound": "",
"smartscheduling": "", "smartscheduling": "",
"smspaymentreminder": "", "smspaymentreminder": "",
"suggesteddates": "", "suggesteddates": "",
"ro_number": "", "vehicle": ""
"owner": "",
"vehicle": "",
"bp": "",
"scheduled_completion": ""
}, },
"successes": { "successes": {
"canceled": "Rendez-vous annulé avec succès.", "canceled": "Rendez-vous annulé avec succès.",
@@ -90,6 +90,11 @@
"actions": "actes" "actions": "actes"
} }
}, },
"audio": {
"manager": {
"description": ""
}
},
"audit": { "audit": {
"fields": { "fields": {
"cc": "", "cc": "",
@@ -149,11 +154,6 @@
"tasks_updated": "" "tasks_updated": ""
} }
}, },
"audio": {
"manager": {
"description": ""
}
},
"billlines": { "billlines": {
"actions": { "actions": {
"newline": "" "newline": ""
@@ -281,9 +281,9 @@
}, },
"errors": { "errors": {
"creatingdefaultview": "", "creatingdefaultview": "",
"duplicate_insurance_company": "",
"loading": "Impossible de charger les détails de la boutique. Veuillez appeler le support technique.", "loading": "Impossible de charger les détails de la boutique. Veuillez appeler le support technique.",
"saving": "", "saving": ""
"duplicate_insurance_company": ""
}, },
"fields": { "fields": {
"ReceivableCustomField": "", "ReceivableCustomField": "",
@@ -564,21 +564,18 @@
"responsibilitycenter_tax_tier": "", "responsibilitycenter_tax_tier": "",
"responsibilitycenter_tax_type": "", "responsibilitycenter_tax_type": "",
"responsibilitycenters": { "responsibilitycenters": {
"gogcode": "",
"item_type": "Item Type",
"item_type_gog": "",
"item_type_paint": "",
"item_type_freight": "",
"taxable_flag": "",
"taxable": "",
"nontaxable": "",
"ap": "", "ap": "",
"ar": "", "ar": "",
"ats": "", "ats": "",
"federal_tax": "", "federal_tax": "",
"federal_tax_itc": "", "federal_tax_itc": "",
"gogcode": "",
"gst_override": "", "gst_override": "",
"invoiceexemptcode": "", "invoiceexemptcode": "",
"item_type": "Item Type",
"item_type_freight": "",
"item_type_gog": "",
"item_type_paint": "",
"itemexemptcode": "", "itemexemptcode": "",
"la1": "", "la1": "",
"la2": "", "la2": "",
@@ -597,6 +594,7 @@
"local_tax": "", "local_tax": "",
"mapa": "", "mapa": "",
"mash": "", "mash": "",
"nontaxable": "",
"paa": "", "paa": "",
"pac": "", "pac": "",
"pag": "", "pag": "",
@@ -617,6 +615,8 @@
"state": "" "state": ""
}, },
"state_tax": "", "state_tax": "",
"taxable": "",
"taxable_flag": "",
"tow": "" "tow": ""
}, },
"schedule_end_time": "", "schedule_end_time": "",
@@ -678,8 +678,6 @@
"zip_post": "" "zip_post": ""
}, },
"labels": { "labels": {
"parts_shop_management": "",
"parts_vendor_management": "",
"2tiername": "", "2tiername": "",
"2tiersetup": "", "2tiersetup": "",
"2tiersource": "", "2tiersource": "",
@@ -702,11 +700,11 @@
"payers": "" "payers": ""
}, },
"cdk_dealerid": "", "cdk_dealerid": "",
"rr_dealerid": "",
"costsmapping": "", "costsmapping": "",
"dms_allocations": "", "dms_allocations": "",
"pbs_serialnumber": "", "pbs_serialnumber": "",
"profitsmapping": "", "profitsmapping": "",
"rr_dealerid": "",
"title": "" "title": ""
}, },
"emaillater": "", "emaillater": "",
@@ -733,6 +731,8 @@
"followers": "" "followers": ""
}, },
"orderstatuses": "", "orderstatuses": "",
"parts_shop_management": "",
"parts_vendor_management": "",
"partslocations": "", "partslocations": "",
"partsscan": "", "partsscan": "",
"printlater": "", "printlater": "",
@@ -1047,7 +1047,9 @@
}, },
"dms": { "dms": {
"errors": { "errors": {
"alreadyexported": "" "alreadyexported": "",
"earlyrorequired": "",
"earlyrorequired.message": ""
}, },
"labels": { "labels": {
"refreshallocations": "" "refreshallocations": ""
@@ -1244,9 +1246,11 @@
"deselectall": "", "deselectall": "",
"download": "", "download": "",
"edit": "modifier", "edit": "modifier",
"gotoadmin": "",
"login": "", "login": "",
"next": "", "next": "",
"ok": "", "ok": "",
"optional": "",
"previous": "", "previous": "",
"print": "", "print": "",
"refresh": "", "refresh": "",
@@ -1257,6 +1261,7 @@
"save": "sauvegarder", "save": "sauvegarder",
"saveandnew": "", "saveandnew": "",
"saveas": "", "saveas": "",
"select": "",
"selectall": "", "selectall": "",
"send": "", "send": "",
"sendbysms": "", "sendbysms": "",
@@ -1286,8 +1291,7 @@
"vehicle": "" "vehicle": ""
}, },
"labels": { "labels": {
"selected": "", "apply": "",
"settings": "",
"actions": "actes", "actions": "actes",
"areyousure": "", "areyousure": "",
"barcode": "code à barre", "barcode": "code à barre",
@@ -1341,8 +1345,10 @@
"search": "Chercher...", "search": "Chercher...",
"searchresults": "", "searchresults": "",
"selectdate": "", "selectdate": "",
"selected": "",
"sendagain": "", "sendagain": "",
"sendby": "", "sendby": "",
"settings": "",
"signin": "", "signin": "",
"sms": "", "sms": "",
"status": "", "status": "",
@@ -1585,13 +1591,13 @@
"labels": { "labels": {
"adjustmenttobeadded": "", "adjustmenttobeadded": "",
"billref": "", "billref": "",
"bulk_location_help": "",
"convertedtolabor": "", "convertedtolabor": "",
"edit": "Ligne d'édition", "edit": "Ligne d'édition",
"ioucreated": "", "ioucreated": "",
"new": "Nouvelle ligne", "new": "Nouvelle ligne",
"nostatus": "", "nostatus": "",
"presets": "", "presets": ""
"bulk_location_help": ""
}, },
"successes": { "successes": {
"created": "", "created": "",
@@ -1619,11 +1625,13 @@
"changestatus": "Changer le statut", "changestatus": "Changer le statut",
"changestimator": "", "changestimator": "",
"convert": "Convertir", "convert": "Convertir",
"convertwithoutearlyro": "",
"createiou": "", "createiou": "",
"deliver": "", "deliver": "",
"deliver_quick": "", "deliver_quick": "",
"dms": { "dms": {
"addpayer": "", "addpayer": "",
"createearlyro": "",
"createnewcustomer": "", "createnewcustomer": "",
"findmakemodelcode": "", "findmakemodelcode": "",
"getmakes": "", "getmakes": "",
@@ -1632,6 +1640,7 @@
}, },
"post": "", "post": "",
"refetchmakesmodels": "", "refetchmakesmodels": "",
"update_ro": "",
"usegeneric": "", "usegeneric": "",
"useselected": "" "useselected": ""
}, },
@@ -1699,9 +1708,9 @@
"actual_delivery": "Livraison réelle", "actual_delivery": "Livraison réelle",
"actual_in": "En réel", "actual_in": "En réel",
"acv_amount": "", "acv_amount": "",
"admin_clerk": "",
"adjustment_bottom_line": "Ajustements", "adjustment_bottom_line": "Ajustements",
"adjustmenthours": "", "adjustmenthours": "",
"admin_clerk": "",
"alt_transport": "", "alt_transport": "",
"area_of_damage_impact": { "area_of_damage_impact": {
"10": "", "10": "",
@@ -1782,9 +1791,8 @@
"ded_status": "Statut de franchise", "ded_status": "Statut de franchise",
"depreciation_taxes": "Amortissement / taxes", "depreciation_taxes": "Amortissement / taxes",
"dms": { "dms": {
"first_name": "",
"last_name": "",
"address": "", "address": "",
"advisor": "",
"amount": "", "amount": "",
"center": "", "center": "",
"control_type": { "control_type": {
@@ -1792,29 +1800,36 @@
}, },
"cost": "", "cost": "",
"cost_dms_acctnumber": "", "cost_dms_acctnumber": "",
"customer": "",
"dms_make": "", "dms_make": "",
"dms_model": "", "dms_model": "",
"dms_model_override": "", "dms_model_override": "",
"make_override": "",
"advisor": "",
"dms_unsold": "", "dms_unsold": "",
"dms_wip_acctnumber": "", "dms_wip_acctnumber": "",
"first_name": "",
"id": "", "id": "",
"inservicedate": "", "inservicedate": "",
"journal": "", "journal": "",
"last_name": "",
"lines": "", "lines": "",
"make_override": "",
"name1": "", "name1": "",
"payer": { "payer": {
"amount": "", "amount": "",
"control_type": "", "control_type": "",
"controlnumber": "", "controlnumber": "",
"dms_acctnumber": "", "dms_acctnumber": "",
"name": "" "name": "",
"payer_type": ""
}, },
"sale": "", "sale": "",
"sale_dms_acctnumber": "", "sale_dms_acctnumber": "",
"story": "", "story": "",
"vinowner": "" "vinowner": "",
"rr_opcode": "",
"rr_opcode_prefix": "",
"rr_opcode_suffix": "",
"rr_opcode_base": ""
}, },
"dms_allocation": "", "dms_allocation": "",
"driveable": "", "driveable": "",
@@ -2099,6 +2114,11 @@
"damageto": "", "damageto": "",
"defaultstory": "", "defaultstory": "",
"disablebillwip": "", "disablebillwip": "",
"earlyro": {
"created": "",
"fields": "",
"willupdate": ""
},
"invoicedatefuture": "", "invoicedatefuture": "",
"kmoutnotgreaterthankmin": "", "kmoutnotgreaterthankmin": "",
"logs": "", "logs": "",
@@ -2256,6 +2276,7 @@
"delete": "", "delete": "",
"deleted": "Le travail a bien été supprimé.", "deleted": "Le travail a bien été supprimé.",
"duplicated": "", "duplicated": "",
"early_ro_created": "",
"exported": "", "exported": "",
"invoiced": "", "invoiced": "",
"ioucreated": "", "ioucreated": "",
@@ -2433,7 +2454,6 @@
"actions": { "actions": {
"link": "", "link": "",
"new": "", "new": "",
"openchat": "" "openchat": ""
}, },
"errors": { "errors": {
@@ -2445,6 +2465,7 @@
"labels": { "labels": {
"addlabel": "", "addlabel": "",
"archive": "", "archive": "",
"mark_unread": "",
"maxtenimages": "", "maxtenimages": "",
"messaging": "Messagerie", "messaging": "Messagerie",
"no_consent": "", "no_consent": "",
@@ -2457,8 +2478,7 @@
"selectmedia": "", "selectmedia": "",
"sentby": "", "sentby": "",
"typeamessage": "Envoyer un message...", "typeamessage": "Envoyer un message...",
"unarchive": "", "unarchive": ""
"mark_unread": ""
}, },
"render": { "render": {
"conversation_list": "" "conversation_list": ""
@@ -2612,20 +2632,20 @@
"name": "" "name": ""
}, },
"labels": { "labels": {
"cell": "",
"create_new": "Créez un nouvel enregistrement de propriétaire.", "create_new": "Créez un nouvel enregistrement de propriétaire.",
"deleteconfirm": "", "deleteconfirm": "",
"email": "",
"existing_owners": "Propriétaires existants", "existing_owners": "Propriétaires existants",
"fromclaim": "", "fromclaim": "",
"fromowner": "", "fromowner": "",
"relatedjobs": "",
"updateowner": "",
"work": "",
"home": "", "home": "",
"cell": "",
"other": "", "other": "",
"email": "",
"phone": "", "phone": "",
"sms": "" "relatedjobs": "",
"sms": "",
"updateowner": "",
"work": ""
}, },
"successes": { "successes": {
"delete": "", "delete": "",
@@ -2636,6 +2656,10 @@
"actions": { "actions": {
"order": "Commander des pièces", "order": "Commander des pièces",
"orderinhouse": "" "orderinhouse": ""
},
"labels": {
"view_counts_only": "",
"view_timestamps": ""
} }
}, },
"parts_dispatch": { "parts_dispatch": {
@@ -2985,8 +3009,6 @@
"settings": "" "settings": ""
}, },
"labels": { "labels": {
"click_for_statuses": "",
"partsreceived": "",
"actual_in": "", "actual_in": "",
"addnewprofile": "", "addnewprofile": "",
"alert": "", "alert": "",
@@ -3005,6 +3027,7 @@
"card_size": "", "card_size": "",
"cardcolor": "", "cardcolor": "",
"cardsettings": "", "cardsettings": "",
"click_for_statuses": "",
"clm_no": "", "clm_no": "",
"comment": "", "comment": "",
"compact": "", "compact": "",
@@ -3025,6 +3048,7 @@
"orientation": "", "orientation": "",
"ownr_nm": "", "ownr_nm": "",
"paintpriority": "", "paintpriority": "",
"partsreceived": "",
"partsstatus": "", "partsstatus": "",
"production_note": "", "production_note": "",
"refinishhours": "", "refinishhours": "",
@@ -3571,18 +3595,12 @@
} }
}, },
"titles": { "titles": {
"simplified-parts-jobs": "",
"parts": "",
"parts_settings": "",
"accounting-payables": "", "accounting-payables": "",
"accounting-payments": "", "accounting-payments": "",
"accounting-receivables": "", "accounting-receivables": "",
"all_tasks": "", "all_tasks": "",
"app": "", "app": "",
"bc": { "bc": {
"simplified-parts-jobs": "",
"parts": "",
"parts_settings": "",
"accounting-payables": "", "accounting-payables": "",
"accounting-payments": "", "accounting-payments": "",
"accounting-receivables": "", "accounting-receivables": "",
@@ -3614,7 +3632,9 @@
"my_tasks": "", "my_tasks": "",
"owner-detail": "", "owner-detail": "",
"owners": "", "owners": "",
"parts": "",
"parts-queue": "", "parts-queue": "",
"parts_settings": "",
"payments-all": "", "payments-all": "",
"phonebook": "", "phonebook": "",
"productionboard": "", "productionboard": "",
@@ -3626,6 +3646,7 @@
"shop-csi": "", "shop-csi": "",
"shop-templates": "", "shop-templates": "",
"shop-vendors": "", "shop-vendors": "",
"simplified-parts-jobs": "",
"tasks": "", "tasks": "",
"temporarydocs": "", "temporarydocs": "",
"timetickets": "", "timetickets": "",
@@ -3661,7 +3682,9 @@
"my_tasks": "", "my_tasks": "",
"owners": "Tous les propriétaires | {{app}}", "owners": "Tous les propriétaires | {{app}}",
"owners-detail": "", "owners-detail": "",
"parts": "",
"parts-queue": "", "parts-queue": "",
"parts_settings": "",
"payments-all": "", "payments-all": "",
"phonebook": "", "phonebook": "",
"productionboard": "", "productionboard": "",
@@ -3677,6 +3700,7 @@
"shop-csi": "", "shop-csi": "",
"shop-templates": "", "shop-templates": "",
"shop_vendors": "Vendeurs | {{app}}", "shop_vendors": "Vendeurs | {{app}}",
"simplified-parts-jobs": "",
"tasks": "", "tasks": "",
"techconsole": "{{app}}", "techconsole": "{{app}}",
"techjobclock": "{{app}}", "techjobclock": "{{app}}",
@@ -3837,10 +3861,10 @@
"user": { "user": {
"actions": { "actions": {
"changepassword": "", "changepassword": "",
"signout": "Déconnexion", "dark_theme": "",
"updateprofile": "Mettre à jour le profil",
"light_theme": "", "light_theme": "",
"dark_theme": "" "signout": "Déconnexion",
"updateprofile": "Mettre à jour le profil"
}, },
"errors": { "errors": {
"updating": "" "updating": ""
@@ -3854,14 +3878,14 @@
"labels": { "labels": {
"actions": "", "actions": "",
"changepassword": "", "changepassword": "",
"profileinfo": "",
"user_settings": "",
"play_sound_for_new_messages": "",
"notification_sound_on": "",
"notification_sound_off": "",
"notification_sound_enabled": "",
"notification_sound_disabled": "", "notification_sound_disabled": "",
"notification_sound_help": "" "notification_sound_enabled": "",
"notification_sound_help": "",
"notification_sound_off": "",
"notification_sound_on": "",
"play_sound_for_new_messages": "",
"profileinfo": "",
"user_settings": ""
}, },
"successess": { "successess": {
"passwordchanged": "" "passwordchanged": ""

View File

@@ -1,44 +1,43 @@
import { Select } from "antd";
import i18n from "../translations/i18n"; import i18n from "../translations/i18n";
export default function CiecaSelect(parts = true, labor = true) { export default function CiecaSelect(parts = true, labor = true) {
return ( const options = [];
<>
{labor && ( if (labor) {
<> options.push(
<Select.Option value="LAA">{i18n.t("joblines.fields.lbr_types.LAA")}</Select.Option> { value: "LAA", label: i18n.t("joblines.fields.lbr_types.LAA") },
<Select.Option value="LAB">{i18n.t("joblines.fields.lbr_types.LAB")}</Select.Option> { value: "LAB", label: i18n.t("joblines.fields.lbr_types.LAB") },
<Select.Option value="LAD">{i18n.t("joblines.fields.lbr_types.LAD")}</Select.Option> { value: "LAD", label: i18n.t("joblines.fields.lbr_types.LAD") },
<Select.Option value="LAE">{i18n.t("joblines.fields.lbr_types.LAE")}</Select.Option> { value: "LAE", label: i18n.t("joblines.fields.lbr_types.LAE") },
<Select.Option value="LAF">{i18n.t("joblines.fields.lbr_types.LAF")}</Select.Option> { value: "LAF", label: i18n.t("joblines.fields.lbr_types.LAF") },
<Select.Option value="LAG">{i18n.t("joblines.fields.lbr_types.LAG")}</Select.Option> { value: "LAG", label: i18n.t("joblines.fields.lbr_types.LAG") },
<Select.Option value="LAM">{i18n.t("joblines.fields.lbr_types.LAM")}</Select.Option> { value: "LAM", label: i18n.t("joblines.fields.lbr_types.LAM") },
<Select.Option value="LAR">{i18n.t("joblines.fields.lbr_types.LAR")}</Select.Option> { value: "LAR", label: i18n.t("joblines.fields.lbr_types.LAR") },
<Select.Option value="LAS">{i18n.t("joblines.fields.lbr_types.LAS")}</Select.Option> { value: "LAS", label: i18n.t("joblines.fields.lbr_types.LAS") },
<Select.Option value="LAU">{i18n.t("joblines.fields.lbr_types.LAU")}</Select.Option> { value: "LAU", label: i18n.t("joblines.fields.lbr_types.LAU") },
<Select.Option value="LA1">{i18n.t("joblines.fields.lbr_types.LA1")}</Select.Option> { value: "LA1", label: i18n.t("joblines.fields.lbr_types.LA1") },
<Select.Option value="LA2">{i18n.t("joblines.fields.lbr_types.LA2")}</Select.Option> { value: "LA2", label: i18n.t("joblines.fields.lbr_types.LA2") },
<Select.Option value="LA3">{i18n.t("joblines.fields.lbr_types.LA3")}</Select.Option> { value: "LA3", label: i18n.t("joblines.fields.lbr_types.LA3") },
<Select.Option value="LA4">{i18n.t("joblines.fields.lbr_types.LA4")}</Select.Option> { value: "LA4", label: i18n.t("joblines.fields.lbr_types.LA4") }
</> );
)} }
{parts && (
<> if (parts) {
<Select.Option value="PAA">{i18n.t("joblines.fields.part_types.PAA")}</Select.Option> options.push(
<Select.Option value="PAC">{i18n.t("joblines.fields.part_types.PAC")}</Select.Option> { value: "PAA", label: i18n.t("joblines.fields.part_types.PAA") },
{ value: "PAC", label: i18n.t("joblines.fields.part_types.PAC") },
<Select.Option value="PAL">{i18n.t("joblines.fields.part_types.PAL")}</Select.Option> { value: "PAL", label: i18n.t("joblines.fields.part_types.PAL") },
<Select.Option value="PAG">{i18n.t("joblines.fields.part_types.PAG")}</Select.Option> { value: "PAG", label: i18n.t("joblines.fields.part_types.PAG") },
<Select.Option value="PAM">{i18n.t("joblines.fields.part_types.PAM")}</Select.Option> { value: "PAM", label: i18n.t("joblines.fields.part_types.PAM") },
<Select.Option value="PAP">{i18n.t("joblines.fields.part_types.PAP")}</Select.Option> { value: "PAP", label: i18n.t("joblines.fields.part_types.PAP") },
<Select.Option value="PAN">{i18n.t("joblines.fields.part_types.PAN")}</Select.Option> { value: "PAN", label: i18n.t("joblines.fields.part_types.PAN") },
<Select.Option value="PAO">{i18n.t("joblines.fields.part_types.PAO")}</Select.Option> { value: "PAO", label: i18n.t("joblines.fields.part_types.PAO") },
<Select.Option value="PAR">{i18n.t("joblines.fields.part_types.PAR")}</Select.Option> { value: "PAR", label: i18n.t("joblines.fields.part_types.PAR") },
<Select.Option value="PAS">{i18n.t("joblines.fields.part_types.PAS")}</Select.Option> { value: "PAS", label: i18n.t("joblines.fields.part_types.PAS") }
</> );
)} }
</>
); return options;
} }
export function GetPartTypeName(part_type) { export function GetPartTypeName(part_type) {

View File

@@ -5,8 +5,10 @@ export function DateFormatter(props) {
return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : null; return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : null;
} }
export function DateTimeFormatter(props) { export function DateTimeFormatter({ hideTime, ...props }) {
return props.children ? dayjs(props.children).format(props.format ? props.format : "MM/DD/YYYY hh:mm a") : null; return props.children
? dayjs(props.children).format(props.format ? props.format : `MM/DD/YYYY${hideTime ? "" : " hh:mm a"}`)
: null;
} }
export function DateTimeFormatterFunction(date) { export function DateTimeFormatterFunction(date) {
@@ -17,11 +19,11 @@ export function TimeFormatter(props) {
return props.children ? dayjs(props.children).format(props.format ? props.format : "hh:mm a") : null; return props.children ? dayjs(props.children).format(props.format ? props.format : "hh:mm a") : null;
} }
export function TimeAgoFormatter(props) { export function TimeAgoFormatter({ removeAgoString = false, ...props }) {
const m = dayjs(props.children); const m = dayjs(props.children);
return props.children ? ( return props.children ? (
<Tooltip placement="top" title={m.format("MM/DD/YYY hh:mm A")}> <Tooltip placement="top" title={m.format("MM/DD/YYYY hh:mm A")}>
{m.fromNow()} {m.fromNow(removeAgoString)}
</Tooltip> </Tooltip>
) : null; ) : null;
} }

View File

@@ -248,7 +248,8 @@ const client = new ApolloClient({
watchQuery: { watchQuery: {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
errorPolicy: "ignore" errorPolicy: "ignore",
notifyOnNetworkStatusChange: false
}, },
query: { query: {
fetchPolicy: "network-only", fetchPolicy: "network-only",

View File

@@ -146,7 +146,8 @@ export async function generateTemplate(
if (templateQueryToExecute) { if (templateQueryToExecute) {
const { data } = await client.query({ const { data } = await client.query({
query: gql(finalQuery), query: gql(finalQuery),
variables: { ...templateObject.variables } variables: { ...templateObject.variables },
fetchPolicy: "no-cache"
}); });
contextData = data; contextData = data;
} }

View File

@@ -38,8 +38,6 @@ services:
condition: service_healthy condition: service_healthy
localstack: localstack:
condition: service_healthy condition: service_healthy
aws-cli:
condition: service_completed_successfully
ports: ports:
- "4001:4000" # Different external port for local access - "4001:4000" # Different external port for local access
volumes: volumes:
@@ -65,8 +63,6 @@ services:
condition: service_healthy condition: service_healthy
localstack: localstack:
condition: service_healthy condition: service_healthy
aws-cli:
condition: service_completed_successfully
ports: ports:
- "4002:4000" # Different external port for local access - "4002:4000" # Different external port for local access
volumes: volumes:
@@ -92,8 +88,6 @@ services:
condition: service_healthy condition: service_healthy
localstack: localstack:
condition: service_healthy condition: service_healthy
aws-cli:
condition: service_completed_successfully
ports: ports:
- "4003:4000" # Different external port for local access - "4003:4000" # Different external port for local access
volumes: volumes:
@@ -156,23 +150,18 @@ services:
# LocalStack # LocalStack
localstack: localstack:
image: localstack/localstack image: localstack/localstack:4.13.1
container_name: localstack container_name: localstack
hostname: localstack hostname: localstack
networks: networks:
- redis-cluster-net - redis-cluster-net
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./certs:/tmp/certs:ro # only if your script reads /tmp/certs/...
- ./localstack/init:/etc/localstack/init/ready.d:ro
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
environment: env_file:
- SERVICES=s3,ses,secretsmanager,cloudwatch,logs - .env.localstack.docker
- DEBUG=0
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=ca-central-1
- EXTRA_CORS_ALLOWED_HEADERS=Authorization,Content-Type
- EXTRA_CORS_ALLOWED_ORIGINS=*
- EXTRA_CORS_EXPOSE_HEADERS=Authorization,Content-Type
ports: ports:
- "4566:4566" - "4566:4566"
healthcheck: healthcheck:
@@ -182,36 +171,6 @@ services:
retries: 5 retries: 5
start_period: 20s start_period: 20s
# AWS-CLI
aws-cli:
image: amazon/aws-cli
container_name: aws-cli
hostname: aws-cli
networks:
- redis-cluster-net
depends_on:
localstack:
condition: service_healthy
volumes:
- './localstack:/tmp/localstack'
- './certs:/tmp/certs'
environment:
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=ca-central-1
entrypoint: /bin/sh -c
command: >
"
aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1
aws --endpoint-url=http://localstack:4566 ses verify-email-identity --email-address noreply@imex.online --region ca-central-1
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rome-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rps-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
"
networks: networks:
redis-cluster-net: redis-cluster-net:
driver: bridge driver: bridge

View File

@@ -68,23 +68,18 @@ services:
# LocalStack: Used to emulate AWS services locally, currently setup for SES # LocalStack: Used to emulate AWS services locally, currently setup for SES
# Notes: Set the ENV Debug to 1 for additional logging # Notes: Set the ENV Debug to 1 for additional logging
localstack: localstack:
image: localstack/localstack image: localstack/localstack:4.13.1
container_name: localstack container_name: localstack
hostname: localstack hostname: localstack
networks: networks:
- redis-cluster-net - redis-cluster-net
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./certs:/tmp/certs:ro # only if your script reads /tmp/certs/...
- ./localstack/init:/etc/localstack/init/ready.d:ro
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
environment: env_file:
- SERVICES=s3,ses,secretsmanager,cloudwatch,logs - .env.localstack.docker
- DEBUG=0
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=ca-central-1
- EXTRA_CORS_ALLOWED_HEADERS=Authorization,Content-Type
- EXTRA_CORS_ALLOWED_ORIGINS=*
- EXTRA_CORS_EXPOSE_HEADERS=Authorization,Content-Type
ports: ports:
- "4566:4566" - "4566:4566"
healthcheck: healthcheck:
@@ -94,38 +89,6 @@ services:
retries: 5 retries: 5
start_period: 20s start_period: 20s
# AWS-CLI - Used in conjunction with LocalStack to set required permission to send emails
aws-cli:
image: amazon/aws-cli
container_name: aws-cli
hostname: aws-cli
networks:
- redis-cluster-net
depends_on:
localstack:
condition: service_healthy
volumes:
- './localstack:/tmp/localstack'
- './certs:/tmp/certs'
environment:
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=ca-central-1
entrypoint: /bin/sh -c
command: >
"
aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1
aws --endpoint-url=http://localstack:4566 ses verify-email-identity --email-address noreply@imex.online --region ca-central-1
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-job-totals --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket parts-estimates --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rome-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rps-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
"
# Node App: The Main IMEX API # Node App: The Main IMEX API
node-app: node-app:
build: build:
@@ -145,8 +108,7 @@ services:
condition: service_healthy condition: service_healthy
localstack: localstack:
condition: service_healthy condition: service_healthy
aws-cli:
condition: service_completed_successfully
ports: ports:
- "4000:4000" - "4000:4000"
- "9229:9229" - "9229:9229"

View File

@@ -947,6 +947,7 @@
- carfax_exclude - carfax_exclude
- cdk_configuration - cdk_configuration
- cdk_dealerid - cdk_dealerid
- chatter_company_id
- chatterid - chatterid
- city - city
- claimscorpid - claimscorpid
@@ -1063,6 +1064,7 @@
- bill_allow_post_to_closed - bill_allow_post_to_closed
- bill_tax_rates - bill_tax_rates
- cdk_configuration - cdk_configuration
- chatter_company_id
- city - city
- country - country
- created_at - created_at
@@ -3702,7 +3704,9 @@
- ded_status - ded_status
- deliverchecklist - deliverchecklist
- depreciation_taxes - depreciation_taxes
- dms_advisor_id
- dms_allocation - dms_allocation
- dms_customer_id
- dms_id - dms_id
- driveable - driveable
- employee_body - employee_body
@@ -3983,7 +3987,9 @@
- ded_status - ded_status
- deliverchecklist - deliverchecklist
- depreciation_taxes - depreciation_taxes
- dms_advisor_id
- dms_allocation - dms_allocation
- dms_customer_id
- dms_id - dms_id
- driveable - driveable
- employee_body - employee_body
@@ -4276,7 +4282,9 @@
- ded_status - ded_status
- deliverchecklist - deliverchecklist
- depreciation_taxes - depreciation_taxes
- dms_advisor_id
- dms_allocation - dms_allocation
- dms_customer_id
- dms_id - dms_id
- driveable - driveable
- employee_body - employee_body

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