Compare commits

...

111 Commits

Author SHA1 Message Date
Dave
784378a999 feature/IO-3571-Create-Job-Done-Loading - Fix set is submitting 2026-03-09 12:53:59 -04:00
Allan Carr
0d502d4dd4 IO-3571 Create Job Done Button Loading
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-03-06 18:31:22 -08:00
Dave Richer
8980d3716b Merged in release/2026-03-13 (pull request #3092)
release/2026-02-27 - Final RR debug fix [FRONT END NOT REQUIRED]

Approved-by: Allan Carr
2026-03-04 20:31:22 +00:00
Dave Richer
764ec5f8f9 Merged in release/2026-02-27 (pull request #3091)
release/2026-02-27 - Final RR debug fix
2026-03-04 20:20:49 +00:00
Dave
a7a7551dae release/2026-02-27 - Final RR debug fix 2026-03-04 15:17:56 -05:00
Dave Richer
571536a7ec Merged in master-AIO (pull request #3089)
release/2026-02-27 - bump
2026-03-04 19:28:47 +00:00
Dave Richer
20e56fff6a Merged in release/2026-02-27 (pull request #3088)
release/2026-02-27 - bump
2026-03-04 19:28:21 +00:00
Dave
8f132ca14d release/2026-02-27 - bump 2026-03-04 14:27:23 -05:00
Dave Richer
99c002dac1 Merged in master-AIO (pull request #3086)
Master AIO
2026-03-04 19:24:37 +00:00
Dave Richer
0cd30ccdec Merged in release/2026-02-27 (pull request #3085)
Release/2026 02 27
2026-03-04 17:45:15 +00:00
Patrick Fic
acd69276a5 Merged in release/revert-revert-pr-3080 (pull request #3083)
Revert "Revert "Release/2026 02 27 (pull request #3070)" (pull request #3080)"
2026-03-04 17:41:44 +00:00
Patrick Fic
faf5878bdf Revert "Revert "Release/2026 02 27 (pull request #3070)" (pull request #3080)" 2026-03-04 17:41:10 +00:00
Dave
f56a540b2f release/2026-02-27 - Fix Time ticket issue, add additional logging around reynolds 2026-03-04 12:21:29 -05:00
Dave
e251e5f8f6 release/2026-02-27 - Disable Responsive Design 2026-03-04 11:38:33 -05:00
Patrick Fic
5a55798d2d Merged in release/revert-pr-3070-2026-03-04 (pull request #3080)
Revert "Release/2026 02 27 (pull request #3070)"
2026-03-04 16:20:15 +00:00
Patrick Fic
c9e41ba72a Revert "Release/2026 02 27 (pull request #3070)" 2026-03-04 16:18:44 +00:00
Dave Richer
522f2b9e26 Merged in release/2026-02-27 (pull request #3070)
Release/2026 02 27
2026-03-04 01:41:53 +00:00
Allan Carr
be9267ddd4 Merged in feature/IO-3594-Kaizen-Datapump-Enhancement (pull request #3076)
Feature/IO-3594 Kaizen Datapump Enhancement

Approved-by: Dave Richer
2026-03-04 00:51:11 +00:00
Patrick Fic
e4a79b51c7 Merged in feature/IO-3515-ocr-bill-posting (pull request #3077)
Feature/IO-3515 ocr bill posting
2026-03-03 22:56:52 +00:00
Patrick Fic
47a9a963fa IO-3515 Minor improvements to Bill AI. 2026-03-03 14:56:28 -08:00
Allan Carr
f3c7a831a1 IO-3594 Kaizen Datapump Enhancement
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-03-03 13:18:16 -08:00
Allan Carr
6ac9310e81 IO-3594 Kaizen Datapump Enhancement
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-03-03 13:17:56 -08:00
Dave
b91e65be0e release/2026-02-27 - Add gating 2026-03-03 15:25:13 -05:00
Dave
3f2358e30c Merge remote-tracking branch 'origin/hotfix/2026-03-03' into release/2026-02-27 2026-03-03 13:08:31 -05:00
Dave Richer
ce02d90c3c Merged in hotfix/2026-03-03-RR-logging-Posting-Enhancements (pull request #3072)
hotfix/2026-03-03-RR-logging-Posting-Enhancements - Implement
2026-03-03 18:05:06 +00:00
Allan Carr
95a71bea6e Merged in feature/IO-3585-Fortellis-Insert-and-Update-Vehicle (pull request #3071)
IO-3585 Fortellis Insert and Update Vehicle Info

Approved-by: Dave Richer
2026-03-03 18:03:41 +00:00
Dave
3b27120d77 hotfix/2026-03-03-RR-logging-Posting-Enhancements - Implement 2026-03-03 13:03:02 -05:00
Dave Richer
f350163056 Merged in feature/IO-3554-Form-Row-Layout (pull request #3068)
feature/IO-3554-Form-Row-Layout - dial in tables
2026-03-02 17:00:21 +00:00
Dave
db4d286a86 feature/IO-3554-Form-Row-Layout - dial in tables 2026-03-02 11:59:32 -05:00
Dave Richer
57cfecb7b8 Merged in feature/IO-3554-Form-Row-Layout (pull request #3066)
feature/IO-3554-Form-Row-Layout - Modify how truncation works on responsive tables.
2026-03-02 16:29:45 +00:00
Dave
56c24e3450 feature/IO-3554-Form-Row-Layout - Modify how truncation works on responsive tables. 2026-03-02 11:29:06 -05:00
Allan Carr
9a41cfd6af Merged in feature/IO-3585-Fortellis-Insert-and-Update-Vehicle (pull request #3064)
IO-3585 Fortellis Insert and Update Vehicle Info

Approved-by: Dave Richer
2026-03-02 15:44:29 +00:00
Dave
2934da4be9 Merge branch 'feature/IO-3586-Socket-Reconnect-Issues' into release/2026-02-27 2026-03-02 10:43:42 -05:00
Allan Carr
c2fb010a59 IO-3585 Fortellis Insert and Update Vehicle Info
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-03-01 22:03:23 -08:00
Dave Richer
ccba7b0137 Merged in hotfix/2026-06-27 (pull request #3061)
hotfix/2026-02-27 - Reynolds Estimate amounts on second call, + RR Docs in _ref
2026-02-27 21:15:52 +00:00
Dave Richer
c116007042 Merged in feature/IO-3554-Form-Row-Layout (pull request #3059)
feature/feature/IO-3554-Form-Row-Layout - Fix Monthly Job Costing (Dashboard)
2026-02-27 18:09:12 +00:00
Dave
31c7abab39 feature/feature/IO-3554-Form-Row-Layout - Fix Monthly Job Costing (Dashboard) 2026-02-27 13:08:34 -05:00
Dave Richer
589e537c94 Merged in feature/IO-3554-Form-Row-Layout (pull request #3057)
feature/feature/IO-3554-Form-Row-Layout - Revert Monthly Job Costing Table
2026-02-27 15:54:54 +00:00
Dave
b2f471fe9a feature/feature/IO-3554-Form-Row-Layout - Revert Monthly Job Costing Table 2026-02-27 10:53:57 -05:00
Dave Richer
7ea4f96664 Merged in feature/IO-3554-Form-Row-Layout (pull request #3055)
feature/feature/IO-3554-Form-Row-Layout - Responsive overhaul
2026-02-26 21:01:30 +00:00
Dave
fd6f46e39d feature/feature/IO-3554-Form-Row-Layout - Responsive overhaul 2026-02-26 15:56:57 -05:00
Dave Richer
0b505b3b4b Merged in feature/IO-3554-Form-Row-Layout (pull request #3053)
feature/feature/IO-3554-Form-Row-Layout - Drawer changes, mask closable changes, missing translation, spinner tip changes
2026-02-25 20:59:04 +00:00
Dave
226cc801ae feature/feature/IO-3554-Form-Row-Layout - Drawer changes, mask closable changes, missing translation, spinner tip changes 2026-02-25 15:57:51 -05:00
Dave Richer
67396afeb7 Merged in feature/IO-3554-Form-Row-Layout (pull request #3051)
feature/feature/IO-3554-Form-Row-Layout - Fix search, bump deps, fix formlayout
2026-02-25 20:32:13 +00:00
Dave
dab66b4d66 feature/feature/IO-3554-Form-Row-Layout - Fix search, bump deps, fix formlayout 2026-02-25 15:31:42 -05:00
Dave Richer
20d51431e7 Merged in feature/IO-3554-Form-Row-Layout (pull request #3049)
feature/feature/IO-3554-Form-Row-Layout - Fix search, bump deps, fix formlayout
2026-02-25 20:23:01 +00:00
Dave
15bb1e72a2 feature/feature/IO-3554-Form-Row-Layout - Fix search, bump deps, fix formlayout 2026-02-25 15:22:39 -05:00
Dave Richer
5edab6d040 Merged in feature/IO-3554-Form-Row-Layout (pull request #3047)
feature/feature/IO-3554-Form-Row-Layout - Fix search, bump deps, fix formlayout
2026-02-25 20:04:19 +00:00
Dave
48017e7471 feature/feature/IO-3554-Form-Row-Layout - Fix search, bump deps, fix formlayout 2026-02-25 15:03:53 -05:00
Dave Richer
acb1cc6367 Merged in feature/IO-3554-Form-Row-Layout (pull request #3004)
Responsive Part 1 - Form Layout
2026-02-25 19:48:47 +00:00
Dave
77befd5d93 feature/feature/IO-3554-Form-Row-Layout - Fix search, bump deps, fix formlayout 2026-02-25 14:47:49 -05:00
Dave
c93b8ed961 Merge remote-tracking branch 'origin/release/2026-02-27' into feature/IO-3554-Form-Row-Layout 2026-02-25 13:56:40 -05:00
Allan Carr
4d58c46a33 Merged in feature/IO-3578-Fortellis-Regex-Fix (pull request #3041)
IO-3578 Fortellis Regex Fix
2026-02-25 00:30:33 +00:00
Dave Richer
7299020bd8 Merged in release/2026-02-27 (pull request #3040)
Release/2026 02 27
2026-02-24 18:08:52 +00:00
Dave
f16a0c491b Merge remote-tracking branch 'origin/feature/IO-3537-Bill-Entry-Scroll-to-Top-for-Errors' into release/2026-02-27 2026-02-24 11:08:46 -05:00
Allan Carr
ae52f12bae Merged in feature/IO-3560-Part-Number-on-Return-Item-Modal (pull request #3035)
IO-3560 Part # on Return Item Modal

Approved-by: Dave Richer
2026-02-24 15:26:39 +00:00
Allan Carr
11475afdb1 Merged in feature/IO-3573-Enhanced-Payroll-Labor-Allocations (pull request #3034)
IO-3573 Enhanced Payroll Labor Allocations

Approved-by: Dave Richer
2026-02-24 15:26:07 +00:00
Allan Carr
7a5e722ec1 Merged in feature/IO-3575-Flat-Rate-ATS-PST-Exempt (pull request #3033)
IO-3575 Extend Audit Trail for Tax Rates and Flat ATS

Approved-by: Dave Richer
2026-02-24 15:25:40 +00:00
Allan Carr
7c686e38da Merge branch 'release/2026-02-27' into feature/IO-3537-Bill-Entry-Scroll-to-Top-for-Errors
Signed-off-by: Allan Carr <allan@imexsystems.ca>

# Conflicts:
#	client/src/components/bill-enter-modal/bill-enter-modal.container.jsx
2026-02-23 23:56:28 -08:00
Allan Carr
9eaf45ac88 IO-3537 Bill Entry Scroll to Top for Errors
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-02-23 23:44:24 -08:00
Allan Carr
8cd2e65305 IO-3560 Part # on Return Item Modal
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-02-23 19:09:35 -08:00
Allan Carr
da9744da6f IO-3573 Enhanced Payroll Labor Allocation
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-02-23 18:33:57 -08:00
Allan Carr
947ded4b5e IO-3573 Enhanced Payroll Labor Allocations
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-02-23 18:20:57 -08:00
Allan Carr
6e6304124b IO-3575 Extend Audit Trail for Tax Rates and Flat ATS
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-02-23 17:59:39 -08:00
Allan Carr
2f694c2638 Merged in feature/IO-3576-Fortellis-Refetch-Make-Model (pull request #3029)
IO-3576 Fortellis Refetch Make Model
2026-02-23 23:27:41 +00:00
Patrick Fic
5f8a08b0a7 IO-3515 Limit logging. 2026-02-23 11:55:22 -08:00
Patrick Fic
fd7970df2c Merge branch 'feature/IO-3515-ocr-bill-posting' into release/2026-02-27 2026-02-23 11:28:45 -08:00
Patrick Fic
03ad66b2a2 IO-3515 PR Comments addressed. 2026-02-20 09:06:11 -08:00
Patrick Fic
6f80e6dcbf IO-3515 fix notifications & auto attach document. 2026-02-19 15:36:40 -08:00
Patrick Fic
21f43285bc IO-3515 additional cleanup, translations 2026-02-19 14:15:57 -08:00
Patrick Fic
b2bc19c5c9 IO-3515 Add translations and logging. 2026-02-19 13:54:39 -08:00
Allan Carr
e075361e23 Merged in feature/IO-3570-Fortellis-Multiple-Veh-Records (pull request #3025)
IO-3570 Strip - from Owner Name in regex
2026-02-19 21:17:44 +00:00
Allan Carr
a6327912ab Merged in feature/IO-3570-Fortellis-Multiple-Veh-Records (pull request #3021)
IO-3570 Fortellis Owner Phone Search
2026-02-19 20:35:41 +00:00
Patrick Fic
ae1408012f IO-3515 resolve issues on search selects not updating, improve confidence scoring. 2026-02-19 12:22:35 -08:00
Allan Carr
c8b7d7461a Merged in feature/IO-3570-Fortellis-Multiple-Veh-Records (pull request #3019)
Feature/IO-3570 Fortellis Multiple Veh Records
2026-02-19 20:03:48 +00:00
Allan Carr
48755dfa58 Merged in feature/IO-3570-Fortellis-Multiple-Veh-Records (pull request #3017)
Feature/IO-3570 Fortellis Multiple Veh Records
2026-02-19 18:53:47 +00:00
Allan Carr
3be344b595 Merged in feature/IO-3570-Fortellis-Multiple-Veh-Records (pull request #3012)
IO-3570 Filter Vehicle Results from VIN Query to records that only have a vehicleVehID
2026-02-19 17:56:50 +00:00
Patrick Fic
5d53d09af9 IO-3515 set po context, update confidence UI showing 2026-02-18 11:57:56 -08:00
Patrick Fic
d4bbdd7383 IO-3515 Improve confidence display. 2026-02-18 10:56:51 -08:00
Dave Richer
8b55df8624 Merged in feature/IO-3544-Ant-Select-Deprecation (pull request #3010)
feature/IO-3544-Ant-Select-Deprecation - Make cdklike-dms-post-form.jsx compatible with search on the payer name
2026-02-18 18:51:37 +00:00
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
Patrick Fic
e5f930b8c8 IO-3515 Refactor button to separate component. 2026-02-18 10:32:44 -08:00
Patrick Fic
6d94265081 Package lock updates. 2026-02-18 10:08:28 -08:00
Patrick Fic
d9e75fe775 Merge branch 'master-AIO' into feature/IO-3515-ocr-bill-posting 2026-02-18 10:08:25 -08:00
Dave Richer
94c3ab6e1b Merged in feature/IO-3544-Ant-Select-Deprecation (pull request #3005)
Feature/IO-3544 Ant Select Deprecation
2026-02-18 17:41:33 +00: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
6ae4e228ce Merged in release/2026-02-13 (pull request #3001)
Release/2026 02 13
2026-02-13 00:33:10 +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
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
d9b3730db9 Merged in release/2026-02-13 (pull request #2992)
feature/IO-3558-Reynolds-Part-2 - Admin Panel
2026-02-11 23:14:02 +00:00
Dave Richer
313a90e8f3 Merged in release/2026-02-13 (pull request #2989)
Release/2026 02 13
2026-02-11 23:11:42 +00:00
Dave
2a352b60a0 Merge remote-tracking branch 'origin/release/2026-02-13' into feature/IO-3554-Form-Row-Layout 2026-02-11 10:15:56 -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
Patrick Fic
64454dce2a IO-3515 add client side polling for now, cost centers. 2026-02-10 11:59:53 -08:00
Patrick Fic
c59acb1b72 IO-3515 add confidence scoring 2026-02-09 14:47:20 -08: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
Dave
5ae0e8e4d5 Initial 2026-02-06 10:43:58 -05: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
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
cdfae5a429 feature/IO-3544-Ant-Select-Deprecation - finish 2026-02-03 10:51:14 -05:00
Patrick Fic
20dad2caba IO-3515 Minimally functional form fill out. 2026-01-29 16:26:16 -08:00
Patrick Fic
96731a29e1 Remove test data. 2026-01-28 16:23:15 -08:00
Patrick Fic
83be45a40b IO-3515 Checkin. Crude form update with some correct values. Pricing still significantly out. 2026-01-28 16:20:27 -08:00
Patrick Fic
55de16281d IO-3515 Bill OCR refactor to split files and introduce generator. 2026-01-28 14:32:11 -08:00
Patrick Fic
ad7e85a578 IO-3515 bifurcate single/multi page extract, add check for polling, add field labels 2026-01-27 15:40:13 -08:00
Patrick Fic
2a6d0446f0 IO-3515 WIP - bulk calls functioning. Further refinement required. 2026-01-26 16:09:58 -08:00
Patrick Fic
c3718fff87 IO-3515 Additional packages and initial route &n simple queue polling. 2026-01-23 15:04:24 -08:00
212 changed files with 8136 additions and 5591 deletions

13
.gitignore vendored
View File

@@ -129,7 +129,11 @@ vitest-coverage/
test-output.txt test-output.txt
server/job/test/fixtures server/job/test/fixtures
# Keep .github ignored by default, but track Copilot instructions
.github .github
!.github/
.github/*
!.github/copilot-instructions.md
_reference/ragmate/.ragmate.env _reference/ragmate/.ragmate.env
docker_data docker_data
/.cursorrules /.cursorrules
@@ -139,3 +143,12 @@ docker_data
/COPILOT.md /COPILOT.md
/GEMINI.md /GEMINI.md
/_reference/select-component-test-plan.md /_reference/select-component-test-plan.md
/.cursorrules
/AGENTS.md
/AI_CONTEXT.md
/CLAUDE.md
/COPILOT.md
/.github/copilot-instructions.md
/GEMINI.md
/_reference/select-component-test-plan.md

View File

@@ -1,4 +1,4 @@
<babeledit_project version="1.2" be_version="2.7.1"> <babeledit_project be_version="2.7.1" version="1.2">
<!-- <!--
BabelEdit project file BabelEdit project file
@@ -2549,6 +2549,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>confidence</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>cost_center</name> <name>cost_center</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -2994,6 +3015,48 @@
<folder_node> <folder_node>
<name>errors</name> <name>errors</name>
<children> <children>
<concept_node>
<name>calculating_totals</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>calculating_totals_generic</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>creating</name> <name>creating</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -3466,6 +3529,310 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<folder_node>
<name>ai</name>
<children>
<concept_node>
<name>accept_and_continue</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<folder_node>
<name>confidence</name>
<children>
<concept_node>
<name>breakdown</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>match</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>missing_data</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>ocr</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>overall</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<concept_node>
<name>disclaimer_title</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>generic_failure</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>multipage</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>processing</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>scan</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>scancomplete</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>scanfailed</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>scanstarted</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<concept_node> <concept_node>
<name>bill_lines</name> <name>bill_lines</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -17402,6 +17769,48 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>earlyrorequired</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>earlyrorequired.message</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>
@@ -20223,6 +20632,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>gotoadmin</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>login</name> <name>login</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -21020,6 +21450,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>apply</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>areyousure</name> <name>areyousure</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -21062,6 +21513,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>beta</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>cancel</name> <name>cancel</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -26660,6 +27132,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>convertwithoutearlyro</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>createiou</name> <name>createiou</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -26747,6 +27240,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>createearlyro</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>createnewcustomer</name> <name>createnewcustomer</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -26878,6 +27392,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>update_ro</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>usegeneric</name> <name>usegeneric</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -29958,6 +30493,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>customer</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>dms_make</name> <name>dms_make</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -30362,6 +30918,90 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<concept_node>
<name>rr_opcode</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>rr_opcode_base</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>rr_opcode_prefix</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>rr_opcode_suffix</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>sale</name> <name>sale</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -35966,6 +36606,74 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<folder_node>
<name>earlyro</name>
<children>
<concept_node>
<name>created</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>fields</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>willupdate</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<concept_node> <concept_node>
<name>invoicedatefuture</name> <name>invoicedatefuture</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -39059,6 +39767,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>early_ro_created</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>exported</name> <name>exported</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -18,3 +18,4 @@ VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891 VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891
VITE_APP_ENABLE_RESPONSIVE_TABLE_FILTERING=false

View File

@@ -20,3 +20,4 @@ VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
VITE_APP_AMP_KEY=46b1193a867d4e3131ae4c3a64a3fc78 VITE_APP_AMP_KEY=46b1193a867d4e3131ae4c3a64a3fc78
VITE_APP_ENABLE_RESPONSIVE_TABLE_FILTERING=false

890
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,9 +8,9 @@
"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.3",
"@ant-design/pro-layout": "^7.22.6", "@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.1.3", "@apollo/client": "^4.1.6",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
@@ -18,43 +18,43 @@
"@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.2",
"@sentry/react": "^10.38.0", "@sentry/react": "^10.40.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.1",
"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.13.0",
"graphql-ws": "^6.0.7", "graphql-ws": "^6.0.7",
"i18next": "^25.8.0", "i18next": "^25.8.13",
"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.38",
"lightningcss": "^1.31.1", "lightningcss": "^1.31.1",
"logrocket": "^12.0.0", "logrocket": "^12.0.0",
"markerjs2": "^2.32.7", "markerjs2": "^2.32.7",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"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.71",
"posthog-js": "^1.336.4", "posthog-js": "^1.355.0",
"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",
@@ -74,7 +74,7 @@
"react-product-fruits": "^2.2.62", "react-product-fruits": "^2.2.62",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-resizable": "^3.1.3", "react-resizable": "^3.1.3",
"react-router-dom": "^7.13.0", "react-router-dom": "^7.13.1",
"react-sticky": "^6.0.3", "react-sticky": "^6.0.3",
"react-virtuoso": "^4.18.1", "react-virtuoso": "^4.18.1",
"recharts": "^3.7.0", "recharts": "^3.7.0",
@@ -87,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.11",
"vite-plugin-ejs": "^1.7.0", "vite-plugin-ejs": "^1.7.0",
"web-vitals": "^5.1.0" "web-vitals": "^5.1.0"
}, },
@@ -144,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",
@@ -156,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

@@ -0,0 +1,184 @@
import { ApolloProvider } from "@apollo/client/react";
import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider, Grid } from "antd";
import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react";
import { CookiesProvider } from "react-cookie";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import { setDarkMode } from "../redux/application/application.actions";
import { selectDarkMode } from "../redux/application/application.selectors";
import { selectCurrentUser } from "../redux/user/user.selectors.js";
import { signOutStart } from "../redux/user/user.actions";
import client from "../utils/GraphQLClient";
import App from "./App";
import getTheme from "./themeProvider";
// Base Split configuration
const config = {
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon"
}
};
function SplitClientProvider({ children }) {
const imexshopid = useSelector((state) => state.user.imexshopid);
const splitClient = useSplitClient({ key: imexshopid || "anon" });
useEffect(() => {
if (import.meta.env.DEV && splitClient && imexshopid) {
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
}
}, [splitClient, imexshopid]);
return children;
}
function AppContainer() {
const { t } = useTranslation();
const dispatch = useDispatch();
const currentUser = useSelector(selectCurrentUser);
const isDarkMode = useSelector(selectDarkMode);
const screens = Grid.useBreakpoint();
const isPhone = !screens.md;
const isUltraWide = Boolean(screens.xxxl);
const theme = useMemo(() => {
const baseTheme = getTheme(isDarkMode);
return {
...baseTheme,
token: {
...(baseTheme.token || {}),
screenXXXL: 2160
},
components: {
...(baseTheme.components || {}),
Table: {
...(baseTheme.components?.Table || {}),
cellFontSizeSM: isPhone ? 12 : 13,
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
cellFontSize: isUltraWide ? 15 : 14,
cellPaddingInlineSM: isPhone ? 8 : 10,
cellPaddingInlineMD: isPhone ? 10 : 14,
cellPaddingInline: isUltraWide ? 20 : 16,
cellPaddingBlockSM: isPhone ? 8 : 10,
cellPaddingBlockMD: isPhone ? 10 : 12,
cellPaddingBlock: isUltraWide ? 14 : 12,
selectionColumnWidth: isPhone ? 44 : 52
}
}
};
}, [isDarkMode, isPhone, isUltraWide]);
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
const antdPagination = useMemo(
() => ({
showSizeChanger: !isPhone,
totalBoundaryShowSizeChanger: 100
}),
[isPhone]
);
const antdForm = useMemo(
() => ({
validateMessages: {
required: t("general.validation.required", { label: "${label}" })
}
}),
[t]
);
// Global seamless logout listener with redirect to /signin
useEffect(() => {
const handleSeamlessLogout = (event) => {
if (event.data?.type !== "seamlessLogoutRequest") return;
// Only accept messages from the parent window
if (event.source !== window.parent) return;
const targetOrigin = event.origin || "*";
if (currentUser?.authorized !== true) {
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "already_logged_out" }, targetOrigin);
return;
}
dispatch(signOutStart());
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, targetOrigin);
};
window.addEventListener("message", handleSeamlessLogout);
return () => {
window.removeEventListener("message", handleSeamlessLogout);
};
}, [dispatch, currentUser?.authorized]);
// Update data-theme attribute (no cleanup to avoid transient style churn)
useEffect(() => {
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
}, [isDarkMode]);
// Sync darkMode with localStorage
useEffect(() => {
const uid = currentUser?.uid;
if (!uid) {
dispatch(setDarkMode(false));
return;
}
const key = `dark-mode-${uid}`;
const raw = localStorage.getItem(key);
if (raw == null) {
dispatch(setDarkMode(false));
return;
}
try {
dispatch(setDarkMode(Boolean(JSON.parse(raw))));
} catch {
dispatch(setDarkMode(false));
}
}, [currentUser?.uid, dispatch]);
// Persist darkMode
useEffect(() => {
const uid = currentUser?.uid;
if (!uid) return;
localStorage.setItem(`dark-mode-${uid}`, JSON.stringify(isDarkMode));
}, [isDarkMode, currentUser?.uid]);
return (
<CookiesProvider>
<ApolloProvider client={client}>
<ConfigProvider
input={antdInput}
locale={enLocale}
theme={theme}
form={antdForm}
table={antdTable}
pagination={antdPagination}
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
popupOverflow="viewport"
>
<GlobalLoadingBar />
<SplitFactoryProvider config={config}>
<SplitClientProvider>
<App />
</SplitClientProvider>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
</CookiesProvider>
);
}
export default Sentry.withProfiler(AppContainer);

View File

@@ -0,0 +1,184 @@
import { ApolloProvider } from "@apollo/client/react";
import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider, Grid } from "antd";
import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react";
import { CookiesProvider } from "react-cookie";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import { setDarkMode } from "../redux/application/application.actions";
import { selectDarkMode } from "../redux/application/application.selectors";
import { selectCurrentUser } from "../redux/user/user.selectors.js";
import { signOutStart } from "../redux/user/user.actions";
import client from "../utils/GraphQLClient";
import App from "./App";
import getTheme from "./themeProvider";
// Base Split configuration
const config = {
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon"
}
};
function SplitClientProvider({ children }) {
const imexshopid = useSelector((state) => state.user.imexshopid);
const splitClient = useSplitClient({ key: imexshopid || "anon" });
useEffect(() => {
if (import.meta.env.DEV && splitClient && imexshopid) {
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
}
}, [splitClient, imexshopid]);
return children;
}
function AppContainer() {
const { t } = useTranslation();
const dispatch = useDispatch();
const currentUser = useSelector(selectCurrentUser);
const isDarkMode = useSelector(selectDarkMode);
const screens = Grid.useBreakpoint();
const isPhone = !screens.md;
const isUltraWide = Boolean(screens.xxxl);
const theme = useMemo(() => {
const baseTheme = getTheme(isDarkMode);
return {
...baseTheme,
token: {
...(baseTheme.token || {}),
screenXXXL: 2160
},
components: {
...(baseTheme.components || {}),
Table: {
...(baseTheme.components?.Table || {}),
cellFontSizeSM: isPhone ? 12 : 13,
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
cellFontSize: isUltraWide ? 15 : 14,
cellPaddingInlineSM: isPhone ? 8 : 10,
cellPaddingInlineMD: isPhone ? 10 : 14,
cellPaddingInline: isUltraWide ? 20 : 16,
cellPaddingBlockSM: isPhone ? 8 : 10,
cellPaddingBlockMD: isPhone ? 10 : 12,
cellPaddingBlock: isUltraWide ? 14 : 12,
selectionColumnWidth: isPhone ? 44 : 52
}
}
};
}, [isDarkMode, isPhone, isUltraWide]);
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
const antdPagination = useMemo(
() => ({
showSizeChanger: !isPhone,
totalBoundaryShowSizeChanger: 100
}),
[isPhone]
);
const antdForm = useMemo(
() => ({
validateMessages: {
required: t("general.validation.required", { label: "${label}" })
}
}),
[t]
);
// Global seamless logout listener with redirect to /signin
useEffect(() => {
const handleSeamlessLogout = (event) => {
if (event.data?.type !== "seamlessLogoutRequest") return;
// Only accept messages from the parent window
if (event.source !== window.parent) return;
const targetOrigin = event.origin || "*";
if (currentUser?.authorized !== true) {
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "already_logged_out" }, targetOrigin);
return;
}
dispatch(signOutStart());
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, targetOrigin);
};
window.addEventListener("message", handleSeamlessLogout);
return () => {
window.removeEventListener("message", handleSeamlessLogout);
};
}, [dispatch, currentUser?.authorized]);
// Update data-theme attribute (no cleanup to avoid transient style churn)
useEffect(() => {
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
}, [isDarkMode]);
// Sync darkMode with localStorage
useEffect(() => {
const uid = currentUser?.uid;
if (!uid) {
dispatch(setDarkMode(false));
return;
}
const key = `dark-mode-${uid}`;
const raw = localStorage.getItem(key);
if (raw == null) {
dispatch(setDarkMode(false));
return;
}
try {
dispatch(setDarkMode(Boolean(JSON.parse(raw))));
} catch {
dispatch(setDarkMode(false));
}
}, [currentUser?.uid, dispatch]);
// Persist darkMode
useEffect(() => {
const uid = currentUser?.uid;
if (!uid) return;
localStorage.setItem(`dark-mode-${uid}`, JSON.stringify(isDarkMode));
}, [isDarkMode, currentUser?.uid]);
return (
<CookiesProvider>
<ApolloProvider client={client}>
<ConfigProvider
input={antdInput}
locale={enLocale}
theme={theme}
form={antdForm}
table={antdTable}
pagination={antdPagination}
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
popupOverflow="viewport"
>
<GlobalLoadingBar />
<SplitFactoryProvider config={config}>
<SplitClientProvider>
<App />
</SplitClientProvider>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
</CookiesProvider>
);
}
export default Sentry.withProfiler(AppContainer);

View File

@@ -443,35 +443,69 @@
flex-direction: column; flex-direction: column;
} }
/* DMS top panels: prevent card/table overflow into adjacent column at desktop+zoom */
.dms-top-panel-col {
min-width: 0;
}
.dms-top-panel-col > .ant-card {
width: 100%;
min-width: 0;
max-width: 100%;
}
.dms-top-panel-col > .ant-card .ant-card-body {
min-width: 0;
max-width: 100%;
}
.dms-top-panel-col .ant-table-wrapper,
.dms-top-panel-col .ant-tabs,
.dms-top-panel-col .ant-tabs-content,
.dms-top-panel-col .ant-tabs-tabpane {
min-width: 0;
max-width: 100%;
}
//.rbc-time-header-gutter { //.rbc-time-header-gutter {
// padding: 0; // padding: 0;
//} //}
/* globally allow shrink inside table cells */ ///* globally allow shrink inside table cells */
.prod-list-table .ant-table-cell, //.prod-list-table .ant-table-cell,
.prod-list-table .ant-table-cell > * { //.prod-list-table .ant-table-cell > * {
min-width: 0; // 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;
//}
/* 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 */ .global-search-autocomplete-fix {
.prod-list-table .ant-table-column-sorters { // This is the extra value render that causes the “duplicate text”
display: flex !important; .ant-select-selection-item {
align-items: center; position: absolute !important;
width: 100%; left: -10000px !important;
} pointer-events: none !important;
}
.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

@@ -68,7 +68,7 @@ const currentTheme = import.meta.env.DEV ? devTheme : prodTheme;
const getTheme = (isDarkMode) => ({ const getTheme = (isDarkMode) => ({
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm, algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
...defaultsDeep(currentTheme, defaultTheme) ...defaultsDeep({}, currentTheme, defaultTheme(isDarkMode))
}); });
export default getTheme; export default getTheme;

View File

@@ -1,4 +1,4 @@
import { Card, Checkbox, Input, Space, Table } from "antd"; import { Card, Checkbox, Input, Space } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -16,6 +16,7 @@ import PayableExportAll from "../payable-export-all-button/payable-export-all-bu
import PayableExportButton from "../payable-export-button/payable-export-button.component"; import PayableExportButton from "../payable-export-button/payable-export-button.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component"; import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import useLocalStorage from "./../../utils/useLocalStorage"; import useLocalStorage from "./../../utils/useLocalStorage";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -179,11 +180,12 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
</Space> </Space>
} }
> >
<Table <ResponsiveTable
loading={loading} loading={loading}
dataSource={dataSource} dataSource={dataSource}
pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }} pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
columns={columns} columns={columns}
mobileColumnKeys={["vendorname", "invoice_number", "ro_number", "total", "actions"]}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={{ rowSelection={{

View File

@@ -1,4 +1,4 @@
import { Card, Input, Space, Table } from "antd"; import { Card, Input, Space } from "antd";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -17,6 +17,7 @@ import PaymentExportButton from "../payment-export-button/payment-export-button.
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component"; import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component"; import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import ResponsiveTable from "../responsive-table/responsive-table.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -192,11 +193,12 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
</Space> </Space>
} }
> >
<Table <ResponsiveTable
loading={loading} loading={loading}
dataSource={dataSource} dataSource={dataSource}
pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }} pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
columns={columns} columns={columns}
mobileColumnKeys={["ro_number", "date", "owner", "amount", "actions"]}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={{ rowSelection={{

View File

@@ -1,4 +1,4 @@
import { Button, Card, Input, Space, Table } from "antd"; import { Button, Card, Input, Space } from "antd";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -17,6 +17,7 @@ import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-butto
import JobMarkSelectedExported from "../jobs-mark-selected-exported/jobs-mark-selected-exported"; import JobMarkSelectedExported from "../jobs-mark-selected-exported/jobs-mark-selected-exported";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import ResponsiveTable from "../responsive-table/responsive-table.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -209,11 +210,12 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
</Space> </Space>
} }
> >
<Table <ResponsiveTable
loading={loading} loading={loading}
dataSource={dataSource} dataSource={dataSource}
pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }} pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
columns={columns} columns={columns}
mobileColumnKeys={["ro_number", "status", "owner", "clm_total", "actions"]}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={{ rowSelection={{

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

@@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { Table } from "antd"; import ResponsiveTable from "../responsive-table/responsive-table.component";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -62,11 +62,12 @@ export default function AuditTrailListComponent({ loading, data }) {
}; };
return ( return (
<Table <ResponsiveTable
{...formItemLayout} {...formItemLayout}
loading={loading} loading={loading}
pagination={{ placement: "top", defaultPageSize: pageLimit }} pagination={{ placement: "top", defaultPageSize: pageLimit }}
columns={columns} columns={columns}
mobileColumnKeys={[" created", "operation", " old_val", "useremail"]}
rowKey="id" rowKey="id"
dataSource={data} dataSource={data}
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -1,4 +1,4 @@
import { Table } from "antd"; import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
@@ -47,11 +47,12 @@ export default function EmailAuditTrailListComponent({ loading, data }) {
}; };
return ( return (
<Table <ResponsiveTable
{...formItemLayout} {...formItemLayout}
loading={loading} loading={loading}
pagination={{ placement: "top", defaultPageSize: pageLimit }} pagination={{ placement: "top", defaultPageSize: pageLimit }}
columns={columns} columns={columns}
mobileColumnKeys={[" created", "useremail"]}
rowKey="id" rowKey="id"
dataSource={data} dataSource={data}
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -28,6 +28,20 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const initialValues =
data && data.bills_by_pk
? {
...data.bills_by_pk,
billlines: (data.bills_by_pk.billlines || []).map((bl) => {
const oem = bl.oem_partno || (bl.jobline && bl.jobline.oem_partno) || "";
const alt = bl.alt_partno || (bl.jobline && bl.jobline.alt_partno) || "";
return {
...bl,
oem_partno: `${oem || ""} ${alt ? `(${alt})` : ""}`.trim()
};
})
}
: undefined;
const handleFinish = ({ billlines }) => { const handleFinish = ({ billlines }) => {
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id); const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
@@ -74,8 +88,9 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
destroyOnHidden destroyOnHidden
title={t("bills.actions.return")} title={t("bills.actions.return")}
onOk={() => form.submit()} onOk={() => form.submit()}
width={700}
> >
<Form initialValues={data?.bills_by_pk} onFinish={handleFinish} form={form}> <Form initialValues={initialValues} onFinish={handleFinish} form={form}>
<Form.List name={["billlines"]}> <Form.List name={["billlines"]}>
{(fields) => { {(fields) => {
return ( return (
@@ -95,9 +110,10 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
/> />
</td> </td>
<td>{t("billlines.fields.line_desc")}</td> <td>{t("billlines.fields.line_desc")}</td>
<td>{t("billlines.fields.quantity")}</td> <td>{t("billlines.fields.oem_partno")}</td>
<td>{t("billlines.fields.actual_price")}</td> <td style={{ textAlign: "right" }}>{t("billlines.fields.quantity")}</td>
<td>{t("billlines.fields.actual_cost")}</td> <td style={{ textAlign: "right" }}>{t("billlines.fields.actual_price")}</td>
<td style={{ textAlign: "right" }}>{t("billlines.fields.actual_cost")}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -127,6 +143,15 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
</Form.Item> </Form.Item>
</td> </td>
<td> <td>
<Form.Item
// label={t("joblines.fields.oem_partno")}
key={`${index}jobline.oem_partno`}
name={[field.name, "oem_partno"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td style={{ textAlign: "right" }}>
<Form.Item <Form.Item
// label={t("joblines.fields.quantity")} // label={t("joblines.fields.quantity")}
key={`${index}quantity`} key={`${index}quantity`}
@@ -135,7 +160,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
<ReadOnlyFormItemComponent /> <ReadOnlyFormItemComponent />
</Form.Item> </Form.Item>
</td> </td>
<td> <td style={{ textAlign: "right" }}>
<Form.Item <Form.Item
// label={t("joblines.fields.actual_price")} // label={t("joblines.fields.actual_price")}
key={`${index}actual_price`} key={`${index}actual_price`}
@@ -144,7 +169,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
<ReadOnlyFormItemComponent type="currency" /> <ReadOnlyFormItemComponent type="currency" />
</Form.Item> </Form.Item>
</td> </td>
<td> <td style={{ textAlign: "right" }}>
<Form.Item <Form.Item
// label={t("joblines.fields.actual_cost")} // label={t("joblines.fields.actual_cost")}
key={`${index}actual_cost`} key={`${index}actual_cost`}

View File

@@ -7,10 +7,8 @@ export default function BillDetailEditcontainer() {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useNavigate(); const history = useNavigate();
const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) const screens = Grid.useBreakpoint();
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = { const bpoints = {
xs: "100%", xs: "100%",
sm: "100%", sm: "100%",
@@ -19,7 +17,14 @@ export default function BillDetailEditcontainer() {
xl: "90%", xl: "90%",
xxl: "90%" xxl: "90%"
}; };
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
let drawerPercentage = "100%";
if (screens.xxl) drawerPercentage = bpoints.xxl;
else if (screens.xl) drawerPercentage = bpoints.xl;
else if (screens.lg) drawerPercentage = bpoints.lg;
else if (screens.md) drawerPercentage = bpoints.md;
else if (screens.sm) drawerPercentage = bpoints.sm;
else if (screens.xs) drawerPercentage = bpoints.xs;
return ( return (
<Drawer <Drawer

View File

@@ -0,0 +1,203 @@
import { Button, Tag, Modal, Typography } from "antd";
import axios from "axios";
import { useState } from "react";
import { FaWandMagicSparkles } from "react-icons/fa6";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal,
bodyshop: selectBodyshop
});
function BillEnterAiScan({
billEnterModal,
bodyshop,
pollingIntervalRef,
setPollingIntervalRef,
form,
fileInputRef,
scanLoading,
setScanLoading,
setIsAiScan
}) {
const notification = useNotification();
const { t } = useTranslation();
const [showBetaModal, setShowBetaModal] = useState(false);
const BETA_ACCEPTANCE_KEY = "ai_scan_beta_acceptance";
const handleBetaAcceptance = () => {
localStorage.setItem(BETA_ACCEPTANCE_KEY, "true");
setShowBetaModal(false);
fileInputRef.current?.click();
};
const checkBetaAcceptance = () => {
const hasAccepted = localStorage.getItem(BETA_ACCEPTANCE_KEY);
if (hasAccepted) {
fileInputRef.current?.click();
} else {
setShowBetaModal(true);
}
};
// Polling function for multipage PDF status
const pollJobStatus = async (textractJobId) => {
try {
const { data } = await axios.get(`/ai/bill-ocr/status/${textractJobId}`);
if (data.status === "COMPLETED") {
// Stop polling
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
setPollingIntervalRef(null);
}
setScanLoading(false);
// Update form with the extracted data
if (data?.data?.billForm) {
form.setFieldsValue(data.data.billForm);
await form.validateFields(["billlines"], { recursive: true });
notification.success({
title: t("bills.labels.ai.scancomplete")
});
}
} else if (data.status === "FAILED") {
// Stop polling on failure
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
setPollingIntervalRef(null);
}
setScanLoading(false);
notification.error({
title: t("bills.labels.ai.scanfailed"),
description: data.error || ""
});
}
// If status is IN_PROGRESS, continue polling
} catch (error) {
// Stop polling on error
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
setPollingIntervalRef(null);
}
setScanLoading(false);
notification.error({
title: t("bills.labels.ai.scanfailed"),
description: error.response?.data?.message || error.message || "Failed to check scan status"
});
}
};
return (
<>
<input
ref={fileInputRef}
type="file"
accept="image/*,application/pdf"
style={{ display: "none" }}
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
setScanLoading(true);
setIsAiScan(true);
const formdata = new FormData();
formdata.append("billScan", file);
formdata.append("jobid", form.getFieldValue("jobid") || billEnterModal.context.job?.id);
formdata.append("bodyshopid", bodyshop.id);
formdata.append("partsorderid", billEnterModal.context.parts_order?.id);
try {
const { data, status } = await axios.post("/ai/bill-ocr", formdata);
// Add the scanned file to the upload field
const currentUploads = form.getFieldValue("upload") || [];
form.setFieldValue("upload", [
...currentUploads,
{
uid: `ai-scan-${Date.now()}`,
name: file.name,
originFileObj: file,
status: "done"
}
]);
if (status === 202) {
// Multipage PDF - start polling
notification.info({
title: t("bills.labels.ai.scanstarted"),
description: t("bills.labels.ai.multipage")
});
//Workaround needed to bypass react-compiler error about manipulating refs in child components. Refactor may be needed in the future to clean this up.
setPollingIntervalRef(
setInterval(() => {
pollJobStatus(data.textractJobId);
}, 3000)
);
// Initial poll
pollJobStatus(data.textractJobId);
} else if (status === 200) {
// Single page - immediate response
setScanLoading(false);
form.setFieldsValue(data.data.billForm);
await form.validateFields(["billlines"], { recursive: true });
notification.success({
title: t("bills.labels.ai.scancomplete")
});
}
} catch (error) {
setScanLoading(false);
notification.error({
title: t("bills.labels.ai.scanfailed"),
description: error.response?.data?.message || error.message || t("bills.labels.ai.generic_failure")
});
}
}
// Reset the input so the same file can be selected again
e.target.value = "";
}}
/>
<Button onClick={checkBetaAcceptance} icon={<FaWandMagicSparkles />} loading={scanLoading} disabled={scanLoading}>
{scanLoading ? t("bills.labels.ai.processing") : t("bills.labels.ai.scan")}
<Tag color="red">{t("general.labels.beta")}</Tag>
</Button>
<Modal
title={t("bills.labels.ai.disclaimer_title")}
open={showBetaModal}
onOk={handleBetaAcceptance}
onCancel={() => setShowBetaModal(false)}
okText={t("bills.labels.ai.accept_and_continue")}
cancelText={t("general.actions.cancel")}
>
{
//This is explicitly not translated.
}
<Typography.Text>
This AI scanning feature is currently in <strong>beta</strong>. While it can accelerate data entry, you{" "}
<strong>must carefully review all extracted results</strong> for accuracy.
</Typography.Text>
<Typography.Text>The AI may make mistakes or miss information. Always verify:</Typography.Text>
<ul>
<li>All line items and quantities</li>
<li>Prices and totals</li>
<li>Part numbers and descriptions</li>
<li>Any other critical invoice details</li>
</ul>
<Typography.Text>
By continuing, you acknowledge that you will review and verify all AI-generated data before posting.
</Typography.Text>
</Modal>
</>
);
}
export default connect(mapStateToProps, null)(BillEnterAiScan);

View File

@@ -2,10 +2,11 @@ import { useApolloClient, useMutation } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Checkbox, Form, Modal, Space } from "antd"; import { Button, Checkbox, Form, Modal, Space } from "antd";
import _ from "lodash"; import _ from "lodash";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useRef, 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 RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { INSERT_NEW_BILL } from "../../graphql/bills.queries"; import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries"; import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
@@ -21,12 +22,12 @@ import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import confirmDialog from "../../utils/asyncConfirm"; import confirmDialog from "../../utils/asyncConfirm";
import useLocalStorage from "../../utils/useLocalStorage"; import useLocalStorage from "../../utils/useLocalStorage";
import BillEnterAiScan from "../bill-enter-ai-scan/bill-enter-ai-scan.component.jsx";
import BillFormContainer from "../bill-form/bill-form.container"; import BillFormContainer from "../bill-form/bill-form.container";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility"; import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility"; import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility";
import { handleUpload as handleUploadToImageProxy } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility"; import { handleUpload as handleUploadToImageProxy } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import { handleUpload } from "../documents-upload/documents-upload.utility";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal, billEnterModal: selectBillEnterModal,
@@ -50,15 +51,20 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED); const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES); const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [scanLoading, setScanLoading] = useState(false);
const [isAiScan, setIsAiScan] = useState(false);
const client = useApolloClient(); const client = useApolloClient();
const [generateLabel, setGenerateLabel] = useLocalStorage("enter_bill_generate_label", false); const [generateLabel, setGenerateLabel] = useLocalStorage("enter_bill_generate_label", false);
const notification = useNotification(); const notification = useNotification();
const fileInputRef = useRef(null);
const pollingIntervalRef = useRef(null);
const formTopRef = useRef(null);
const { const {
treatments: { Enhanced_Payroll, Imgproxy } treatments: { Enhanced_Payroll, Imgproxy, Bill_OCR_AI }
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
attributes: {}, attributes: {},
names: ["Enhanced_Payroll", "Imgproxy"], names: ["Enhanced_Payroll", "Imgproxy", "Bill_OCR_AI"],
splitKey: bodyshop.imexshopid splitKey: bodyshop.imexshopid
}); });
@@ -113,6 +119,8 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
create_ppc, create_ppc,
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
original_actual_price, original_actual_price,
// eslint-disable-next-line no-unused-vars
confidence,
...restI ...restI
} = i; } = i;
@@ -378,6 +386,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
vendorid: values.vendorid, vendorid: values.vendorid,
billlines: [] billlines: []
}); });
setIsAiScan(false);
// form.resetFields(); // form.resetFields();
} else { } else {
toggleModalVisible(); toggleModalVisible();
@@ -388,10 +397,22 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
const handleCancel = () => { const handleCancel = () => {
const r = window.confirm(t("general.labels.cancel")); const r = window.confirm(t("general.labels.cancel"));
if (r === true) { if (r === true) {
// Clean up polling on cancel
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
setScanLoading(false);
setIsAiScan(false);
toggleModalVisible(); toggleModalVisible();
} }
}; };
//Workaround needed to bypass react-compiler error about manipulating refs in child components. Refactor may be needed in the future to clean this up.
const setPollingIntervalRef = (func) => {
pollingIntervalRef.current = func;
};
useEffect(() => { useEffect(() => {
if (enterAgain) form.submit(); if (enterAgain) form.submit();
}, [enterAgain, form]); }, [enterAgain, form]);
@@ -401,12 +422,44 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
form.setFieldsValue(formValues); form.setFieldsValue(formValues);
} else { } else {
form.resetFields(); form.resetFields();
// Clean up polling on modal close
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
setScanLoading(false);
setIsAiScan(false);
} }
}, [billEnterModal.open, form, formValues]); }, [billEnterModal.open, form, formValues]);
// Cleanup on unmount
useEffect(() => {
return () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
};
}, []);
return ( return (
<Modal <Modal
title={t("bills.labels.new")} title={
<Space size="large">
{t("bills.labels.new")}
{Bill_OCR_AI.treatment === "on" && (
<BillEnterAiScan
fileInputRef={fileInputRef}
form={form}
pollingIntervalRef={pollingIntervalRef}
setPollingIntervalRef={setPollingIntervalRef}
scanLoading={scanLoading}
setScanLoading={setScanLoading}
setIsAiScan={setIsAiScan}
/>
)}
</Space>
}
width={"98%"} width={"98%"}
open={billEnterModal.open} open={billEnterModal.open}
okText={t("general.actions.save")} okText={t("general.actions.save")}
@@ -447,13 +500,25 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
autoComplete={"off"} autoComplete={"off"}
layout="vertical" layout="vertical"
form={form} form={form}
onFinishFailed={() => { onFinishFailed={(errorInfo) => {
setEnterAgain(false); setEnterAgain(false);
// Scroll to the top of the form to show validation errors
if (errorInfo.errorFields && errorInfo.errorFields.length > 0) {
setTimeout(() => {
formTopRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
}, 100);
}
}} }}
> >
<RbacWrapper action="bills:enter"> <div ref={formTopRef}>
<BillFormContainer form={form} disableInvNumber={billEnterModal.context.disableInvNumber} /> <RbacWrapper action="bills:enter">
</RbacWrapper> <BillFormContainer
form={form}
isAiScan={isAiScan}
disableInvNumber={billEnterModal.context.disableInvNumber}
/>
</RbacWrapper>
</div>
</Form> </Form>
</Modal> </Modal>
); );

View File

@@ -1,4 +1,5 @@
import { Form, Input, Table } from "antd"; import { Form, Input } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
@@ -108,7 +109,14 @@ export default function BillFormLinesExtended({ lineData, discount, form, respon
<Form.Item noStyle name="billlineskeys"> <Form.Item noStyle name="billlineskeys">
<button onClick={() => console.log(form.getFieldsValue())}>form</button> <button onClick={() => console.log(form.getFieldsValue())}>form</button>
<Input onChange={(e) => setSearch(e.target.value)} allowClear /> <Input onChange={(e) => setSearch(e.target.value)} allowClear />
<Table pagination={false} size="small" columns={columns} rowKey="id" dataSource={data} /> <ResponsiveTable
pagination={false}
size="small"
columns={columns}
mobileColumnKeys={["line_desc", "oem_partno", "part_type", "act_price"]}
rowKey="id"
dataSource={data}
/>
</Form.Item> </Form.Item>
); );
} }

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

@@ -8,12 +8,15 @@ import { MdOpenInNew } from "react-icons/md";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries"; import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component"; import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobSearchSelect from "../job-search-select/job-search-select.component"; import JobSearchSelect from "../job-search-select/job-search-select.component";
@@ -21,8 +24,6 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component"; import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility"; import { CalculateBillTotal } from "./bill-form.totals.utility";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -43,11 +44,14 @@ export function BillFormComponent({
loadOutstandingReturns, loadOutstandingReturns,
loadInventory, loadInventory,
preferredMake, preferredMake,
disableInHouse disableInHouse,
isAiScan
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
const [discount, setDiscount] = useState(0); const [discount, setDiscount] = useState(0);
const notification = useNotification();
const jobIdFormWatch = Form.useWatch("jobid", form);
const { const {
treatments: { Extended_Bill_Posting, ClosingPeriod } treatments: { Extended_Bill_Posting, ClosingPeriod }
@@ -123,6 +127,23 @@ export function BillFormComponent({
bodyshop.inhousevendorid bodyshop.inhousevendorid
]); ]);
useEffect(() => {
// When the jobid is set by AI scan, we need to reload the lines. This prevents having to hoist the apollo query.
if (jobIdFormWatch !== null) {
if (form.getFieldValue("jobid") !== null && form.getFieldValue("jobid") !== undefined) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
if (form.getFieldValue("vendorid") !== null && form.getFieldValue("vendorid") !== undefined) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid")
}
});
}
}
}
}, [jobIdFormWatch, form]);
return ( return (
<div> <div>
<FormFieldsChanged form={form} /> <FormFieldsChanged form={form} />
@@ -328,13 +349,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>
@@ -374,7 +394,15 @@ export function BillFormComponent({
]); ]);
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); try {
totals = CalculateBillTotal(values);
} catch (error) {
notification.error({
title: t("bills.errors.calculating_totals"),
message: error.message || t("bills.errors.calculating_totals_generic"),
key: "bill_totals_calculation_error"
});
}
} }
if (totals) { if (totals) {
@@ -452,6 +480,7 @@ export function BillFormComponent({
responsibilityCenters={responsibilityCenters} responsibilityCenters={responsibilityCenters}
disabled={disabled} disabled={disabled}
billEdit={billEdit} billEdit={billEdit}
isAiScan={isAiScan}
/> />
)} )}
<Divider titlePlacement="left" style={{ display: billEdit ? "none" : null }}> <Divider titlePlacement="left" style={{ display: billEdit ? "none" : null }}>

View File

@@ -15,7 +15,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber, disableInHouse }) { export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber, disableInHouse,isAiScan }) {
const { const {
treatments: { Simple_Inventory } treatments: { Simple_Inventory }
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
@@ -50,6 +50,7 @@ export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableI
loadOutstandingReturns={loadOutstandingReturns} loadOutstandingReturns={loadOutstandingReturns}
loadInventory={loadInventory} loadInventory={loadInventory}
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null} preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
isAiScan={isAiScan}
/> />
{!billEdit && <BillCmdReturnsTableComponent form={form} returnLoading={returnLoading} returnData={returnData} />} {!billEdit && <BillCmdReturnsTableComponent form={form} returnLoading={returnLoading} returnData={returnData} />}
{Simple_Inventory.treatment === "on" && ( {Simple_Inventory.treatment === "on" && (

View File

@@ -5,14 +5,15 @@ 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";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectDarkMode } from "../../redux/application/application.selectors"; import { selectDarkMode } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CiecaSelect from "../../utils/Ciecaselect"; import CiecaSelect from "../../utils/Ciecaselect";
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component"; import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component"; import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js"; import ConfidenceDisplay from "./bill-form.lines.confidence.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -29,7 +30,8 @@ export function BillEnterModalLinesComponent({
discount, discount,
form, form,
responsibilityCenters, responsibilityCenters,
billEdit billEdit,
isAiScan
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form; const { setFieldsValue, getFieldsValue, getFieldValue } = form;
@@ -139,6 +141,29 @@ export function BillEnterModalLinesComponent({
const columns = (remove) => { const columns = (remove) => {
return [ return [
...(isAiScan
? [
{
title: t("billlines.fields.confidence"),
dataIndex: "confidence",
editable: true,
width: "5rem",
formItemProps: (field) => ({
key: `${field.index}confidence`,
name: [field.name, "confidence"],
label: t("billlines.fields.confidence")
}),
formInput: (record) => {
const rowValue = getFieldValue(["billlines", record.name]);
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<ConfidenceDisplay rowValue={rowValue} />
</div>
);
}
}
]
: []),
{ {
title: t("billlines.fields.jobline"), title: t("billlines.fields.jobline"),
dataIndex: "joblineid", dataIndex: "joblineid",
@@ -212,6 +237,7 @@ export function BillEnterModalLinesComponent({
}), }),
formInput: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} /> formInput: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} />
}, },
{ {
title: t("billlines.fields.quantity"), title: t("billlines.fields.quantity"),
dataIndex: "quantity", dataIndex: "quantity",
@@ -250,7 +276,16 @@ export function BillEnterModalLinesComponent({
key: `${field.name}actual_price`, key: `${field.name}actual_price`,
name: [field.name, "actual_price"], name: [field.name, "actual_price"],
label: t("billlines.fields.actual_price"), label: t("billlines.fields.actual_price"),
rules: [{ required: true }] rules: [
{ required: true },
{
validator: (_, value) => {
return Math.abs(parseFloat(value)) < 0.01 ? Promise.reject() : Promise.resolve();
},
warningOnly: true
}
],
hasFeedback: true
}), }),
formInput: (record, index) => ( formInput: (record, index) => (
<CurrencyInput <CurrencyInput
@@ -399,11 +434,17 @@ export function BillEnterModalLinesComponent({
rules: [{ required: true }] rules: [{ required: true }]
}), }),
formInput: () => ( formInput: () => (
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled} tabIndex={0}> <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
@@ -419,13 +460,11 @@ export function BillEnterModalLinesComponent({
name: [field.name, "location"] name: [field.name, "location"]
}), }),
formInput: () => ( formInput: () => (
<Select disabled={disabled} tabIndex={0}> <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>
) )
} }
]), ]),
@@ -466,22 +505,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" ? (

View File

@@ -0,0 +1,87 @@
import { Progress, Space, Tag, Tooltip } from "antd";
import { useTranslation } from "react-i18next";
const parseConfidence = (confidenceStr) => {
if (!confidenceStr || typeof confidenceStr !== "string") return null;
const match = confidenceStr.match(/T([\d.]+)\s*-\s*O([\d.]+)\s*-\s*J([\d.]+)/);
if (!match) return null;
return {
total: parseFloat(match[1]),
ocr: parseFloat(match[2]),
jobMatch: parseFloat(match[3])
};
};
const getConfidenceColor = (value) => {
if (value >= 80) return "green";
if (value >= 60) return "orange";
if (value >= 40) return "gold";
return "red";
};
const ConfidenceDisplay = ({ rowValue: { confidence, actual_price, actual_cost } }) => {
const { t } = useTranslation();
const parsed = parseConfidence(confidence);
const parsed_actual_price = parseFloat(actual_price);
const parsed_actual_cost = parseFloat(actual_cost);
if (!parsed) {
return <span style={{ color: "#959595", fontSize: "0.85em" }}>N/A</span>;
}
const { total, ocr, jobMatch } = parsed;
const color = getConfidenceColor(total);
return (
<Tooltip
title={
<div style={{ padding: "4px 0" }}>
<div style={{ marginBottom: 8, fontWeight: 600 }}>{t("bills.labels.ai.confidence.breakdown")}</div>
<div style={{ marginBottom: 4 }}>
<strong>{t("bills.labels.ai.confidence.overall")}:</strong> {total.toFixed(1)}%
<Progress
percent={total}
size="small"
strokeColor={getConfidenceColor(total)}
showInfo={false}
style={{ marginTop: 2 }}
/>
</div>
<div style={{ marginBottom: 4 }}>
<strong>{t("bills.labels.ai.confidence.ocr")}:</strong> {ocr.toFixed(1)}%
<Progress
percent={ocr}
size="small"
strokeColor={getConfidenceColor(ocr)}
showInfo={false}
style={{ marginTop: 2 }}
/>
</div>
<div>
<strong>{t("bills.labels.ai.confidence.match")}:</strong> {jobMatch.toFixed(1)}%
<Progress
percent={jobMatch}
size="small"
strokeColor={getConfidenceColor(jobMatch)}
showInfo={false}
style={{ marginTop: 2 }}
/>
</div>
</div>
}
>
<Space size="small">
{!parsed_actual_cost || !parsed_actual_price || parsed_actual_cost === 0 || parsed_actual_price === 0 ? (
<Tag color="red" style={{ margin: 0, cursor: "help", userSelect: "none" }}>
{t("bills.labels.ai.confidence.missing_data")}
</Tag>
) : null}
<Tag color={color} style={{ margin: 0, cursor: "help", userSelect: "none" }}>
{total.toFixed(0)}%
</Tag>
</Space>
</Tooltip>
);
};
export default ConfidenceDisplay;

View File

@@ -1,5 +1,5 @@
import { EditFilled, SyncOutlined } from "@ant-design/icons"; import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Checkbox, Input, Space, Table } from "antd"; import { Button, Card, Checkbox, Input, Space } from "antd";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FaTasks } from "react-icons/fa"; import { FaTasks } from "react-icons/fa";
@@ -18,6 +18,7 @@ import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import LockerWrapperComponent from "../lock-wrapper/lock-wrapper.component"; import LockerWrapperComponent from "../lock-wrapper/lock-wrapper.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component"; import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -237,12 +238,13 @@ export function BillsListTableComponent({
</Space> </Space>
} }
> >
<Table <ResponsiveTable
loading={billsQuery.loading} loading={billsQuery.loading}
scroll={{ scroll={{
x: true // y: "50rem" x: true // y: "50rem"
}} }}
columns={columns} columns={columns}
mobileColumnKeys={["vendorname", "invoice_number", "date", "total", "actions"]}
rowKey="id" rowKey="id"
dataSource={hasBillsAccess ? filteredBills : []} dataSource={hasBillsAccess ? filteredBills : []}
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -3,7 +3,8 @@ import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { useQuery } from "@apollo/client/react"; import { useQuery } from "@apollo/client/react";
import queryString from "query-string"; import queryString from "query-string";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { Input, Table } from "antd"; import { Input } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
@@ -79,7 +80,7 @@ export default function BillsVendorsList() {
: (data && data.vendors) || []; : (data && data.vendors) || [];
return ( return (
<Table <ResponsiveTable
loading={loading} loading={loading}
title={() => { title={() => {
return ( return (
@@ -91,6 +92,7 @@ export default function BillsVendorsList() {
dataSource={dataSource} dataSource={dataSource}
pagination={{ placement: "top" }} pagination={{ placement: "top" }}
columns={columns} columns={columns}
mobileColumnKeys={["name", "cost_center", "city"]}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={{ rowSelection={{

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

@@ -1,4 +1,5 @@
import { Card, Input, Table } from "antd"; import { Card, Input } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
@@ -103,10 +104,11 @@ export default function ContractsCarsComponent({ loading, data, selectedCarId, h
/> />
} }
> >
<Table <ResponsiveTable
loading={loading} loading={loading}
pagination={{ placement: "top" }} pagination={{ placement: "top" }}
columns={columns} columns={columns}
mobileColumnKeys={["status", "fleetnumber", "readiness", "year"]}
rowKey="id" rowKey="id"
dataSource={filteredData} dataSource={filteredData}
onChange={handleTableChange} onChange={handleTableChange}

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

@@ -1,4 +1,5 @@
import { Card, Input, Table } from "antd"; import { Card, Input } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
@@ -127,10 +128,11 @@ export default function ContractsJobsComponent({ loading, data, selectedJob, han
/> />
} }
> >
<Table <ResponsiveTable
loading={loading} loading={loading}
pagination={{ placement: "top", defaultPageSize: pageLimit, defaultCurrent: defaultCurrent }} pagination={{ placement: "top", defaultPageSize: pageLimit, defaultCurrent: defaultCurrent }}
columns={columns} columns={columns}
mobileColumnKeys={["ro_number", "owner", "status", "vehicle", "plate_no"]}
rowKey="id" rowKey="id"
dataSource={filteredData} dataSource={filteredData}
onChange={handleTableChange} onChange={handleTableChange}

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

@@ -1,5 +1,6 @@
import { useLazyQuery } from "@apollo/client/react"; import { useLazyQuery } from "@apollo/client/react";
import { Button, Form, Modal, Table } from "antd"; import { Button, Form, Modal } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useEffect } from "react"; import { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -64,7 +65,7 @@ export function ContractsFindModalContainer({ contractFinderModal, toggleModalVi
{t("general.labels.search")} {t("general.labels.search")}
</Button> </Button>
{error && <AlertComponent type="error" title={JSON.stringify(error)} />} {error && <AlertComponent type="error" title={JSON.stringify(error)} />}
<Table <ResponsiveTable
loading={loading} loading={loading}
columns={[ columns={[
{ {
@@ -124,6 +125,7 @@ export function ContractsFindModalContainer({ contractFinderModal, toggleModalVi
render: (text, record) => <DateTimeFormatter>{record.actualreturn}</DateTimeFormatter> render: (text, record) => <DateTimeFormatter>{record.actualreturn}</DateTimeFormatter>
} }
]} ]}
mobileColumnKeys={["agreementnumber", "job.ro_number", "driver_ln", "status"]}
rowKey="id" rowKey="id"
dataSource={data?.cccontracts} dataSource={data?.cccontracts}
/> />

View File

@@ -1,5 +1,5 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space, Table, Typography } from "antd"; import { Button, Card, Input, Space, Typography } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -14,6 +14,7 @@ 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 { pageLimit } from "../../utils/config"; import { pageLimit } from "../../utils/config";
import ResponsiveTable from "../responsive-table/responsive-table.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -170,13 +171,14 @@ export function ContractsList({ bodyshop, loading, contracts, refetch, total, se
} }
> >
<ContractsFindModalContainer /> <ContractsFindModalContainer />
<Table <ResponsiveTable
loading={loading} loading={loading}
scroll={{ scroll={{
x: "50%" //y: "40rem" x: "50%" //y: "40rem"
}} }}
pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(page || 1, 10), total: total }} pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(page || 1, 10), total: total }}
columns={columns} columns={columns}
mobileColumnKeys={["agreementnumber", "driver_ln", "status", "scheduledreturn"]}
rowKey="id" rowKey="id"
dataSource={contracts} dataSource={contracts}
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -1,4 +1,5 @@
import { Card, Table } from "antd"; import { Card } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import queryString from "query-string"; import queryString from "query-string";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
@@ -73,10 +74,11 @@ export default function CourtesyCarContractListComponent({ contracts, totalContr
return ( return (
<Card title={t("menus.header.courtesycars-contracts")}> <Card title={t("menus.header.courtesycars-contracts")}>
<Table <ResponsiveTable
scroll={{ x: true }} scroll={{ x: true }}
pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(page || 1), total: totalContracts }} pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(page || 1), total: totalContracts }}
columns={columns} columns={columns}
mobileColumnKeys={["agreementnumber", "driver_ln", "status", "job.ro_number"]}
rowKey="id" rowKey="id"
dataSource={contracts} dataSource={contracts}
onChange={handleTableChange} onChange={handleTableChange}

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

@@ -1,5 +1,6 @@
import { SyncOutlined, WarningFilled } from "@ant-design/icons"; import { SyncOutlined, WarningFilled } from "@ant-design/icons";
import { Button, Card, Dropdown, Input, Space, Table, Tooltip } from "antd"; import { Button, Card, Dropdown, Input, Space, Tooltip } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -275,10 +276,11 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
</Space> </Space>
} }
> >
<Table <ResponsiveTable
loading={loading} loading={loading}
pagination={{ placement: "top" }} pagination={{ placement: "top" }}
columns={columns} columns={columns}
mobileColumnKeys={["status", "fleetnumber", "vin", "readiness"]}
rowKey="id" rowKey="id"
dataSource={tableData} dataSource={tableData}
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -1,5 +1,6 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Table } from "antd"; import { Button, Card } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import queryString from "query-string"; import queryString from "query-string";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -86,10 +87,11 @@ export default function CsiResponseListPaginated({ refetch, loading, responses,
return ( return (
<Card extra={<Button onClick={() => refetch()} icon={<SyncOutlined />} />}> <Card extra={<Button onClick={() => refetch()} icon={<SyncOutlined />} />}>
<Table <ResponsiveTable
loading={loading} loading={loading}
pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(state.page || 1), total: total }} pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(state.page || 1), total: total }}
columns={columns} columns={columns}
mobileColumnKeys={["ro_number", "owner_name", "completedon"]}
rowKey="id" rowKey="id"
dataSource={responses} dataSource={responses}
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -1,4 +1,5 @@
import { Card, Table, Tag } from "antd"; import { Card, Tag } from "antd";
import ResponsiveTable from "../../responsive-table/responsive-table.component";
import axios from "axios"; import axios from "axios";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -182,10 +183,11 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
</div> </div>
</Card> </Card>
<Card style={{ marginTop: "5px" }} type="inner" title={t("job_lifecycle.titles.top_durations")}> <Card style={{ marginTop: "5px" }} type="inner" title={t("job_lifecycle.titles.top_durations")}>
<Table <ResponsiveTable
size="small" size="small"
pagination={false} pagination={false}
columns={columns} columns={columns}
mobileColumnKeys={["status", "humanReadable", "averageHumanReadable", "statusCount"]}
rowKey={(record) => record.status} rowKey={(record) => record.status}
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)} dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}
/> />

View File

@@ -1,4 +1,4 @@
import { Card, Input, Space, Table, Typography } from "antd"; import { Card, Input, Space, Typography } from "antd";
import axios from "axios"; import axios from "axios";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -7,6 +7,7 @@ import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import DashboardRefreshRequired from "../refresh-required.component"; import DashboardRefreshRequired from "../refresh-required.component";
import { pageLimit } from "../../../utils/config"; import { pageLimit } from "../../../utils/config";
import ResponsiveTable from "../../responsive-table/responsive-table.component.jsx";
export default function DashboardMonthlyJobCosting({ data, ...cardProps }) { export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -103,31 +104,33 @@ export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
{...cardProps} {...cardProps}
> >
<LoadingSkeleton loading={loading}> <LoadingSkeleton loading={loading}>
<div style={{ height: "100%" }}> <div style={{ height: "100%", minHeight: 0, width: "100%", overflow: "auto" }}>
<Table <ResponsiveTable
size="small"
tableLayout="fixed"
onChange={handleTableChange} onChange={handleTableChange}
pagination={{ placement: "top", defaultPageSize: pageLimit }} pagination={{ placement: "top", defaultPageSize: pageLimit }}
columns={columns} columns={columns}
scroll={{ x: true, y: "calc(100% - 4em)" }} scroll={{ x: "max-content" }}
rowKey="id" rowKey="id"
style={{ height: "100%" }} style={{ width: "100%" }}
dataSource={filteredData} dataSource={filteredData}
summary={() => ( summary={() => (
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
<Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title> <Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
{Dinero(costingData?.allSummaryData && costingData.allSummaryData.totalSales).toFormat()} {Dinero(costingData?.allSummaryData && costingData.allSummaryData.totalSales).toFormat()}
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
{Dinero(costingData?.allSummaryData && costingData.allSummaryData.totalCost).toFormat()} {Dinero(costingData?.allSummaryData && costingData.allSummaryData.totalCost).toFormat()}
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
{Dinero(costingData?.allSummaryData && costingData.allSummaryData.gpdollars).toFormat()} {Dinero(costingData?.allSummaryData && costingData.allSummaryData.gpdollars).toFormat()}
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell></Table.Summary.Cell> <ResponsiveTable.Summary.Cell></ResponsiveTable.Summary.Cell>
</Table.Summary.Row> </ResponsiveTable.Summary.Row>
)} )}
/> />
</div> </div>

View File

@@ -363,6 +363,7 @@ export default function DashboardScheduledDeliveryToday({ data, ...cardProps })
onChange={handleTableChange} onChange={handleTableChange}
pagination={false} pagination={false}
columns={isTvModeScheduledDelivery ? tvColumns : columns} columns={isTvModeScheduledDelivery ? tvColumns : columns}
// mobileColumnKeys={["ro_number", "owner", "status", "vehicle"]}
scroll={{ x: true, y: "calc(100% - 2em)" }} scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id" rowKey="id"
style={{ height: "85%" }} style={{ height: "85%" }}

View File

@@ -368,6 +368,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
onChange={handleTableChange} onChange={handleTableChange}
pagination={false} pagination={false}
columns={isTvModeScheduledIn ? tvColumns : columns} columns={isTvModeScheduledIn ? tvColumns : columns}
mobileColumnKeys={["ro_number", "owner", "vehicle", "start"]}
scroll={{ x: true, y: "calc(100% - 2em)" }} scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id" rowKey="id"
style={{ height: "85%" }} style={{ height: "85%" }}

View File

@@ -363,6 +363,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
onChange={handleTableChange} onChange={handleTableChange}
pagination={false} pagination={false}
columns={isTvModeScheduledOut ? tvColumns : columns} columns={isTvModeScheduledOut ? tvColumns : columns}
// mobileColumnKeys={["ro_number", "owner", "status", "vehicle"]}
scroll={{ x: true, y: "calc(100% - 2em)" }} scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id" rowKey="id"
style={{ height: "85%" }} style={{ height: "85%" }}

View File

@@ -1,5 +1,6 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Form, Input, Table } from "antd"; import { Button, Card, Form, Input } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -78,7 +79,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
dataIndex: "Lines", dataIndex: "Lines",
key: "Lines", key: "Lines",
render: (text, record) => ( render: (text, record) => (
<table style={{ tableLayout: "auto", width: "100%" }}> <ResponsiveTable style={{ tableLayout: "auto", width: "100%" }}>
<tr> <tr>
<th>{t("bills.fields.invoice_number")}</th> <th>{t("bills.fields.invoice_number")}</th>
<th>{t("bodyshop.fields.dms.dms_acctnumber")}</th> <th>{t("bodyshop.fields.dms.dms_acctnumber")}</th>
@@ -91,7 +92,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
<td>{l.Amount}</td> <td>{l.Amount}</td>
</tr> </tr>
))} ))}
</table> </ResponsiveTable>
) )
} }
]; ];
@@ -115,9 +116,10 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
/> />
} }
> >
<Table <ResponsiveTable
pagination={{ placement: "top", defaultPageSize: pageLimit }} pagination={{ placement: "top", defaultPageSize: pageLimit }}
columns={columns} columns={columns}
mobileColumnKeys={["status", "reference", "Lines"]}
rowKey={(record) => `${record.InvoiceNumber}${record.Account}`} rowKey={(record) => `${record.InvoiceNumber}${record.Account}`}
dataSource={allocationsSummary} dataSource={allocationsSummary}
locale={{ emptyText: t("dms.labels.refreshallocations") }} locale={{ emptyText: t("dms.labels.refreshallocations") }}

View File

@@ -1,4 +1,5 @@
import { Alert, Button, Card, Table, Typography } from "antd"; import { Alert, Button, Card, Typography } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -116,9 +117,10 @@ export function DmsAllocationsSummary({ mode, socket, bodyshop, jobId, title, on
<Alert type="warning" title={t("jobs.labels.dms.disablebillwip")} /> <Alert type="warning" title={t("jobs.labels.dms.disablebillwip")} />
)} )}
<Table <ResponsiveTable
pagination={{ placement: "top", defaultPageSize: pageLimit }} pagination={{ placement: "top", defaultPageSize: pageLimit }}
columns={columns} columns={columns}
mobileColumnKeys={["center", "sale", "cost", "sale_dms_acctnumber"]}
rowKey="center" rowKey="center"
dataSource={allocationsSummary} dataSource={allocationsSummary}
locale={{ emptyText: t("dms.labels.refreshallocations") }} locale={{ emptyText: t("dms.labels.refreshallocations") }}
@@ -135,15 +137,17 @@ export function DmsAllocationsSummary({ mode, socket, bodyshop, jobId, title, on
const hasNonZeroSaleTotal = totals.totalSale.getAmount() !== 0; const hasNonZeroSaleTotal = totals.totalSale.getAmount() !== 0;
return ( return (
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
<Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title> <Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell>{hasNonZeroSaleTotal ? totals.totalSale.toFormat() : null}</Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
<Table.Summary.Cell /> {hasNonZeroSaleTotal ? totals.totalSale.toFormat() : null}
<Table.Summary.Cell /> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell /> <ResponsiveTable.Summary.Cell />
</Table.Summary.Row> <ResponsiveTable.Summary.Cell />
<ResponsiveTable.Summary.Cell />
</ResponsiveTable.Summary.Row>
); );
}} }}
/> />

View File

@@ -1,4 +1,5 @@
import { Alert, Button, Card, Table, Tabs, Typography } from "antd"; import { Alert, Button, Card, Tabs, Typography } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -261,9 +262,10 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
into taxable / non-taxable segments. into taxable / non-taxable segments.
</Typography.Paragraph> </Typography.Paragraph>
<Table <ResponsiveTable
pagination={false} pagination={false}
columns={roggColumns} columns={roggColumns}
mobileColumnKeys={["jobNo", "opCode", "breakOut", "itemType"]}
rowKey="key" rowKey="key"
dataSource={roggRows} dataSource={roggRows}
locale={{ emptyText: "No ROGOG lines would be generated." }} locale={{ emptyText: "No ROGOG lines would be generated." }}
@@ -286,19 +288,23 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
const hasCostTotal = Number(roggTotals.totalDlrCost) !== 0; const hasCostTotal = Number(roggTotals.totalDlrCost) !== 0;
return ( return (
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell index={0}> <ResponsiveTable.Summary.Cell index={0}>
<Typography.Title level={5}>{t("general.labels.totals")}</Typography.Title> <Typography.Title level={5}>{t("general.labels.totals")}</Typography.Title>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell index={1} /> <ResponsiveTable.Summary.Cell index={1} />
<Table.Summary.Cell index={2} /> <ResponsiveTable.Summary.Cell index={2} />
<Table.Summary.Cell index={3} /> <ResponsiveTable.Summary.Cell index={3} />
<Table.Summary.Cell index={4} /> <ResponsiveTable.Summary.Cell index={4} />
<Table.Summary.Cell index={5} /> <ResponsiveTable.Summary.Cell index={5} />
<Table.Summary.Cell index={6} /> <ResponsiveTable.Summary.Cell index={6} />
<Table.Summary.Cell index={7}>{hasCustTotal ? roggTotals.totalCustPrice : null}</Table.Summary.Cell> <ResponsiveTable.Summary.Cell index={7}>
<Table.Summary.Cell index={8}>{hasCostTotal ? roggTotals.totalDlrCost : null}</Table.Summary.Cell> {hasCustTotal ? roggTotals.totalCustPrice : null}
</Table.Summary.Row> </ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell index={8}>
{hasCostTotal ? roggTotals.totalDlrCost : null}
</ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
); );
}} }}
/> />
@@ -313,9 +319,10 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}> <Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>
This mirrors the shell that would be sent for ROLABOR when all financials are carried in GOG. This mirrors the shell that would be sent for ROLABOR when all financials are carried in GOG.
</Typography.Paragraph> </Typography.Paragraph>
<Table <ResponsiveTable
pagination={false} pagination={false}
columns={rolaborColumns} columns={rolaborColumns}
mobileColumnKeys={["jobNo", "opCode", "breakOut", "itemType"]}
rowKey="key" rowKey="key"
dataSource={rolaborRows} dataSource={rolaborRows}
locale={{ emptyText: "No ROLABOR lines would be generated." }} locale={{ emptyText: "No ROLABOR lines would be generated." }}

View File

@@ -1,5 +1,6 @@
import { useLazyQuery } from "@apollo/client/react"; import { useLazyQuery } from "@apollo/client/react";
import { Button, Input, Modal, Table } from "antd"; import { Button, Input, Modal } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -60,7 +61,7 @@ export function DmsCdkVehicles({ form, job }) {
okButtonProps={{ disabled: !selectedModel }} okButtonProps={{ disabled: !selectedModel }}
> >
{error && <AlertComponent title={error.message} type="error" />} {error && <AlertComponent title={error.message} type="error" />}
<Table <ResponsiveTable
title={() => ( title={() => (
<Input.Search <Input.Search
onSearch={(val) => callSearch({ variables: { search: val } })} onSearch={(val) => callSearch({ variables: { search: val } })}
@@ -69,6 +70,7 @@ export function DmsCdkVehicles({ form, job }) {
/> />
)} )}
columns={columns} columns={columns}
mobileColumnKeys={["make", "model", "makecode", "modelcode"]}
loading={loading} loading={loading}
rowKey="id" rowKey="id"
dataSource={data ? data.search_dms_vehicles : []} dataSource={data ? data.search_dms_vehicles : []}

View File

@@ -1,4 +1,5 @@
import { Button, Checkbox, Col, Table } from "antd"; import { Button, Checkbox, Col } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
@@ -72,7 +73,7 @@ export default function CDKCustomerSelector({ bodyshop, socket }) {
return ( return (
<Col span={24}> <Col span={24}>
<Table <ResponsiveTable
title={() => ( title={() => (
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}> <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
<Button onClick={onUseSelected} disabled={!selectedCustomer}> <Button onClick={onUseSelected} disabled={!selectedCustomer}>
@@ -86,6 +87,7 @@ export default function CDKCustomerSelector({ bodyshop, socket }) {
)} )}
pagination={{ placement: "top" }} pagination={{ placement: "top" }}
columns={columns} columns={columns}
mobileColumnKeys={["id", "vinOwner", "name1", "address"]}
rowKey={rowKey} rowKey={rowKey}
dataSource={customerList} dataSource={customerList}
rowSelection={{ rowSelection={{

View File

@@ -1,4 +1,5 @@
import { Button, Checkbox, Col, Table } from "antd"; import { Button, Checkbox, Col } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
@@ -78,7 +79,7 @@ export default function FortellisCustomerSelector({ bodyshop, jobid, socket }) {
return ( return (
<Col span={24}> <Col span={24}>
<Table <ResponsiveTable
title={() => ( title={() => (
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}> <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
<Button onClick={onUseSelected} disabled={!selectedCustomer}> <Button onClick={onUseSelected} disabled={!selectedCustomer}>
@@ -92,6 +93,7 @@ export default function FortellisCustomerSelector({ bodyshop, jobid, socket }) {
)} )}
pagination={{ placement: "top" }} pagination={{ placement: "top" }}
columns={columns} columns={columns}
mobileColumnKeys={["id", "vinOwner", "firstName", "address"]}
rowKey={(r) => r.customerId} rowKey={(r) => r.customerId}
dataSource={customerList} dataSource={customerList}
rowSelection={{ rowSelection={{

View File

@@ -1,4 +1,5 @@
import { Button, Col, Table } from "antd"; import { Button, Col } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
@@ -66,7 +67,7 @@ export default function PBSCustomerSelector({ bodyshop, socket }) {
return ( return (
<Col span={24}> <Col span={24}>
<Table <ResponsiveTable
title={() => ( title={() => (
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}> <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
<Button onClick={onUseSelected} disabled={!selectedCustomer}> <Button onClick={onUseSelected} disabled={!selectedCustomer}>
@@ -80,6 +81,7 @@ export default function PBSCustomerSelector({ bodyshop, socket }) {
)} )}
pagination={{ placement: "top" }} pagination={{ placement: "top" }}
columns={columns} columns={columns}
mobileColumnKeys={["ContactId", "name1", "address"]}
rowKey={(r) => r.ContactId} rowKey={(r) => r.ContactId}
dataSource={customerList} dataSource={customerList}
rowSelection={{ rowSelection={{

View File

@@ -1,4 +1,5 @@
import { Alert, Button, Checkbox, message, Modal, Space, Table } from "antd"; import { Alert, Button, Checkbox, message, Modal, Space } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
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";
@@ -72,14 +73,14 @@ export default function RRCustomerSelector({
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 // If list is empty, it means early RO exists and customer selection should be skipped
// Don't open the modal in this case // Don't open the modal in this case
if (normalized.length === 0) { if (normalized.length === 0) {
setRefreshing(false); setRefreshing(false);
return; 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;
@@ -195,8 +196,8 @@ export default function RRCustomerSelector({
description={ description={
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}> <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div> <div>
We created the Repair Order. Please validate the totals and taxes in the DMS system. When done, We created the Repair Order. Please validate the totals and taxes in the DMS system. When done, click{" "}
click <strong>Finished</strong> to finalize and mark this export as complete. <strong>Finished</strong> to finalize and mark this export as complete.
</div> </div>
<div> <div>
<Space> <Space>
@@ -215,14 +216,8 @@ export default function RRCustomerSelector({
} }
return ( return (
<Modal <Modal open={open} onCancel={handleClose} footer={null} width={800} title={t("dms.selectCustomer")}>
open={open} <ResponsiveTable
onCancel={handleClose}
footer={null}
width={800}
title={t("dms.selectCustomer")}
>
<Table
title={() => ( title={() => (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}> <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{/* Open RO limit banner */} {/* Open RO limit banner */}
@@ -304,6 +299,7 @@ export default function RRCustomerSelector({
)} )}
pagination={{ placement: "top" }} pagination={{ placement: "top" }}
columns={columns} columns={columns}
mobileColumnKeys={["custNo", "vinOwner", "name", "address"]}
rowKey={(r) => r.custNo} rowKey={(r) => r.custNo}
dataSource={customerList} dataSource={customerList}
rowSelection={{ rowSelection={{

View File

@@ -4,6 +4,7 @@ import dayjs from "../../utils/day";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectDarkMode } from "../../redux/application/application.selectors.js"; import { selectDarkMode } from "../../redux/application/application.selectors.js";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
isDarkMode: selectDarkMode isDarkMode: selectDarkMode
@@ -19,25 +20,35 @@ export function DmsLogEvents({
detailsNonce, detailsNonce,
isDarkMode, isDarkMode,
colorizeJson = false, colorizeJson = false,
showDetails = true showDetails = true,
allowXmlPayload = true
}) { }) {
const { t } = useTranslation();
const [openSet, setOpenSet] = useState(() => new Set()); const [openSet, setOpenSet] = useState(() => new Set());
const [copiedKey, setCopiedKey] = useState(null);
// Inject JSON highlight styles once (only when colorize is enabled) // Inject JSON highlight styles once (only when colorize is enabled)
useEffect(() => { useEffect(() => {
if (!colorizeJson) return; if (!colorizeJson) return;
if (typeof document === "undefined") return; if (typeof document === "undefined") return;
if (document.getElementById("json-highlight-styles")) return; let style = document.getElementById("json-highlight-styles");
const style = document.createElement("style"); if (!style) {
style.id = "json-highlight-styles"; style = document.createElement("style");
style.id = "json-highlight-styles";
document.head.appendChild(style);
}
style.textContent = ` style.textContent = `
.json-key { color: #fa8c16; } .json-key { color: #fa8c16; }
.json-string { color: #52c41a; } .json-string { color: #52c41a; }
.json-number { color: #722ed1; } .json-number { color: #722ed1; }
.json-boolean { color: #1890ff; } .json-boolean { color: #1890ff; }
.json-null { color: #faad14; } .json-null { color: #faad14; }
.xml-tag { color: #1677ff; }
.xml-attr { color: #d46b08; }
.xml-value { color: #389e0d; }
.xml-decl { color: #7c3aed; }
.xml-comment { color: #8c8c8c; }
`; `;
document.head.appendChild(style);
}, [colorizeJson]); }, [colorizeJson]);
// Trim openSet if logs shrink // Trim openSet if logs shrink
@@ -65,6 +76,13 @@ export function DmsLogEvents({
// Only treat meta as "present" when we are allowed to show details // Only treat meta as "present" when we are allowed to show details
const hasMeta = !isEmpty(meta) && showDetails; const hasMeta = !isEmpty(meta) && showDetails;
const isOpen = hasMeta && openSet.has(idx); const isOpen = hasMeta && openSet.has(idx);
const xml = hasMeta && allowXmlPayload ? extractXmlFromMeta(meta) : { request: null, response: null };
const hasRequestXml = !!xml.request;
const hasResponseXml = !!xml.response;
const copyPayload = hasMeta ? getCopyPayload(meta) : null;
const copyPayloadKey = `copy-${idx}`;
const copyReqKey = `copy-req-${idx}`;
const copyResKey = `copy-res-${idx}`;
return { return {
key: idx, key: idx,
@@ -92,10 +110,42 @@ export function DmsLogEvents({
return next; return next;
}) })
} }
style={{ cursor: "pointer", userSelect: "none" }} style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
> >
{isOpen ? "Hide details" : "Details"} {isOpen ? t("dms.labels.hide_details") : t("dms.labels.details")}
</a> </a>
<Divider orientation="vertical" />
<a
role="button"
onClick={() => handleCopyAction(copyPayloadKey, copyPayload, setCopiedKey)}
style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
>
{copiedKey === copyPayloadKey ? t("dms.labels.copied") : t("dms.labels.copy")}
</a>
{hasRequestXml && (
<>
<Divider orientation="vertical" />
<a
role="button"
onClick={() => handleCopyAction(copyReqKey, xml.request, setCopiedKey)}
style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
>
{copiedKey === copyReqKey ? t("dms.labels.copied") : t("dms.labels.copy_request")}
</a>
</>
)}
{hasResponseXml && (
<>
<Divider orientation="vertical" />
<a
role="button"
onClick={() => handleCopyAction(copyResKey, xml.response, setCopiedKey)}
style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
>
{copiedKey === copyResKey ? t("dms.labels.copied") : t("dms.labels.copy_response")}
</a>
</>
)}
</> </>
)} )}
</Space> </Space>
@@ -103,14 +153,30 @@ export function DmsLogEvents({
{/* Row 2: details body (only when open) */} {/* Row 2: details body (only when open) */}
{hasMeta && isOpen && ( {hasMeta && isOpen && (
<div style={{ marginLeft: 6 }}> <div style={{ marginLeft: 6 }}>
<JsonBlock isDarkMode={isDarkMode} data={meta} colorize={colorizeJson} /> <JsonBlock isDarkMode={isDarkMode} data={removeXmlFromMeta(meta)} colorize={colorizeJson} />
{hasRequestXml && (
<XmlBlock
isDarkMode={isDarkMode}
title={t("dms.labels.request_xml")}
xmlText={xml.request}
colorize={colorizeJson}
/>
)}
{hasResponseXml && (
<XmlBlock
isDarkMode={isDarkMode}
title={t("dms.labels.response_xml")}
xmlText={xml.response}
colorize={colorizeJson}
/>
)}
</div> </div>
)} )}
</Space> </Space>
) )
}; };
}), }),
[logs, openSet, colorizeJson, isDarkMode, showDetails] [logs, openSet, colorizeJson, copiedKey, isDarkMode, showDetails, allowXmlPayload, t]
); );
return <Timeline reverse items={items} />; return <Timeline reverse items={items} />;
@@ -179,6 +245,121 @@ const safeStringify = (obj, spaces = 2) => {
} }
}; };
/**
* Get request/response XML from various Reynolds log meta shapes.
* @param meta
* @returns {{request: string|null, response: string|null}}
*/
const extractXmlFromMeta = (meta) => {
const request =
firstString(meta?.requestXml) ||
firstString(meta?.xml?.request) ||
firstString(meta?.response?.xml?.request) ||
firstString(meta?.response?.requestXml);
const response =
firstString(meta?.responseXml) || firstString(meta?.xml?.response) || firstString(meta?.response?.xml?.response);
return { request, response };
};
/**
* Return the value to copy when clicking the "Copy" action.
* @param meta
* @returns {*}
*/
const getCopyPayload = (meta) => {
if (meta?.payload != null) return meta.payload;
return meta;
};
/**
* Remove bulky XML fields from object shown in JSON block (XML is rendered separately).
* @param meta
* @returns {*}
*/
const removeXmlFromMeta = (meta) => {
if (meta == null || typeof meta !== "object") return meta;
const cloned = safeClone(meta);
if (cloned == null || typeof cloned !== "object") return meta;
if (typeof cloned.requestXml === "string") delete cloned.requestXml;
if (typeof cloned.responseXml === "string") delete cloned.responseXml;
if (cloned.xml && typeof cloned.xml === "object") {
if (typeof cloned.xml.request === "string") delete cloned.xml.request;
if (typeof cloned.xml.response === "string") delete cloned.xml.response;
if (isEmpty(cloned.xml)) delete cloned.xml;
}
if (cloned.response?.xml && typeof cloned.response.xml === "object") {
if (typeof cloned.response.xml.request === "string") delete cloned.response.xml.request;
if (typeof cloned.response.xml.response === "string") delete cloned.response.xml.response;
if (isEmpty(cloned.response.xml)) delete cloned.response.xml;
}
return cloned;
};
/**
* Safe deep clone for plain JSON structures.
* @param value
* @returns {*}
*/
const safeClone = (value) => {
try {
return JSON.parse(JSON.stringify(value));
} catch {
return value;
}
};
/**
* First non-empty string helper.
* @param value
* @returns {string|null}
*/
const firstString = (value) => {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed ? trimmed : null;
};
/**
* Copy arbitrary text/object to clipboard.
* @param key
* @param value
* @param setCopied
* @returns {Promise<void>}
*/
const handleCopyAction = async (key, value, setCopied) => {
const text = typeof value === "string" ? value : safeStringify(value, 2);
if (!text) return;
const copied = await copyTextToClipboard(text);
if (!copied) return;
setCopied(key);
setTimeout(() => {
setCopied((prev) => (prev === key ? null : prev));
}, 1200);
};
/**
* Clipboard helper (modern async Clipboard API).
* @param text
* @returns {Promise<boolean>}
*/
const copyTextToClipboard = async (text) => {
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
return false;
}
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
return false;
}
};
/** /**
* JSON display block with optional syntax highlighting. * JSON display block with optional syntax highlighting.
* @param data * @param data
@@ -210,6 +391,105 @@ const JsonBlock = ({ data, colorize, isDarkMode }) => {
return <pre style={preStyle}>{jsonText}</pre>; return <pre style={preStyle}>{jsonText}</pre>;
}; };
/**
* XML display block with normalized indentation.
* @param title
* @param xmlText
* @param isDarkMode
* @returns {JSX.Element}
* @constructor
*/
const XmlBlock = ({ title, xmlText, isDarkMode, colorize = false }) => {
const base = {
margin: "8px 0 0",
maxWidth: 720,
overflowX: "auto",
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: 12,
lineHeight: 1.45,
padding: 8,
borderRadius: 6,
background: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.04)",
border: isDarkMode ? "1px solid rgba(255,255,255,0.12)" : "1px solid rgba(0,0,0,0.08)",
color: isDarkMode ? "var(--card-text-fallback)" : "#141414",
whiteSpace: "pre"
};
return (
<div style={{ marginTop: 6 }}>
<div style={{ fontSize: 11, fontWeight: 600 }}>{title}</div>
{colorize ? (
<pre style={base} dangerouslySetInnerHTML={{ __html: highlightXml(formatXml(xmlText)) }} />
) : (
<pre style={base}>{formatXml(xmlText)}</pre>
)}
</div>
);
};
/**
* Basic XML pretty-printer.
* @param xml
* @returns {string}
*/
const formatXml = (xml) => {
if (typeof xml !== "string") return "";
const normalized = xml.replace(/\r\n/g, "\n").replace(/>\s*</g, ">\n<").trim();
const lines = normalized.split("\n");
let indent = 0;
const out = [];
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line) continue;
if (/^<\/[^>]+>/.test(line)) indent = Math.max(indent - 1, 0);
out.push(`${" ".repeat(indent)}${line}`);
const opens = (line.match(/<[^/!?][^>]*>/g) || []).length;
const closes = (line.match(/<\/[^>]+>/g) || []).length;
const selfClosing = (line.match(/<[^>]+\/>/g) || []).length;
const declaration = /^<\?xml/.test(line) ? 1 : 0;
indent += opens - closes - selfClosing - declaration;
if (indent < 0) indent = 0;
}
return out.join("\n");
};
/**
* Syntax highlight pretty-printed XML text for HTML display.
* @param xmlText
* @returns {string}
*/
const highlightXml = (xmlText) => {
const esc = String(xmlText || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const lines = esc.split("\n");
return lines
.map((line) => {
let out = line;
out = out.replace(/(&lt;!--[\s\S]*?--&gt;)/g, '<span class="xml-comment">$1</span>');
out = out.replace(/(&lt;\?xml[\s\S]*?\?&gt;)/g, '<span class="xml-decl">$1</span>');
out = out.replace(/(&lt;\/?)([A-Za-z_][\w:.-]*)([\s\S]*?)(\/?&gt;)/g, (_m, open, tag, attrs, close) => {
const coloredAttrs = attrs.replace(
/([A-Za-z_][\w:.-]*)(=)("[^"]*"|'[^']*'|&quot;[\s\S]*?&quot;|&apos;[\s\S]*?&apos;)/g,
'<span class="xml-attr">$1</span>$2<span class="xml-value">$3</span>'
);
return `${open}<span class="xml-tag">${tag}</span>${coloredAttrs}${close}`;
});
return out;
})
.join("\n");
};
/** /**
* Syntax highlight JSON text for HTML display. * Syntax highlight JSON text for HTML display.
* @param jsonText * @param jsonText

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>

View File

@@ -1,5 +1,6 @@
import { ReloadOutlined } from "@ant-design/icons"; import { ReloadOutlined } from "@ant-design/icons";
import { Alert, Button, Form, Input, InputNumber, Modal, Radio, Select, Space, Table, Typography } from "antd"; import { Alert, Button, Form, Input, InputNumber, Modal, Radio, Select, Space, Typography } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
// Simple customer selector table // Simple customer selector table
@@ -26,7 +27,14 @@ function CustomerSelectorTable({ customers, onSelect, isSubmitting }) {
return ( return (
<div> <div>
<Table columns={columns} dataSource={customers} rowKey="custNo" pagination={false} size="small" /> <ResponsiveTable
columns={columns}
mobileColumnKeys={["name", "select", "custNo", "vinOwner"]}
dataSource={customers}
rowKey="custNo"
pagination={false}
size="small"
/>
<div style={{ marginTop: 16, display: "flex", gap: 8 }}> <div style={{ marginTop: 16, display: "flex", gap: 8 }}>
<Button <Button
type="primary" type="primary"

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

@@ -163,7 +163,7 @@ export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOv
<Modal <Modal
destroyOnHidden destroyOnHidden
open={modalVisible} open={modalVisible}
maskClosable={false} mask={{ closable: false }}
width={"80%"} width={"80%"}
onOk={() => form.submit()} onOk={() => form.submit()}
title={t("emails.labels.emailpreview")} title={t("emails.labels.emailpreview")}

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

@@ -39,11 +39,13 @@ export default function FormsFieldChanged({ form, skipPrompt }) {
{errors.length > 0 && ( {errors.length > 0 && (
<AlertComponent <AlertComponent
type="error" type="error"
title={ message={t("general.labels.validationerror")}
description={
<div> <div>
<ul>{errors.map((e, idx) => e.errors.map((e2, idx2) => <li key={`${idx}${idx2}`}>{e2}</li>))}</ul> <ul>{errors.map((e, idx) => e.errors.map((e2, idx2) => <li key={`${idx}${idx2}`}>{e2}</li>))}</ul>
</div> </div>
} }
showIcon
/> />
)} )}
</Space> </Space>

View File

@@ -184,22 +184,29 @@ export default function GlobalSearchOs() {
return ( return (
<AutoComplete <AutoComplete
options={data} options={data}
onSearch={handleSearch} defaultActiveFirstOption
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key !== "Enter") return; if (e.key !== "Enter") return;
const firstUrlForSearch = data?.[0]?.options?.[0]?.label?.props?.to; const firstUrlForSearch = data?.[0]?.options?.[0]?.label?.props?.to;
if (!firstUrlForSearch) return; if (!firstUrlForSearch) return;
navigate(firstUrlForSearch); navigate(firstUrlForSearch);
}} }}
defaultActiveFirstOption
onClear={() => setData([])}
> >
<Input.Search <Input.Search
// className="global-search-autocomplete-fix"
size="large" size="large"
placeholder={t("general.labels.globalsearch")} placeholder={t("general.labels.globalsearch")}
enterButton enterButton
allowClear allowClear
loading={loading} loading={loading}
onChange={(e) => {
const value = e.target.value;
if (!value) {
setData([]);
} else {
handleSearch(value);
}
}}
/> />
</AutoComplete> </AutoComplete>
); );

View File

@@ -160,9 +160,6 @@ export default function GlobalSearch() {
return ( return (
<AutoComplete <AutoComplete
options={options} options={options}
showSearch={{
onSearch: handleSearch
}}
defaultActiveFirstOption defaultActiveFirstOption
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key !== "Enter") return; if (e.key !== "Enter") return;
@@ -172,11 +169,13 @@ export default function GlobalSearch() {
}} }}
> >
<Input.Search <Input.Search
// className="global-search-autocomplete-fix"
size="large" size="large"
placeholder={t("general.labels.globalsearch")} placeholder={t("general.labels.globalsearch")}
enterButton enterButton
allowClear allowClear
loading={loading} loading={loading}
onChange={(e) => handleSearch(e.target.value)}
/> />
</AutoComplete> </AutoComplete>
); );

View File

@@ -1,5 +1,5 @@
import { EditFilled, FileAddFilled, SyncOutlined } from "@ant-design/icons"; import { EditFilled, FileAddFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space, Table, Typography } from "antd"; import { Button, Card, Input, Space, Typography } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -10,6 +10,7 @@ import CurrencyFormatter from "../../utils/CurrencyFormatter";
import InventoryBillRo from "../inventory-bill-ro/inventory-bill-ro.component"; import InventoryBillRo from "../inventory-bill-ro/inventory-bill-ro.component";
import InventoryLineDelete from "../inventory-line-delete/inventory-line-delete.component"; import InventoryLineDelete from "../inventory-line-delete/inventory-line-delete.component";
import { pageLimit } from "../../utils/config"; import { pageLimit } from "../../utils/config";
import ResponsiveTable from "../responsive-table/responsive-table.component";
const mapStateToProps = createStructuredSelector({}); const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
@@ -185,10 +186,11 @@ export function JobsList({ refetch, loading, jobs, total, setInventoryUpsertCont
</Space> </Space>
} }
> >
<Table <ResponsiveTable
loading={loading} loading={loading}
pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(page || 1), total: total }} pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(page || 1), total: total }}
columns={columns} columns={columns}
mobileColumnKeys={["line_desc", "actual_price", "consumedbyjob", "actions"]}
rowKey="id" rowKey="id"
dataSource={jobs} dataSource={jobs}
onChange={handleTableChange} onChange={handleTableChange}

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) => {
@@ -103,13 +106,13 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
<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

@@ -1,6 +1,7 @@
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, Col, Row, Table, Tag } from "antd"; import { Button, Card, Col, Row, Tag } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
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";
@@ -163,12 +164,24 @@ export function JobAuditTrail({ bodyshop, jobId }) {
/> />
} }
> >
<Table loading={loading} columns={columns} rowKey="id" dataSource={data ? data.audit_trail : []} /> <ResponsiveTable
loading={loading}
columns={columns}
mobileColumnKeys={["status", "created", "useremail", "operation"]}
rowKey="id"
dataSource={data ? data.audit_trail : []}
/>
</Card> </Card>
</Col> </Col>
<Col span={24}> <Col span={24}>
<Card title={t("jobs.labels.emailaudit")}> <Card title={t("jobs.labels.emailaudit")}>
<Table loading={loading} columns={emailColumns} rowKey="id" dataSource={data ? data.email_audit_trail : []} /> <ResponsiveTable
loading={loading}
columns={emailColumns}
mobileColumnKeys={["status", "created", "useremail", "operation"]}
rowKey="id"
dataSource={data ? data.email_audit_trail : []}
/>
</Card> </Card>
</Col> </Col>
</Row> </Row>

View File

@@ -1,5 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Alert, Card, Table } from "antd"; import { Alert, Card } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { t } from "i18next"; import { t } from "i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -68,7 +69,15 @@ export function JobCloseRGuardPpd({ job, warningCallback }) {
return ( return (
<Card title={t("jobs.labels.ppdnotexported")}> <Card title={t("jobs.labels.ppdnotexported")}>
<Table dataSource={linesWithPPD} columns={columns} pagination={false} rowKey="id" bordered size="small" /> <ResponsiveTable
dataSource={linesWithPPD}
columns={columns}
mobileColumnKeys={["line_desc", "ppd", "act_price", "act_price_before_ppc"]}
pagination={false}
rowKey="id"
bordered
size="small"
/>
{linesWithPPD.length > 0 && ( {linesWithPPD.length > 0 && (
<Alert style={{ margin: "8px 0px" }} type="warning" title={t("jobs.labels.outstanding_ppd")} /> <Alert style={{ margin: "8px 0px" }} type="warning" title={t("jobs.labels.outstanding_ppd")} />
)} )}

View File

@@ -1,6 +1,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Alert, Card, Table } from "antd"; import { Alert, Card } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { t } from "i18next"; import { t } from "i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -62,7 +63,15 @@ export function JobCloseRGuardSublet({ job, warningCallback }) {
return ( return (
<Card title={t("jobs.labels.subletsnotcompleted")}> <Card title={t("jobs.labels.subletsnotcompleted")}>
<Table dataSource={subletsNotDone} columns={columns} pagination={false} rowKey="id" bordered size="small" /> <ResponsiveTable
dataSource={subletsNotDone}
columns={columns}
mobileColumnKeys={["line_desc", "act_price", "part_qty", "notes"]}
pagination={false}
rowKey="id"
bordered
size="small"
/>
{subletsNotDone.length > 0 && ( {subletsNotDone.length > 0 && (
<Alert style={{ margin: "8px 0px" }} type="warning" title={t("jobs.labels.outstanding_sublets")} /> <Alert style={{ margin: "8px 0px" }} type="warning" title={t("jobs.labels.outstanding_sublets")} />
)} )}

View File

@@ -1,4 +1,5 @@
import { Input, Space, Table, Typography } from "antd"; import { Input, Space, Typography } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
@@ -65,7 +66,7 @@ export default function JobCostingPartsTable({ data, summaryData }) {
return ( return (
<div> <div>
<Table <ResponsiveTable
title={() => { title={() => {
return ( return (
<Space wrap> <Space wrap>
@@ -87,18 +88,19 @@ export default function JobCostingPartsTable({ data, summaryData }) {
onChange={handleTableChange} onChange={handleTableChange}
pagination={{ placement: "top", defaultPageSize: pageLimit }} pagination={{ placement: "top", defaultPageSize: pageLimit }}
columns={columns} columns={columns}
mobileColumnKeys={["cost_center", "sales", "costs", "gpdollars", "gppercent"]}
rowKey="id" rowKey="id"
dataSource={filteredData} dataSource={filteredData}
summary={() => ( summary={() => (
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
<Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title> <Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell>{Dinero(summaryData.totalSales).toFormat()}</Table.Summary.Cell> <ResponsiveTable.Summary.Cell>{Dinero(summaryData.totalSales).toFormat()}</ResponsiveTable.Summary.Cell>
<Table.Summary.Cell>{Dinero(summaryData.totalCost).toFormat()}</Table.Summary.Cell> <ResponsiveTable.Summary.Cell>{Dinero(summaryData.totalCost).toFormat()}</ResponsiveTable.Summary.Cell>
<Table.Summary.Cell>{Dinero(summaryData.gpdollars).toFormat()}</Table.Summary.Cell> <ResponsiveTable.Summary.Cell>{Dinero(summaryData.gpdollars).toFormat()}</ResponsiveTable.Summary.Cell>
<Table.Summary.Cell></Table.Summary.Cell> <ResponsiveTable.Summary.Cell></ResponsiveTable.Summary.Cell>
</Table.Summary.Row> </ResponsiveTable.Summary.Row>
)} )}
/> />
</div> </div>

View File

@@ -58,10 +58,8 @@ const span = {
export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTrail }) { export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTrail }) {
const { scenarioNotificationsOn } = useSocket(); const { scenarioNotificationsOn } = useSocket();
const [updateJob] = useMutation(UPDATE_JOB); const [updateJob] = useMutation(UPDATE_JOB);
const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) const screens = Grid.useBreakpoint();
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = { const bpoints = {
xs: "100%", xs: "100%",
sm: "100%", sm: "100%",
@@ -70,7 +68,14 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTra
xl: "75%", xl: "75%",
xxl: "75%" xxl: "75%"
}; };
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
let drawerPercentage = "100%";
if (screens.xxl) drawerPercentage = bpoints.xxl;
else if (screens.xl) drawerPercentage = bpoints.xl;
else if (screens.lg) drawerPercentage = bpoints.lg;
else if (screens.md) drawerPercentage = bpoints.md;
else if (screens.sm) drawerPercentage = bpoints.sm;
else if (screens.xs) drawerPercentage = bpoints.xs;
const searchParams = queryString.parse(useLocation().search); const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams; const { selected } = searchParams;

View File

@@ -1,4 +1,4 @@
import { Table } from "antd"; import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import JobLineNotePopup from "../job-line-note-popup/job-line-note-popup.component"; import JobLineNotePopup from "../job-line-note-popup/job-line-note-popup.component";
import PartsStatusPie from "../parts-status-pie/parts-status-pie.component"; import PartsStatusPie from "../parts-status-pie/parts-status-pie.component";
@@ -101,7 +101,12 @@ function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
<div> <div>
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}> <CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
<PartsStatusPie joblines_status={joblines_status} /> <PartsStatusPie joblines_status={joblines_status} />
<Table rowKey="id" columns={columns} dataSource={filteredJobLines || []} /> <ResponsiveTable
rowKey="id"
columns={columns}
mobileColumnKeys={["status", "line_desc", "part_type", "part_qty"]}
dataSource={filteredJobLines || []}
/>
</CardTemplate> </CardTemplate>
</div> </div>
); );

View File

@@ -690,6 +690,7 @@ export function JobLinesComponent({
<Table <Table
columns={columns} columns={columns}
// mobileColumnKeys={["status", "line_desc", "actions", "line_no"]}
rowKey="id" rowKey="id"
loading={loading} loading={loading}
pagination={false} pagination={false}

View File

@@ -1,6 +1,7 @@
import { useQuery } from "@apollo/client/react"; import { useQuery } from "@apollo/client/react";
import { gql } from "@apollo/client"; import { gql } from "@apollo/client";
import { Badge, Card, Space, Table, Tag } from "antd"; import { Badge, Card, Space, Tag } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import axios from "axios"; import axios from "axios";
import { isEmpty } from "lodash"; import { isEmpty } from "lodash";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
@@ -311,12 +312,13 @@ export function JobLifecycleComponent({ bodyshop, job, statuses }) {
</> </>
} }
> >
<Table <ResponsiveTable
style={{ style={{
overflow: "auto", overflow: "auto",
width: "100%" width: "100%"
}} }}
columns={columns} columns={columns}
mobileColumnKeys={["value", "start", "start_readable", "end"]}
dataSource={lifecycleData.lifecycle} dataSource={lifecycleData.lifecycle}
rowKey="start" rowKey="start"
/> />

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,5 +1,6 @@
import { EditFilled } from "@ant-design/icons"; import { EditFilled } from "@ant-design/icons";
import { Button, Card, Space, Table } from "antd"; import { Button, Card, Space } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -186,8 +187,9 @@ export function JobPayments({ job, bodyshop, setPaymentContext, setCardPaymentCo
</Space> </Space>
} }
> >
<Table <ResponsiveTable
columns={columns} columns={columns}
mobileColumnKeys={["date", "amount", "actions", "payer"]}
rowKey="id" rowKey="id"
pagination={false} pagination={false}
onChange={handleTableChange} onChange={handleTableChange}
@@ -199,18 +201,18 @@ export function JobPayments({ job, bodyshop, setPaymentContext, setCardPaymentCo
expandedRowRender: (record) => <PaymentExpandedRowComponent record={record} bodyshop={bodyshop} /> expandedRowRender: (record) => <PaymentExpandedRowComponent record={record} bodyshop={bodyshop} />
}} }}
summary={() => ( summary={() => (
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
<strong>{t("payments.labels.totalpayments")}</strong> <strong>{t("payments.labels.totalpayments")}</strong>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell /> <ResponsiveTable.Summary.Cell />
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
<strong>{total.toFormat()}</strong> <strong>{total.toFormat()}</strong>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell /> <ResponsiveTable.Summary.Cell />
<Table.Summary.Cell /> <ResponsiveTable.Summary.Cell />
<Table.Summary.Cell /> <ResponsiveTable.Summary.Cell />
</Table.Summary.Row> </ResponsiveTable.Summary.Row>
)} )}
/> />
</Card> </Card>

View File

@@ -1,4 +1,5 @@
import { Checkbox, Table, Typography } from "antd"; import { Checkbox, Typography } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
@@ -79,11 +80,12 @@ export default function JobReconciliationBillsTable({ billLineState, invoiceLine
return ( return (
<div> <div>
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title> <Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
<Table <ResponsiveTable
pagination={false} pagination={false}
size="small" size="small"
scroll={{ y: "60vh" }} scroll={{ y: "60vh" }}
columns={columns} columns={columns}
mobileColumnKeys={["line_desc", "from", "actual_price", "actual_cost"]}
rowKey="id" rowKey="id"
dataSource={invoiceLineData} dataSource={invoiceLineData}
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -1,4 +1,5 @@
import { Table, Typography } from "antd"; import { Typography } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
@@ -96,9 +97,10 @@ export default function JobReconcilitionPartsTable({ jobLineState, jobLineData }
return ( return (
<div> <div>
<Typography.Title level={4}>{t("jobs.labels.lines")}</Typography.Title> <Typography.Title level={4}>{t("jobs.labels.lines")}</Typography.Title>
<Table <ResponsiveTable
pagination={false} pagination={false}
columns={columns} columns={columns}
mobileColumnKeys={["status", "line_desc", "total", "oem_partno"]}
size="small" size="small"
scroll={{ y: "60vh" }} scroll={{ y: "60vh" }}
rowKey="id" rowKey="id"

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

@@ -1,4 +1,5 @@
import { Space, Table } from "antd"; import { Space } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -104,8 +105,9 @@ export default function JobTotalsTableLabor({ job }) {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
return ( return (
<Table <ResponsiveTable
columns={columns} columns={columns}
mobileColumnKeys={["total", "profitcenter_labor", "rate", "mod_lb_hrs"]}
rowKey="id" rowKey="id"
pagination={false} pagination={false}
onChange={handleTableChange} onChange={handleTableChange}
@@ -115,29 +117,29 @@ export default function JobTotalsTableLabor({ job }) {
}} }}
summary={() => ( summary={() => (
<> <>
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
<strong>{t("jobs.labels.labor_rates_subtotal")}</strong> <strong>{t("jobs.labels.labor_rates_subtotal")}</strong>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell /> <ResponsiveTable.Summary.Cell />
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
{(job.job_totals.rates.mapa.hours + job.job_totals.rates.mash.hours).toFixed(1)} {(job.job_totals.rates.mapa.hours + job.job_totals.rates.mash.hours).toFixed(1)}
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
{InstanceRenderManager({ {InstanceRenderManager({
imex: null, imex: null,
rome: ( rome: (
<> <>
<Table.Summary.Cell /> <ResponsiveTable.Summary.Cell />
<Table.Summary.Cell /> <ResponsiveTable.Summary.Cell />
</> </>
) )
})} })}
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}</strong> <strong>{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}</strong>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
</Table.Summary.Row> </ResponsiveTable.Summary.Row>
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
<Space> <Space>
{t("jobs.labels.mapa")} {t("jobs.labels.mapa")}
{InstanceRenderManager({ {InstanceRenderManager({
@@ -156,34 +158,34 @@ export default function JobTotalsTableLabor({ job }) {
}) })
})} })}
</Space> </Space>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
<CurrencyFormatter>{job.job_totals.rates.mapa.rate}</CurrencyFormatter> <CurrencyFormatter>{job.job_totals.rates.mapa.rate}</CurrencyFormatter>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell>{job.job_totals.rates.mapa.hours.toFixed(1)}</Table.Summary.Cell> <ResponsiveTable.Summary.Cell>{job.job_totals.rates.mapa.hours.toFixed(1)}</ResponsiveTable.Summary.Cell>
{InstanceRenderManager({ {InstanceRenderManager({
imex: ( imex: (
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.total).toFormat()} {Dinero(job.job_totals.rates.mapa.total).toFormat()}
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
), ),
rome: ( rome: (
<> <>
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.base).toFormat()} {Dinero(job.job_totals.rates.mapa.base).toFormat()}
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.adjustment).toFormat()} {Dinero(job.job_totals.rates.mapa.adjustment).toFormat()}
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.total).toFormat()} {Dinero(job.job_totals.rates.mapa.total).toFormat()}
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
</> </>
) )
})} })}
</Table.Summary.Row> </ResponsiveTable.Summary.Row>
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
<Space wrap> <Space wrap>
{t("jobs.labels.mash")} {t("jobs.labels.mash")}
{InstanceRenderManager({ {InstanceRenderManager({
@@ -202,51 +204,51 @@ export default function JobTotalsTableLabor({ job }) {
}) })
})} })}
</Space> </Space>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
<CurrencyFormatter>{job.job_totals.rates.mash.rate}</CurrencyFormatter> <CurrencyFormatter>{job.job_totals.rates.mash.rate}</CurrencyFormatter>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell>{job.job_totals.rates.mash.hours.toFixed(1)}</Table.Summary.Cell> <ResponsiveTable.Summary.Cell>{job.job_totals.rates.mash.hours.toFixed(1)}</ResponsiveTable.Summary.Cell>
{InstanceRenderManager({ {InstanceRenderManager({
imex: ( imex: (
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.total).toFormat()} {Dinero(job.job_totals.rates.mash.total).toFormat()}
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
), ),
rome: ( rome: (
<> <>
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.base).toFormat()} {Dinero(job.job_totals.rates.mash.base).toFormat()}
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.adjustment).toFormat()} {Dinero(job.job_totals.rates.mash.adjustment).toFormat()}
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.total).toFormat()} {Dinero(job.job_totals.rates.mash.total).toFormat()}
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
</> </>
) )
})} })}
</Table.Summary.Row> </ResponsiveTable.Summary.Row>
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
<strong>{t("jobs.labels.rates_subtotal")}</strong> <strong>{t("jobs.labels.rates_subtotal")}</strong>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell /> <ResponsiveTable.Summary.Cell />
<Table.Summary.Cell /> <ResponsiveTable.Summary.Cell />
{InstanceRenderManager({ {InstanceRenderManager({
imex: null, imex: null,
rome: ( rome: (
<> <>
<Table.Summary.Cell /> <ResponsiveTable.Summary.Cell />
<Table.Summary.Cell /> <ResponsiveTable.Summary.Cell />
</> </>
) )
})} })}
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.rates.subtotal).toFormat()}</strong> <strong>{Dinero(job.job_totals.rates.subtotal).toFormat()}</strong>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
</Table.Summary.Row> </ResponsiveTable.Summary.Row>
</> </>
)} )}
/> />

View File

@@ -1,4 +1,4 @@
import { Table } from "antd"; import ResponsiveTable from "../responsive-table/responsive-table.component";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -65,8 +65,9 @@ export default function JobTotalsTableOther({ job }) {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
return ( return (
<Table <ResponsiveTable
columns={columns} columns={columns}
mobileColumnKeys={["total", "key"]}
rowKey="key" rowKey="key"
pagination={false} pagination={false}
onChange={handleTableChange} onChange={handleTableChange}
@@ -76,24 +77,24 @@ export default function JobTotalsTableOther({ job }) {
}} }}
summary={() => ( summary={() => (
<> <>
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
<strong>{t("jobs.labels.additionaltotal")}</strong> <strong>{t("jobs.labels.additionaltotal")}</strong>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.additional.total).toFormat()}</strong> <strong>{Dinero(job.job_totals.additional.total).toFormat()}</strong>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
</Table.Summary.Row> </ResponsiveTable.Summary.Row>
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
<strong>{t("jobs.labels.subletstotal")}</strong> <strong>{t("jobs.labels.subletstotal")}</strong>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.parts.sublets.total).toFormat()}</strong> <strong>{Dinero(job.job_totals.parts.sublets.total).toFormat()}</strong>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
</Table.Summary.Row> </ResponsiveTable.Summary.Row>
</> </>
)} )}
/> />

View File

@@ -1,4 +1,4 @@
import { Table } from "antd"; import ResponsiveTable from "../responsive-table/responsive-table.component";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -64,8 +64,9 @@ export default function JobTotalsTableParts({ job }) {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
return ( return (
<Table <ResponsiveTable
columns={columns} columns={columns}
mobileColumnKeys={["total", "id"]}
rowKey="id" rowKey="id"
pagination={false} pagination={false}
onChange={handleTableChange} onChange={handleTableChange}
@@ -75,36 +76,38 @@ export default function JobTotalsTableParts({ job }) {
}} }}
summary={() => ( summary={() => (
<> <>
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell>{t("jobs.labels.prt_dsmk_total")}</Table.Summary.Cell> <ResponsiveTable.Summary.Cell>{t("jobs.labels.prt_dsmk_total")}</ResponsiveTable.Summary.Cell>
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
{Dinero(job.job_totals.parts.parts.prt_dsmk_total).toFormat()} {Dinero(job.job_totals.parts.parts.prt_dsmk_total).toFormat()}
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
</Table.Summary.Row> </ResponsiveTable.Summary.Row>
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell> <ResponsiveTable.Summary.Cell>
<strong>{t("jobs.labels.partstotal")}</strong> <strong>{t("jobs.labels.partstotal")}</strong>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
<Table.Summary.Cell align="right"> <ResponsiveTable.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.parts.parts.total).toFormat()}</strong> <strong>{Dinero(job.job_totals.parts.parts.total).toFormat()}</strong>
</Table.Summary.Cell> </ResponsiveTable.Summary.Cell>
</Table.Summary.Row> </ResponsiveTable.Summary.Row>
{ {
//TODO:AIO This shoudl only be in the US version. need to verify whether this causes problems for the CA version. //TODO:AIO This shoudl only be in the US version. need to verify whether this causes problems for the CA version.
insuranceAdjustments.length > 0 && ( insuranceAdjustments.length > 0 && (
<Table.Summary.Row> <ResponsiveTable.Summary.Row>
<Table.Summary.Cell colSpan={24}>{t("jobs.labels.profileadjustments")}</Table.Summary.Cell> <ResponsiveTable.Summary.Cell colSpan={24}>
</Table.Summary.Row> {t("jobs.labels.profileadjustments")}
</ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
) )
} }
{insuranceAdjustments.map((adj, idx) => ( {insuranceAdjustments.map((adj, idx) => (
<Table.Summary.Row key={idx}> <ResponsiveTable.Summary.Row key={idx}>
<Table.Summary.Cell>{t(`jobs.fields.${adj.id.toLowerCase()}`)}</Table.Summary.Cell> <ResponsiveTable.Summary.Cell>{t(`jobs.fields.${adj.id.toLowerCase()}`)}</ResponsiveTable.Summary.Cell>
<Table.Summary.Cell align="right">{adj.amount.toFormat()}</Table.Summary.Cell> <ResponsiveTable.Summary.Cell align="right">{adj.amount.toFormat()}</ResponsiveTable.Summary.Cell>
</Table.Summary.Row> </ResponsiveTable.Summary.Row>
))} ))}
</> </>
)} )}

View File

@@ -1,4 +1,4 @@
import { Table } from "antd"; import ResponsiveTable from "../responsive-table/responsive-table.component";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -245,8 +245,9 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
]; ];
return ( return (
<Table <ResponsiveTable
columns={columns} columns={columns}
mobileColumnKeys={["total", "key"]}
rowKey="key" rowKey="key"
showHeader={false} showHeader={false}
pagination={false} pagination={false}

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

@@ -1,5 +1,5 @@
import { DownloadOutlined, SyncOutlined } from "@ant-design/icons"; import { DownloadOutlined, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space, Table } from "antd"; import { Button, Card, Input, Space } from "antd";
import axios from "axios"; import axios from "axios";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -9,6 +9,7 @@ import { useNotification } from "../../contexts/Notifications/notificationContex
import { selectPartnerVersion } from "../../redux/application/application.selectors"; import { selectPartnerVersion } from "../../redux/application/application.selectors";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import { logImEXEvent } from "../../firebase/firebase.utils.js"; import { logImEXEvent } from "../../firebase/firebase.utils.js";
import ResponsiveTable from "../responsive-table/responsive-table.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -141,10 +142,11 @@ export function JobsAvailableScan({ partnerVersion, refetch }) {
</Space> </Space>
} }
> >
<Table <ResponsiveTable
loading={loading} loading={loading}
pagination={{ placement: "top" }} pagination={{ placement: "top" }}
columns={columns} columns={columns}
mobileColumnKeys={["cieca_id", "owner", "vehicle", "actions"]}
rowKey="id" rowKey="id"
dataSource={data} dataSource={data}
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -1,6 +1,6 @@
import { DeleteFilled, DownloadOutlined, PlusCircleFilled, SyncOutlined } from "@ant-design/icons"; import { DeleteFilled, DownloadOutlined, PlusCircleFilled, SyncOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client/react"; import { useMutation } from "@apollo/client/react";
import { Alert, Button, Card, Input, Space, Table } from "antd"; import { Alert, Button, Card, Input, Space } from "antd";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -13,6 +13,7 @@ import { TimeAgoFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { logImEXEvent } from "../../firebase/firebase.utils.js"; import { logImEXEvent } from "../../firebase/firebase.utils.js";
import ResponsiveTable from "../responsive-table/responsive-table.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -209,7 +210,14 @@ export function JobsAvailableComponent({ bodyshop, loading, data, refetch, addJo
</Space> </Space>
} }
> >
<Table loading={loading} columns={columns} rowKey="id" dataSource={availableJobs} onChange={handleTableChange} /> <ResponsiveTable
loading={loading}
columns={columns}
mobileColumnKeys={["cieca_id", "job_id", "ownr_name", "vehicle_info", "actions"]}
rowKey="id"
dataSource={availableJobs}
onChange={handleTableChange}
/>
</Card> </Card>
); );
} }

View File

@@ -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

@@ -2,7 +2,7 @@ import { useMutation } from "@apollo/client/react";
import { Button, Divider, Form, Input, Modal, 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";
@@ -19,10 +19,10 @@ import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import RREarlyROForm from "../dms-post-form/rr-early-ro-form"; 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(
@@ -37,16 +37,17 @@ 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); // Track early RO creation state
const [earlyRoCreatedThisSession, setEarlyRoCreatedThisSession] = useState(false); // Track if created in THIS modal session 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(); // Extract socket from context const { socket } = useSocket();
// Get Fortellis treatment for proper DMS mode detection
const { const {
treatments: { Fortellis } treatments: { Fortellis }
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
@@ -55,16 +56,64 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
splitKey: bodyshop?.imexshopid splitKey: bodyshop?.imexshopid
}); });
// Check if bodyshop has Reynolds integration using the proper getDmsMode function
const dmsMode = getDmsMode(bodyshop, Fortellis.treatment); const dmsMode = getDmsMode(bodyshop, Fortellis.treatment);
const isReynoldsMode = dmsMode === DMS_MAP.reynolds; 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,
@@ -78,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")
}); });
@@ -97,19 +144,20 @@ 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 handleEarlyROSuccess = (result) => { const handleEarlyROSuccess = (result) => {
setEarlyRoCreated(true); // Mark early RO as created setEarlyRoCreated(true);
setEarlyRoCreatedThisSession(true); // Mark as created in this session setEarlyRoCreatedThisSession(true);
notification.success({ notification.success({
title: t("jobs.successes.early_ro_created"), title: t("jobs.successes.early_ro_created"),
description: `RO Number: ${result.roNumber || "N/A"}` description: `RO Number: ${result.roNumber || "N/A"}`
}); });
// Delay refetch to keep success message visible for 2 seconds
setTimeout(() => { setTimeout(() => {
refetch?.(); refetch?.();
}, 2000); }, 2000);
@@ -130,29 +178,28 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
disabled={job.converted || jobRO} disabled={job.converted || jobRO}
loading={loading} loading={loading}
onClick={() => { onClick={() => {
setEarlyRoCreated(!!job?.dms_id); // Initialize state based on current job setEarlyRoCreated(!!job?.dms_id);
setEarlyRoCreatedThisSession(false); // Reset session state when opening modal setEarlyRoCreatedThisSession(false);
setOpen(true); setOpen(true);
}} }}
> >
{t("jobs.actions.convert")} {t("jobs.actions.convert")}
</Button> </Button>
{/* Convert Job Modal */}
<Modal <Modal
open={open} open={open}
onCancel={handleModalClose} onCancel={handleModalClose}
closable={!(earlyRoCreatedThisSession && !job.converted)} // Only restrict if created in THIS session closable={!(earlyRoCreatedThisSession && !job.converted)}
maskClosable={!(earlyRoCreatedThisSession && !job.converted)} // Only restrict if created in THIS session mask={{ closable: !(earlyRoCreatedThisSession && !job.converted) }}
title={t("jobs.actions.convert")} title={t("jobs.actions.convert")}
footer={null} footer={null}
width={700} width={700}
destroyOnHidden destroyOnHidden
> >
{/* Standard Convert Form */}
<Form <Form
layout="vertical" layout="vertical"
form={form} form={form}
preserve={false}
onFinish={handleConvert} onFinish={handleConvert}
initialValues={{ initialValues={{
driveable: true, driveable: true,
@@ -164,7 +211,6 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
referral_source_extra: job.referral_source_extra ?? "" referral_source_extra: job.referral_source_extra ?? ""
}} }}
> >
{/* Show Reynolds Early RO section at the top if applicable */}
{isReynoldsMode && !job.dms_id && !earlyRoCreated && ( {isReynoldsMode && !job.dms_id && !earlyRoCreated && (
<> <>
<RREarlyROForm <RREarlyROForm
@@ -181,127 +227,78 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
<Form.Item <Form.Item
name={["ins_co_nm"]} name={["ins_co_nm"]}
label={t("jobs.fields.ins_co_nm")} label={t("jobs.fields.ins_co_nm")}
rules={[ rules={[{ required: true }]}
{
required: true
//message: t("general.validation.required"),
}
]}
> >
<Select showSearch> <Select
{bodyshop.md_ins_cos.map((s, i) => ( showSearch={{
<Select.Option key={i} value={s.name}> optionFilterProp:'label'
{s.name} }}
</Select.Option> options={insuranceOptions}
))} />
</Select>
</Form.Item> </Form.Item>
{bodyshop.enforce_class && ( {bodyshop.enforce_class && (
<Form.Item <Form.Item name="class" label={t("jobs.fields.class")} rules={[{ required: bodyshop.enforce_class }]}>
name={"class"} <Select options={classOptions} />
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> </Form.Item>
)} )}
{bodyshop.enforce_referral && ( {bodyshop.enforce_referral && (
<> <>
<Form.Item <Form.Item
name={"referral_source"} name="referral_source"
label={t("jobs.fields.referralsource")} label={t("jobs.fields.referralsource")}
rules={[ rules={[{ required: bodyshop.enforce_referral }]}
{
required: bodyshop.enforce_referral
//message: t("general.validation.required"),
}
]}
> >
<Select> <Select options={referralOptions} />
{bodyshop.md_referral_sources.map((s) => (
<Select.Option key={s} value={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 /> <Input />
</Form.Item> </Form.Item>
</> </>
)} )}
{bodyshop.enforce_conversion_csr && ( {bodyshop.enforce_conversion_csr && (
<Form.Item <Form.Item
name={"employee_csr"} name="employee_csr"
label={t( label={t(
InstanceRenderManager({ InstanceRenderManager({
imex: "jobs.fields.employee_csr", imex: "jobs.fields.employee_csr",
rome: "jobs.fields.employee_csr_writer" rome: "jobs.fields.employee_csr_writer"
}) })
)} )}
rules={[ rules={[{ required: bodyshop.enforce_conversion_csr }]}
{
required: bodyshop.enforce_conversion_csr
//message: t("general.validation.required"),
}
]}
> >
<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().includes(input.toLowerCase())
}} }}
style={{ width: 200 }} style={{ width: 200 }}
>
{bodyshop.employees options={csrOptions}
.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> </Form.Item>
)} )}
{bodyshop.enforce_conversion_category && ( {bodyshop.enforce_conversion_category && (
<Form.Item <Form.Item name="category" label={t("jobs.fields.category")} rules={[{ required: bodyshop.enforce_conversion_category }]}>
name={"category"} <Select allowClear options={categoryOptions} />
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> </Form.Item>
)} )}
{bodyshop.region_config.toLowerCase().startsWith("ca") && ( {bodyshop.region_config.toLowerCase().startsWith("ca") && (
<Form.Item label={t("jobs.fields.ca_gst_registrant")} name="ca_gst_registrant" valuePropName="checked"> <Form.Item label={t("jobs.fields.ca_gst_registrant")} name="ca_gst_registrant" valuePropName="checked">
<Switch /> <Switch />
</Form.Item> </Form.Item>
)} )}
<Form.Item label={t("jobs.fields.driveable")} name="driveable" valuePropName="checked"> <Form.Item label={t("jobs.fields.driveable")} name="driveable" valuePropName="checked">
<Switch /> <Switch />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.towin")} name="towin" valuePropName="checked"> <Form.Item label={t("jobs.fields.towin")} name="towin" valuePropName="checked">
<Switch /> <Switch />
</Form.Item> </Form.Item>
@@ -316,6 +313,7 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
> >
{t("jobs.actions.convert")} {t("jobs.actions.convert")}
</Button> </Button>
<Button onClick={handleModalClose} disabled={earlyRoCreatedThisSession && !job.converted}> <Button onClick={handleModalClose} disabled={earlyRoCreatedThisSession && !job.converted}>
{t("general.actions.close")} {t("general.actions.close")}
</Button> </Button>

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

@@ -1,4 +1,5 @@
import { Card, Input, Table } from "antd"; import { Card, Input } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context"; import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
@@ -95,11 +96,12 @@ export default function JobsCreateOwnerInfoSearchComponent({ loading, owners })
/> />
} }
> >
<Table <ResponsiveTable
loading={loading} loading={loading}
scroll={{ x: true }} scroll={{ x: true }}
pagination={{ placement: "top" }} pagination={{ placement: "top" }}
columns={columns} columns={columns}
mobileColumnKeys={["ownr_ln", "ownr_ph1", "ownr_ph2", "ownr_fn"]}
rowKey="id" rowKey="id"
dataSource={owners} dataSource={owners}
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -1,5 +1,6 @@
import { PlusOutlined, SearchOutlined } from "@ant-design/icons"; import { PlusOutlined, SearchOutlined } from "@ant-design/icons";
import { Button, Input, Popover, Table } from "antd"; import { Button, Input, Popover } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import PredefinedVehicles from "./predefined-vehicles.js"; import PredefinedVehicles from "./predefined-vehicles.js";
@@ -22,9 +23,9 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
const popContent = () => ( const popContent = () => (
<div> <div>
<Table <ResponsiveTable
size="small" size="small"
title={() => <Input.Search onSearch={(value) => setSearch(value)} enterButton/>} title={() => <Input.Search onSearch={(value) => setSearch(value)} enterButton />}
dataSource={filteredPredefinedVehicles} dataSource={filteredPredefinedVehicles}
columns={[ columns={[
{ {
@@ -61,6 +62,7 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
) )
} }
]} ]}
mobileColumnKeys={["make", "model", "select"]}
/> />
</div> </div>
); );

View File

@@ -1,4 +1,5 @@
import { Card, Input, Space, Table } from "antd"; import { Card, Input, Space } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -68,11 +69,12 @@ export default function JobsCreateVehicleInfoSearchComponent({ loading, vehicles
</Space> </Space>
} }
> >
<Table <ResponsiveTable
loading={loading} loading={loading}
scroll={{ x: true }} scroll={{ x: true }}
pagination={{ placement: "top" }} pagination={{ placement: "top" }}
columns={columns} columns={columns}
mobileColumnKeys={["v_vin", "description", "plate"]}
rowKey="id" rowKey="id"
dataSource={vehicles} dataSource={vehicles}
onChange={handleTableChange} onChange={handleTableChange}

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,13 +233,10 @@ 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} />

View File

@@ -214,7 +214,7 @@ export function JobsDetailHeaderActions({
okText, okText,
cancelText, cancelText,
centered: true, centered: true,
maskClosable: false, mask: { closable: false },
onCancel: () => { onCancel: () => {
closeConfirmById(id); closeConfirmById(id);
onCancel?.(); onCancel?.();
@@ -714,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

@@ -133,14 +133,16 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
</FormRow> </FormRow>
) )
})} })}
<Divider titlePlacement="left" orientation="horizontal" style={{ marginTop: ".8rem", float: "right" }}>
{t("jobs.forms.laborrates")} <FormRow
</Divider> extra={
<Space> <Space>
<JobsDetailRatesChangeButton form={form} disabled={jobRO} /> <JobsDetailRatesChangeButton form={form} disabled={jobRO} />
{InstanceRenderManager({ imex: <JobsMarkPstExempt form={form} /> })} {InstanceRenderManager({ imex: <JobsMarkPstExempt form={form} /> })}
</Space> </Space>
<FormRow noDivider> }
header={t("jobs.forms.laborrates")}
>
<Form.Item label={t("jobs.fields.labor_rate_desc")} name="labor_rate_desc"> <Form.Item label={t("jobs.fields.labor_rate_desc")} name="labor_rate_desc">
<Input disabled={jobRO} /> <Input disabled={jobRO} />
</Form.Item> </Form.Item>

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