Revert "Release/2026 02 27 (pull request #3070)"

This commit is contained in:
Patrick Fic
2026-03-04 16:18:44 +00:00
parent 522f2b9e26
commit c9e41ba72a
204 changed files with 5497 additions and 7715 deletions

20
.gitignore vendored
View File

@@ -129,26 +129,6 @@ 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
/AGENTS.md
/AI_CONTEXT.md
/CLAUDE.md
/COPILOT.md
/GEMINI.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 be_version="2.7.1" version="1.2"> <babeledit_project version="1.2" be_version="2.7.1">
<!-- <!--
BabelEdit project file BabelEdit project file
@@ -2549,27 +2549,6 @@
</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>
@@ -3015,48 +2994,6 @@
<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>
@@ -3529,310 +3466,6 @@
</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>
@@ -17769,48 +17402,6 @@
</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>
@@ -20632,27 +20223,6 @@
</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>
@@ -21450,27 +21020,6 @@
</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>
@@ -21513,27 +21062,6 @@
</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>
@@ -27132,27 +26660,6 @@
</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>
@@ -27240,27 +26747,6 @@
</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>
@@ -27392,27 +26878,6 @@
</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>
@@ -30493,27 +29958,6 @@
</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>
@@ -30918,90 +30362,6 @@
</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>
@@ -36606,74 +35966,6 @@
</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>
@@ -39767,27 +39059,6 @@
</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,4 +18,3 @@ 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,4 +20,3 @@ 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.35.3", "@amplitude/analytics-browser": "^2.34.0",
"@ant-design/pro-layout": "^7.22.6", "@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.1.6", "@apollo/client": "^4.1.3",
"@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.8", "@firebase/app": "^0.14.7",
"@firebase/auth": "^1.12.0", "@firebase/auth": "^1.12.0",
"@firebase/firestore": "^4.11.0", "@firebase/firestore": "^4.10.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.2.2", "@sentry/cli": "^3.1.0",
"@sentry/react": "^10.40.0", "@sentry/react": "^10.38.0",
"@sentry/vite-plugin": "^4.9.1", "@sentry/vite-plugin": "^4.8.0",
"@splitsoftware/splitio-react": "^2.6.1", "@splitsoftware/splitio-react": "^2.6.1",
"@tanem/react-nprogress": "^5.0.63", "@tanem/react-nprogress": "^5.0.58",
"antd": "^6.3.1", "antd": "^6.2.2",
"apollo-link-logger": "^3.0.0", "apollo-link-logger": "^3.0.0",
"autosize": "^6.0.1", "autosize": "^6.0.1",
"axios": "^1.13.5", "axios": "^1.13.4",
"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.3.1", "dotenv": "^17.2.3",
"env-cmd": "^11.0.0", "env-cmd": "^11.0.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"graphql": "^16.13.0", "graphql": "^16.12.0",
"graphql-ws": "^6.0.7", "graphql-ws": "^6.0.7",
"i18next": "^25.8.13", "i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.0",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.12.38", "libphonenumber-js": "^1.12.36",
"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.71", "phone": "^3.1.70",
"posthog-js": "^1.355.0", "posthog-js": "^1.336.4",
"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.1", "react-router-dom": "^7.13.0",
"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.11", "styled-components": "^6.3.8",
"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.2", "@playwright/test": "^1.58.0",
"@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.4", "@vitejs/plugin-react": "^5.1.2",
"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.3.0", "globals": "^17.2.0",
"jsdom": "^28.1.0", "jsdom": "^27.4.0",
"memfs": "^4.56.10", "memfs": "^4.56.10",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",
"playwright": "^1.58.2", "playwright": "^1.58.0",
"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.5.1", "vite-plugin-babel": "^1.4.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

@@ -1,7 +1,7 @@
import { ApolloProvider } from "@apollo/client/react"; import { ApolloProvider } from "@apollo/client/react";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react"; import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider, Grid } from "antd"; import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US"; import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { CookiesProvider } from "react-cookie"; import { CookiesProvider } from "react-cookie";
@@ -43,47 +43,10 @@ function AppContainer() {
const currentUser = useSelector(selectCurrentUser); const currentUser = useSelector(selectCurrentUser);
const isDarkMode = useSelector(selectDarkMode); const isDarkMode = useSelector(selectDarkMode);
const screens = Grid.useBreakpoint();
const isPhone = !screens.md;
const isUltraWide = Boolean(screens.xxxl);
const theme = useMemo(() => { const theme = useMemo(() => getTheme(isDarkMode), [isDarkMode]);
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 antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
const antdPagination = useMemo(
() => ({
showSizeChanger: !isPhone,
totalBoundaryShowSizeChanger: 100
}),
[isPhone]
);
const antdForm = useMemo( const antdForm = useMemo(
() => ({ () => ({
@@ -159,16 +122,7 @@ function AppContainer() {
return ( return (
<CookiesProvider> <CookiesProvider>
<ApolloProvider client={client}> <ApolloProvider client={client}>
<ConfigProvider <ConfigProvider input={antdInput} locale={enLocale} theme={theme} form={antdForm}>
input={antdInput}
locale={enLocale}
theme={theme}
form={antdForm}
table={antdTable}
pagination={antdPagination}
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
popupOverflow="viewport"
>
<GlobalLoadingBar /> <GlobalLoadingBar />
<SplitFactoryProvider config={config}> <SplitFactoryProvider config={config}>
<SplitClientProvider> <SplitClientProvider>

View File

@@ -443,30 +443,6 @@
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;
//} //}
@@ -499,13 +475,3 @@
margin-left: auto; margin-left: auto;
flex: 0 0 auto; flex: 0 0 auto;
} }
.global-search-autocomplete-fix {
// This is the extra value render that causes the “duplicate text”
.ant-select-selection-item {
position: absolute !important;
left: -10000px !important;
pointer-events: none !important;
}
}

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(isDarkMode)) ...defaultsDeep(currentTheme, defaultTheme)
}); });
export default getTheme; export default getTheme;

View File

@@ -1,4 +1,4 @@
import { Card, Checkbox, Input, Space } from "antd"; import { Card, Checkbox, Input, Space, Table } 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,7 +16,6 @@ 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({
@@ -180,12 +179,11 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
</Space> </Space>
} }
> >
<ResponsiveTable <Table
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 } from "antd"; import { Card, Input, Space, Table } 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,7 +17,6 @@ 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
@@ -193,12 +192,11 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
</Space> </Space>
} }
> >
<ResponsiveTable <Table
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 } from "antd"; import { Button, Card, Input, Space, Table } 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,7 +17,6 @@ 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
@@ -210,12 +209,11 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
</Space> </Space>
} }
> >
<ResponsiveTable <Table
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,18 +29,19 @@ export function AllocationsAssignmentComponent({
<Select <Select
id="employeeSelector" id="employeeSelector"
showSearch={{ showSearch={{
optionFilterProp: "label", optionFilterProp: "children",
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 filterOption: (input, option) => option.props.children.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) => ({ >
value: emp.id, {bodyshop.employees.map((emp) => (
key: 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>
<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,17 +31,19 @@ export default connect(
<div> <div>
<Select <Select
showSearch={{ showSearch={{
optionFilterProp: "label", optionFilterProp: "children",
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 filterOption: (input, option) => option.props.children.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) => ({ >
value: emp.id, {bodyshop.employees.map((emp) => (
label: `${emp.first_name} ${emp.last_name}` <Select.Option value={emp.id} key={emp.id}>
}))} {`${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 ResponsiveTable from "../responsive-table/responsive-table.component"; import { Table } from "antd";
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,12 +62,11 @@ export default function AuditTrailListComponent({ loading, data }) {
}; };
return ( return (
<ResponsiveTable <Table
{...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 ResponsiveTable from "../responsive-table/responsive-table.component"; import { Table } from "antd";
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,12 +47,11 @@ export default function EmailAuditTrailListComponent({ loading, data }) {
}; };
return ( return (
<ResponsiveTable <Table
{...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,20 +28,6 @@ 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);
@@ -88,9 +74,8 @@ 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={initialValues} onFinish={handleFinish} form={form}> <Form initialValues={data?.bills_by_pk} onFinish={handleFinish} form={form}>
<Form.List name={["billlines"]}> <Form.List name={["billlines"]}>
{(fields) => { {(fields) => {
return ( return (
@@ -110,10 +95,9 @@ 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.oem_partno")}</td> <td>{t("billlines.fields.quantity")}</td>
<td style={{ textAlign: "right" }}>{t("billlines.fields.quantity")}</td> <td>{t("billlines.fields.actual_price")}</td>
<td style={{ textAlign: "right" }}>{t("billlines.fields.actual_price")}</td> <td>{t("billlines.fields.actual_cost")}</td>
<td style={{ textAlign: "right" }}>{t("billlines.fields.actual_cost")}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -143,15 +127,6 @@ 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`}
@@ -160,7 +135,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
<ReadOnlyFormItemComponent /> <ReadOnlyFormItemComponent />
</Form.Item> </Form.Item>
</td> </td>
<td style={{ textAlign: "right" }}> <td>
<Form.Item <Form.Item
// label={t("joblines.fields.actual_price")} // label={t("joblines.fields.actual_price")}
key={`${index}actual_price`} key={`${index}actual_price`}
@@ -169,7 +144,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
<ReadOnlyFormItemComponent type="currency" /> <ReadOnlyFormItemComponent type="currency" />
</Form.Item> </Form.Item>
</td> </td>
<td style={{ textAlign: "right" }}> <td>
<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,8 +7,10 @@ export default function BillDetailEditcontainer() {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useNavigate(); const history = useNavigate();
const screens = Grid.useBreakpoint(); const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = { const bpoints = {
xs: "100%", xs: "100%",
sm: "100%", sm: "100%",
@@ -17,14 +19,7 @@ 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

@@ -1,203 +0,0 @@
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,11 +2,10 @@ 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, useRef, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import 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";
@@ -22,12 +21,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 as handleUploadToImageProxy } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility"; import { handleUpload } from "../documents-upload/documents-upload.utility";
import { handleUpload as handleUploadToImageProxy } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal, billEnterModal: selectBillEnterModal,
@@ -51,20 +50,15 @@ 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, Bill_OCR_AI } treatments: { Enhanced_Payroll, Imgproxy }
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
attributes: {}, attributes: {},
names: ["Enhanced_Payroll", "Imgproxy", "Bill_OCR_AI"], names: ["Enhanced_Payroll", "Imgproxy"],
splitKey: bodyshop.imexshopid splitKey: bodyshop.imexshopid
}); });
@@ -119,8 +113,6 @@ 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;
@@ -386,7 +378,6 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
vendorid: values.vendorid, vendorid: values.vendorid,
billlines: [] billlines: []
}); });
setIsAiScan(false);
// form.resetFields(); // form.resetFields();
} else { } else {
toggleModalVisible(); toggleModalVisible();
@@ -397,22 +388,10 @@ 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]);
@@ -422,44 +401,12 @@ 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={ title={t("bills.labels.new")}
<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")}
@@ -500,25 +447,13 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
autoComplete={"off"} autoComplete={"off"}
layout="vertical" layout="vertical"
form={form} form={form}
onFinishFailed={(errorInfo) => { onFinishFailed={() => {
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);
}
}} }}
> >
<div ref={formTopRef}> <RbacWrapper action="bills:enter">
<RbacWrapper action="bills:enter"> <BillFormContainer form={form} disableInvNumber={billEnterModal.context.disableInvNumber} />
<BillFormContainer </RbacWrapper>
form={form}
isAiScan={isAiScan}
disableInvNumber={billEnterModal.context.disableInvNumber}
/>
</RbacWrapper>
</div>
</Form> </Form>
</Modal> </Modal>
); );

View File

@@ -1,5 +1,4 @@
import { Form, Input } from "antd"; import { Form, Input, 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 CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
@@ -109,14 +108,7 @@ 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 />
<ResponsiveTable <Table pagination={false} size="small" columns={columns} rowKey="id" dataSource={data} />
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,22 +99,20 @@ 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 <Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
showSearch {bodyshopHasDmsKey(bodyshop)
style={{ minWidth: "3rem" }} ? CiecaSelect(true, false)
disabled={disabled} : responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
options={ </Select>
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 <Select disabled={disabled}>
disabled={disabled} {bodyshop.md_parts_locations.map((loc, idx) => (
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))} <Select.Option key={idx} value={loc}>
/> {loc}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("billlines.fields.deductedfromlbr")} label={t("billlines.fields.deductedfromlbr")}
@@ -138,10 +136,22 @@ export function BillFormItemsExtendedFormItem({
]} ]}
name={["billlineskeys", record.id, "lbr_adjustment", "mod_lbr_ty"]} name={["billlineskeys", record.id, "lbr_adjustment", "mod_lbr_ty"]}
> >
<Select <Select allowClear>
allowClear <Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
options={CiecaSelect(false, true)} <Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
/> <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,15 +8,12 @@ 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";
@@ -24,6 +21,8 @@ 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
@@ -44,14 +43,11 @@ 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 }
@@ -127,23 +123,6 @@ 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} />
@@ -349,12 +328,13 @@ 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 <Select style={{ width: "10rem" }} disabled={disabled} allowClear>
style={{ width: "10rem" }} {bodyshop.md_parts_locations.map((loc, idx) => (
disabled={disabled} <Select.Option key={idx} value={loc}>
allowClear {loc}
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))} </Select.Option>
/> ))}
</Select>
</Form.Item> </Form.Item>
)} )}
</LayoutFormRow> </LayoutFormRow>
@@ -394,15 +374,7 @@ 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) {
try { totals = CalculateBillTotal(values);
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) {
@@ -480,7 +452,6 @@ 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,isAiScan }) { export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber, disableInHouse }) {
const { const {
treatments: { Simple_Inventory } treatments: { Simple_Inventory }
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
@@ -50,7 +50,6 @@ 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,15 +5,14 @@ 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 { selectDarkMode } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectDarkMode } from "../../redux/application/application.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 ConfidenceDisplay from "./bill-form.lines.confidence.component.jsx"; import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -30,8 +29,7 @@ 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;
@@ -141,29 +139,6 @@ 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",
@@ -237,7 +212,6 @@ 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",
@@ -276,16 +250,7 @@ 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: [ rules: [{ required: true }]
{ 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
@@ -434,17 +399,11 @@ export function BillEnterModalLinesComponent({
rules: [{ required: true }] rules: [{ required: true }]
}), }),
formInput: () => ( formInput: () => (
<Select <Select showSearch style={{ minWidth: "3rem" }} disabled={disabled} tabIndex={0}>
showSearch {bodyshopHasDmsKey(bodyshop)
style={{ minWidth: "3rem" }} ? CiecaSelect(true, false)
disabled={disabled} : responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
tabIndex={0} </Select>
options={
bodyshopHasDmsKey(bodyshop)
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => ({ value: item.name, label: item.name }))
}
/>
) )
}, },
...(billEdit ...(billEdit
@@ -460,11 +419,13 @@ export function BillEnterModalLinesComponent({
name: [field.name, "location"] name: [field.name, "location"]
}), }),
formInput: () => ( formInput: () => (
<Select <Select disabled={disabled} tabIndex={0}>
disabled={disabled} {bodyshop.md_parts_locations.map((loc, idx) => (
tabIndex={0} <Select.Option key={idx} value={loc}>
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))} {loc}
/> </Select.Option>
))}
</Select>
) )
} }
]), ]),
@@ -505,10 +466,22 @@ 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 <Select allowClear>
allowClear <Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
options={CiecaSelect(false, true)} <Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
/> <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

@@ -1,87 +0,0 @@
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 } from "antd"; import { Button, Card, Checkbox, Input, Space, Table } 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,7 +18,6 @@ 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({
@@ -238,13 +237,12 @@ export function BillsListTableComponent({
</Space> </Space>
} }
> >
<ResponsiveTable <Table
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,8 +3,7 @@ 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 } from "antd"; import { Input, Table } 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";
@@ -80,7 +79,7 @@ export default function BillsVendorsList() {
: (data && data.vendors) || []; : (data && data.vendors) || [];
return ( return (
<ResponsiveTable <Table
loading={loading} loading={loading}
title={() => { title={() => {
return ( return (
@@ -92,7 +91,6 @@ 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,12 +19,13 @@ 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) => ({ >
key: item.id || idx, {roOptions.map((item, idx) => (
value: item.id || idx, <Select.Option key={item.id || idx}>
label: ` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}` {` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`}
}))} </Select.Option>
/> ))}
</Select>
</div> </div>
{loading ? <LoadingOutlined /> : null} {loading ? <LoadingOutlined /> : null}

View File

@@ -1,5 +1,4 @@
import { Card, Input } from "antd"; import { Card, Input, 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 { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
@@ -104,11 +103,10 @@ export default function ContractsCarsComponent({ loading, data, selectedCarId, h
/> />
} }
> >
<ResponsiveTable <Table
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>
options={bodyshop.md_ins_cos.map((s) => ({ {bodyshop.md_ins_cos.map((s) => (
key: s.name, <Select.Option key={s.name} value={s.name}>
value: s.name, {s.name}
label: s.name </Select.Option>
}))} ))}
/> </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>
options={bodyshop.md_classes.map((s) => ({ {bodyshop.md_classes.map((s) => (
key: s, <Select.Option key={s} value={s}>
value: s, {s}
label: s </Select.Option>
}))} ))}
/> </Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("contracts.labels.convertform.applycleanupcharge")} label={t("contracts.labels.convertform.applycleanupcharge")}

View File

@@ -1,5 +1,4 @@
import { Card, Input } from "antd"; import { Card, Input, Table } 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";
@@ -128,11 +127,10 @@ export default function ContractsJobsComponent({ loading, data, selectedJob, han
/> />
} }
> >
<ResponsiveTable <Table
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,6 +2,8 @@ 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();
@@ -13,17 +15,11 @@ const ContractStatusComponent = ({ value, onChange, ref }) => {
}, [value, option, onChange]); }, [value, option, onChange]);
return ( return (
<Select <Select ref={ref} value={option} style={{ width: 100 }} onChange={setOption}>
ref={ref} <Option value="contracts.status.new">{t("contracts.status.new")}</Option>
value={option} <Option value="contracts.status.out">{t("contracts.status.out")}</Option>
style={{ width: 100 }} <Option value="contracts.status.returned">{t("contracts.status.out")}</Option>
onChange={setOption} </Select>
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,6 +1,5 @@
import { useLazyQuery } from "@apollo/client/react"; import { useLazyQuery } from "@apollo/client/react";
import { Button, Form, Modal } from "antd"; import { Button, Form, Modal, Table } 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";
@@ -65,7 +64,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)} />}
<ResponsiveTable <Table
loading={loading} loading={loading}
columns={[ columns={[
{ {
@@ -125,7 +124,6 @@ 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, Typography } from "antd"; import { Button, Card, Input, Space, Table, 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,7 +14,6 @@ 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
@@ -171,14 +170,13 @@ export function ContractsList({ bodyshop, loading, contracts, refetch, total, se
} }
> >
<ContractsFindModalContainer /> <ContractsFindModalContainer />
<ResponsiveTable <Table
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,5 +1,4 @@
import { Card } from "antd"; import { Card, Table } 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";
@@ -74,11 +73,10 @@ export default function CourtesyCarContractListComponent({ contracts, totalContr
return ( return (
<Card title={t("menus.header.courtesycars-contracts")}> <Card title={t("menus.header.courtesycars-contracts")}>
<ResponsiveTable <Table
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,6 +2,8 @@ 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();
@@ -21,11 +23,10 @@ const CourtesyCarReadinessComponent = ({ value, onChange, ref }) => {
width: 100 width: 100
}} }}
onChange={setOption} onChange={setOption}
options={[ >
{ value: "courtesycars.readiness.ready", label: t("courtesycars.readiness.ready") }, <Option value="courtesycars.readiness.ready">{t("courtesycars.readiness.ready")}</Option>
{ value: "courtesycars.readiness.notready", label: t("courtesycars.readiness.notready") } <Option value="courtesycars.readiness.notready">{t("courtesycars.readiness.notready")}</Option>
]} </Select>
/>
); );
}; };
export default CourtesyCarReadinessComponent; export default CourtesyCarReadinessComponent;

View File

@@ -2,6 +2,8 @@ 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();
@@ -20,15 +22,14 @@ const CourtesyCarStatusComponent = ({ value, onChange, ref }) => {
width: 100 width: 100
}} }}
onChange={setOption} onChange={setOption}
options={[ >
{ value: "courtesycars.status.in", label: t("courtesycars.status.in") }, <Option value="courtesycars.status.in">{t("courtesycars.status.in")}</Option>
{ value: "courtesycars.status.inservice", label: t("courtesycars.status.inservice") }, <Option value="courtesycars.status.inservice">{t("courtesycars.status.inservice")}</Option>
{ value: "courtesycars.status.out", label: t("courtesycars.status.out") }, <Option value="courtesycars.status.out">{t("courtesycars.status.out")}</Option>
{ value: "courtesycars.status.sold", label: t("courtesycars.status.sold") }, <Option value="courtesycars.status.sold">{t("courtesycars.status.sold")}</Option>
{ value: "courtesycars.status.leasereturn", label: t("courtesycars.status.leasereturn") }, <Option value="courtesycars.status.leasereturn">{t("courtesycars.status.leasereturn")}</Option>
{ value: "courtesycars.status.unavailable", label: t("courtesycars.status.unavailable") } <Option value="courtesycars.status.unavailable">{t("courtesycars.status.unavailable")}</Option>
]} </Select>
/>
); );
}; };
export default CourtesyCarStatusComponent; export default CourtesyCarStatusComponent;

View File

@@ -1,6 +1,5 @@
import { SyncOutlined, WarningFilled } from "@ant-design/icons"; import { SyncOutlined, WarningFilled } from "@ant-design/icons";
import { Button, Card, Dropdown, Input, Space, Tooltip } from "antd"; import { Button, Card, Dropdown, Input, Space, Table, 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";
@@ -276,11 +275,10 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
</Space> </Space>
} }
> >
<ResponsiveTable <Table
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,6 +1,5 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { Button, Card } from "antd"; import { Button, Card, Table } 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";
@@ -87,11 +86,10 @@ export default function CsiResponseListPaginated({ refetch, loading, responses,
return ( return (
<Card extra={<Button onClick={() => refetch()} icon={<SyncOutlined />} />}> <Card extra={<Button onClick={() => refetch()} icon={<SyncOutlined />} />}>
<ResponsiveTable <Table
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,5 +1,4 @@
import { Card, Tag } from "antd"; import { Card, Table, 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";
@@ -183,11 +182,10 @@ 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")}>
<ResponsiveTable <Table
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, Typography } from "antd"; import { Card, Input, Space, Table, 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,7 +7,6 @@ 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();
@@ -104,33 +103,31 @@ export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
{...cardProps} {...cardProps}
> >
<LoadingSkeleton loading={loading}> <LoadingSkeleton loading={loading}>
<div style={{ height: "100%", minHeight: 0, width: "100%", overflow: "auto" }}> <div style={{ height: "100%" }}>
<ResponsiveTable <Table
size="small"
tableLayout="fixed"
onChange={handleTableChange} onChange={handleTableChange}
pagination={{ placement: "top", defaultPageSize: pageLimit }} pagination={{ placement: "top", defaultPageSize: pageLimit }}
columns={columns} columns={columns}
scroll={{ x: "max-content" }} scroll={{ x: true, y: "calc(100% - 4em)" }}
rowKey="id" rowKey="id"
style={{ width: "100%" }} style={{ height: "100%" }}
dataSource={filteredData} dataSource={filteredData}
summary={() => ( summary={() => (
<ResponsiveTable.Summary.Row> <Table.Summary.Row>
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>
<Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title> <Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>
{Dinero(costingData?.allSummaryData && costingData.allSummaryData.totalSales).toFormat()} {Dinero(costingData?.allSummaryData && costingData.allSummaryData.totalSales).toFormat()}
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>
{Dinero(costingData?.allSummaryData && costingData.allSummaryData.totalCost).toFormat()} {Dinero(costingData?.allSummaryData && costingData.allSummaryData.totalCost).toFormat()}
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>
{Dinero(costingData?.allSummaryData && costingData.allSummaryData.gpdollars).toFormat()} {Dinero(costingData?.allSummaryData && costingData.allSummaryData.gpdollars).toFormat()}
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell></ResponsiveTable.Summary.Cell> <Table.Summary.Cell></Table.Summary.Cell>
</ResponsiveTable.Summary.Row> </Table.Summary.Row>
)} )}
/> />
</div> </div>

View File

@@ -363,7 +363,6 @@ 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,7 +368,6 @@ 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,7 +363,6 @@ 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,6 +1,5 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Form, Input } from "antd"; import { Button, Card, Form, Input, Table } 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";
@@ -79,7 +78,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
dataIndex: "Lines", dataIndex: "Lines",
key: "Lines", key: "Lines",
render: (text, record) => ( render: (text, record) => (
<ResponsiveTable style={{ tableLayout: "auto", width: "100%" }}> <table 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>
@@ -92,7 +91,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
<td>{l.Amount}</td> <td>{l.Amount}</td>
</tr> </tr>
))} ))}
</ResponsiveTable> </table>
) )
} }
]; ];
@@ -116,10 +115,9 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
/> />
} }
> >
<ResponsiveTable <Table
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,5 +1,4 @@
import { Alert, Button, Card, Typography } from "antd"; import { Alert, Button, Card, Table, 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";
@@ -117,10 +116,9 @@ 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")} />
)} )}
<ResponsiveTable <Table
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") }}
@@ -137,17 +135,15 @@ export function DmsAllocationsSummary({ mode, socket, bodyshop, jobId, title, on
const hasNonZeroSaleTotal = totals.totalSale.getAmount() !== 0; const hasNonZeroSaleTotal = totals.totalSale.getAmount() !== 0;
return ( return (
<ResponsiveTable.Summary.Row> <Table.Summary.Row>
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>
<Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title> <Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>{hasNonZeroSaleTotal ? totals.totalSale.toFormat() : null}</Table.Summary.Cell>
{hasNonZeroSaleTotal ? totals.totalSale.toFormat() : null} <Table.Summary.Cell />
</ResponsiveTable.Summary.Cell> <Table.Summary.Cell />
<ResponsiveTable.Summary.Cell /> <Table.Summary.Cell />
<ResponsiveTable.Summary.Cell /> </Table.Summary.Row>
<ResponsiveTable.Summary.Cell />
</ResponsiveTable.Summary.Row>
); );
}} }}
/> />

View File

@@ -1,5 +1,4 @@
import { Alert, Button, Card, Tabs, Typography } from "antd"; import { Alert, Button, Card, Table, 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";
@@ -262,10 +261,9 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
into taxable / non-taxable segments. into taxable / non-taxable segments.
</Typography.Paragraph> </Typography.Paragraph>
<ResponsiveTable <Table
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." }}
@@ -288,23 +286,19 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
const hasCostTotal = Number(roggTotals.totalDlrCost) !== 0; const hasCostTotal = Number(roggTotals.totalDlrCost) !== 0;
return ( return (
<ResponsiveTable.Summary.Row> <Table.Summary.Row>
<ResponsiveTable.Summary.Cell index={0}> <Table.Summary.Cell index={0}>
<Typography.Title level={5}>{t("general.labels.totals")}</Typography.Title> <Typography.Title level={5}>{t("general.labels.totals")}</Typography.Title>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell index={1} /> <Table.Summary.Cell index={1} />
<ResponsiveTable.Summary.Cell index={2} /> <Table.Summary.Cell index={2} />
<ResponsiveTable.Summary.Cell index={3} /> <Table.Summary.Cell index={3} />
<ResponsiveTable.Summary.Cell index={4} /> <Table.Summary.Cell index={4} />
<ResponsiveTable.Summary.Cell index={5} /> <Table.Summary.Cell index={5} />
<ResponsiveTable.Summary.Cell index={6} /> <Table.Summary.Cell index={6} />
<ResponsiveTable.Summary.Cell index={7}> <Table.Summary.Cell index={7}>{hasCustTotal ? roggTotals.totalCustPrice : null}</Table.Summary.Cell>
{hasCustTotal ? roggTotals.totalCustPrice : null} <Table.Summary.Cell index={8}>{hasCostTotal ? roggTotals.totalDlrCost : null}</Table.Summary.Cell>
</ResponsiveTable.Summary.Cell> </Table.Summary.Row>
<ResponsiveTable.Summary.Cell index={8}>
{hasCostTotal ? roggTotals.totalDlrCost : null}
</ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
); );
}} }}
/> />
@@ -319,10 +313,9 @@ 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>
<ResponsiveTable <Table
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,6 +1,5 @@
import { useLazyQuery } from "@apollo/client/react"; import { useLazyQuery } from "@apollo/client/react";
import { Button, Input, Modal } from "antd"; import { Button, Input, Modal, 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 { connect } from "react-redux"; import { connect } from "react-redux";
@@ -61,7 +60,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" />}
<ResponsiveTable <Table
title={() => ( title={() => (
<Input.Search <Input.Search
onSearch={(val) => callSearch({ variables: { search: val } })} onSearch={(val) => callSearch({ variables: { search: val } })}
@@ -70,7 +69,6 @@ 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,5 +1,4 @@
import { Button, Checkbox, Col } from "antd"; import { Button, Checkbox, Col, Table } 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";
@@ -73,7 +72,7 @@ export default function CDKCustomerSelector({ bodyshop, socket }) {
return ( return (
<Col span={24}> <Col span={24}>
<ResponsiveTable <Table
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}>
@@ -87,7 +86,6 @@ 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,5 +1,4 @@
import { Button, Checkbox, Col } from "antd"; import { Button, Checkbox, Col, Table } 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";
@@ -79,7 +78,7 @@ export default function FortellisCustomerSelector({ bodyshop, jobid, socket }) {
return ( return (
<Col span={24}> <Col span={24}>
<ResponsiveTable <Table
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}>
@@ -93,7 +92,6 @@ 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,5 +1,4 @@
import { Button, Col } from "antd"; import { Button, Col, Table } 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";
@@ -67,7 +66,7 @@ export default function PBSCustomerSelector({ bodyshop, socket }) {
return ( return (
<Col span={24}> <Col span={24}>
<ResponsiveTable <Table
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}>
@@ -81,7 +80,6 @@ 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,5 +1,4 @@
import { Alert, Button, Checkbox, message, Modal, Space } from "antd"; import { Alert, Button, Checkbox, message, Modal, Space, Table } 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";
@@ -73,14 +72,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;
@@ -196,8 +195,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, click{" "} We created the Repair Order. Please validate the totals and taxes in the DMS system. When done,
<strong>Finished</strong> to finalize and mark this export as complete. click <strong>Finished</strong> to finalize and mark this export as complete.
</div> </div>
<div> <div>
<Space> <Space>
@@ -216,8 +215,14 @@ export default function RRCustomerSelector({
} }
return ( return (
<Modal open={open} onCancel={handleClose} footer={null} width={800} title={t("dms.selectCustomer")}> <Modal
<ResponsiveTable open={open}
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 */}
@@ -299,7 +304,6 @@ 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,7 +4,6 @@ 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
@@ -20,35 +19,25 @@ 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;
let style = document.getElementById("json-highlight-styles"); if (document.getElementById("json-highlight-styles")) return;
if (!style) { const style = document.createElement("style");
style = document.createElement("style"); style.id = "json-highlight-styles";
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
@@ -76,13 +65,6 @@ 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,
@@ -110,42 +92,10 @@ export function DmsLogEvents({
return next; return next;
}) })
} }
style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }} style={{ cursor: "pointer", userSelect: "none" }}
> >
{isOpen ? t("dms.labels.hide_details") : t("dms.labels.details")} {isOpen ? "Hide details" : "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>
@@ -153,30 +103,14 @@ 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={removeXmlFromMeta(meta)} colorize={colorizeJson} /> <JsonBlock isDarkMode={isDarkMode} data={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, copiedKey, isDarkMode, showDetails, allowXmlPayload, t] [logs, openSet, colorizeJson, isDarkMode, showDetails]
); );
return <Timeline reverse items={items} />; return <Timeline reverse items={items} />;
@@ -245,121 +179,6 @@ 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
@@ -391,105 +210,6 @@ 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,19 +272,11 @@ export default function CdkLikePostForm({ bodyshop, socket, job, logsRef, mode,
name={[field.name, "name"]} name={[field.name, "name"]}
rules={[{ required: true }]} rules={[{ required: true }]}
> >
<Select <Select style={{ width: "100%" }} onSelect={(value) => handlePayerSelect(value, index)}>
showSearch={{ {bodyshop.cdk_configuration?.payers?.map((payer) => (
optionFilterProp: "label", <Select.Option key={payer.name}>{payer.name}</Select.Option>
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,6 +1,5 @@
import { ReloadOutlined } from "@ant-design/icons"; import { ReloadOutlined } from "@ant-design/icons";
import { Alert, Button, Form, Input, InputNumber, Modal, Radio, Select, Space, Typography } from "antd"; import { Alert, Button, Form, Input, InputNumber, Modal, Radio, Select, Space, Table, 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
@@ -27,14 +26,7 @@ function CustomerSelectorTable({ customers, onSelect, isSubmitting }) {
return ( return (
<div> <div>
<ResponsiveTable <Table columns={columns} dataSource={customers} rowKey="custNo" pagination={false} size="small" />
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,13 +86,11 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b
} }
]} ]}
> >
<Select <Select>
options={[ <Select.Option key={currentUser.email}>{currentUser.email}</Select.Option>
{ key: currentUser.email, value: currentUser.email, label: currentUser.email }, <Select.Option key={bodyshop.email}>{bodyshop.email}</Select.Option>
{ key: bodyshop.email, value: bodyshop.email, label: bodyshop.email }, {bodyshop.md_from_emails && bodyshop.md_from_emails.map((e) => <Select.Option key={e}>{e}</Select.Option>)}
...(bodyshop.md_from_emails ? bodyshop.md_from_emails.map((e) => ({ key: e, value: e, label: e })) : []) </Select>
]}
/>
</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}
mask={{ closable: false }} maskClosable={false}
width={"80%"} width={"80%"}
onOk={() => form.submit()} onOk={() => form.submit()}
title={t("emails.labels.emailpreview")} title={t("emails.labels.emailpreview")}

View File

@@ -1,6 +1,7 @@
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 }) => {
@@ -11,24 +12,26 @@ 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,6 +1,7 @@
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 }) => {
@@ -11,29 +12,30 @@ 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,13 +39,11 @@ export default function FormsFieldChanged({ form, skipPrompt }) {
{errors.length > 0 && ( {errors.length > 0 && (
<AlertComponent <AlertComponent
type="error" type="error"
message={t("general.labels.validationerror")} title={
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,29 +184,22 @@ export default function GlobalSearchOs() {
return ( return (
<AutoComplete <AutoComplete
options={data} options={data}
defaultActiveFirstOption onSearch={handleSearch}
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,6 +160,9 @@ 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;
@@ -169,13 +172,11 @@ 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, Typography } from "antd"; import { Button, Card, Input, Space, Table, 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,7 +10,6 @@ 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) => ({
@@ -186,11 +185,10 @@ export function JobsList({ refetch, loading, jobs, total, setInventoryUpsertCont
</Space> </Space>
} }
> >
<ResponsiveTable <Table
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,19 +67,16 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
); );
}; };
const handleInsSelect = (value) => { const handleInsSelect = (value, option) => {
const selectedVendor = bodyshop.md_ins_cos.find(s => s.name === value); form.setFieldsValue({
if (selectedVendor) { addr1: option.obj.name,
form.setFieldsValue({ addr2: option.obj.street1,
addr1: selectedVendor.name, addr3: option.obj.street2,
addr2: selectedVendor.street1, city: option.obj.city,
addr3: selectedVendor.street2, state: option.obj.state,
city: selectedVendor.city, zip: option.obj.zip,
state: selectedVendor.state, vendorid: null
zip: selectedVendor.zip, });
vendorid: null
});
}
}; };
const handleVendorSelect = (vendorid) => { const handleVendorSelect = (vendorid) => {
@@ -106,13 +103,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 <Select onSelect={handleInsSelect}>
onSelect={handleInsSelect} {bodyshop.md_ins_cos.map((s) => (
options={bodyshop.md_ins_cos.map((s) => ({ <Select.Option key={s.name} obj={s} value={s.name}>
value: s.name, {s.name}
label: s.name </Select.Option>
}))} ))}
/> </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,7 +1,6 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client/react"; import { useQuery } from "@apollo/client/react";
import { Button, Card, Col, Row, Tag } from "antd"; import { Button, Card, Col, Row, Table, 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";
@@ -164,24 +163,12 @@ export function JobAuditTrail({ bodyshop, jobId }) {
/> />
} }
> >
<ResponsiveTable <Table loading={loading} columns={columns} rowKey="id" dataSource={data ? data.audit_trail : []} />
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")}>
<ResponsiveTable <Table loading={loading} columns={emailColumns} rowKey="id" dataSource={data ? data.email_audit_trail : []} />
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,6 +1,5 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Alert, Card } from "antd"; import { Alert, Card, Table } 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";
@@ -69,15 +68,7 @@ export function JobCloseRGuardPpd({ job, warningCallback }) {
return ( return (
<Card title={t("jobs.labels.ppdnotexported")}> <Card title={t("jobs.labels.ppdnotexported")}>
<ResponsiveTable <Table dataSource={linesWithPPD} columns={columns} pagination={false} rowKey="id" bordered size="small" />
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,7 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Alert, Card } from "antd"; import { Alert, Card, Table } 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";
@@ -63,15 +62,7 @@ export function JobCloseRGuardSublet({ job, warningCallback }) {
return ( return (
<Card title={t("jobs.labels.subletsnotcompleted")}> <Card title={t("jobs.labels.subletsnotcompleted")}>
<ResponsiveTable <Table dataSource={subletsNotDone} columns={columns} pagination={false} rowKey="id" bordered size="small" />
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,5 +1,4 @@
import { Input, Space, Typography } from "antd"; import { Input, Space, Table, 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";
@@ -66,7 +65,7 @@ export default function JobCostingPartsTable({ data, summaryData }) {
return ( return (
<div> <div>
<ResponsiveTable <Table
title={() => { title={() => {
return ( return (
<Space wrap> <Space wrap>
@@ -88,19 +87,18 @@ 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={() => (
<ResponsiveTable.Summary.Row> <Table.Summary.Row>
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>
<Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title> <Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell>{Dinero(summaryData.totalSales).toFormat()}</ResponsiveTable.Summary.Cell> <Table.Summary.Cell>{Dinero(summaryData.totalSales).toFormat()}</Table.Summary.Cell>
<ResponsiveTable.Summary.Cell>{Dinero(summaryData.totalCost).toFormat()}</ResponsiveTable.Summary.Cell> <Table.Summary.Cell>{Dinero(summaryData.totalCost).toFormat()}</Table.Summary.Cell>
<ResponsiveTable.Summary.Cell>{Dinero(summaryData.gpdollars).toFormat()}</ResponsiveTable.Summary.Cell> <Table.Summary.Cell>{Dinero(summaryData.gpdollars).toFormat()}</Table.Summary.Cell>
<ResponsiveTable.Summary.Cell></ResponsiveTable.Summary.Cell> <Table.Summary.Cell></Table.Summary.Cell>
</ResponsiveTable.Summary.Row> </Table.Summary.Row>
)} )}
/> />
</div> </div>

View File

@@ -58,8 +58,10 @@ 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 screens = Grid.useBreakpoint(); const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = { const bpoints = {
xs: "100%", xs: "100%",
sm: "100%", sm: "100%",
@@ -68,14 +70,7 @@ 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 ResponsiveTable from "../responsive-table/responsive-table.component"; import { Table } from "antd";
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,12 +101,7 @@ 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} />
<ResponsiveTable <Table rowKey="id" columns={columns} dataSource={filteredJobLines || []} />
rowKey="id"
columns={columns}
mobileColumnKeys={["status", "line_desc", "part_type", "part_qty"]}
dataSource={filteredJobLines || []}
/>
</CardTemplate> </CardTemplate>
</div> </div>
); );

View File

@@ -690,7 +690,6 @@ 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,7 +1,6 @@
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, Tag } from "antd"; import { Badge, Card, Space, Table, 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";
@@ -312,13 +311,12 @@ export function JobLifecycleComponent({ bodyshop, job, statuses }) {
</> </>
} }
> >
<ResponsiveTable <Table
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,15 +88,17 @@ export function JoblineBulkAssign({ setSelectedLines, selectedLines, insertAudit
> >
<Select <Select
showSearch={{ showSearch={{
optionFilterProp: "label", optionFilterProp: "children",
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}} }}
style={{ width: 200 }} style={{ width: 200 }}
options={bodyshop.employee_teams.map((team) => ({ >
value: team.id, {bodyshop.employee_teams.map((team) => (
label: team.name <Select.Option value={team.id} key={team.id} name={team.name}>
}))} {team.name}
/> </Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
<Space wrap> <Space wrap>

View File

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

View File

@@ -115,18 +115,19 @@ export function JobLineDispatchButton({
> >
<Select <Select
showSearch={{ showSearch={{
optionFilterProp: "label", optionFilterProp: "children",
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 filterOption: (input, option) => option.props.children.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) => (
value: emp.id, <Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}>
key: emp.id, {`${emp.first_name} ${emp.last_name}`}
label: `${emp.first_name} ${emp.last_name}` </Select.Option>
}))} ))}
/> </Select>
</Form.Item> </Form.Item>
<Space wrap> <Space wrap>

View File

@@ -64,12 +64,13 @@ 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) => ({ >
key: idx, {Object.values(bodyshop.md_order_statuses).map((s, idx) => (
value: s, <Select.Option key={idx} value={s}>
label: s {s}
}))} </Select.Option>
/> ))}
</Select>
</LoadingSpinner> </LoadingSpinner>
</div> </div>
); );

View File

@@ -75,12 +75,13 @@ 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) => ({ >
key: s.id, {Object.values(bodyshop.employee_teams).map((s, idx) => (
value: s.id, <Select.Option key={idx} value={s.id}>
label: s.name {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 options={[ <Select allowClear>
{ value: "LAA", label: t("joblines.fields.lbr_types.LAA") }, <Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
{ value: "LAB", label: t("joblines.fields.lbr_types.LAB") }, <Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
{ value: "LAD", label: t("joblines.fields.lbr_types.LAD") }, <Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
{ value: "LAE", label: t("joblines.fields.lbr_types.LAE") }, <Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
{ value: "LAF", label: t("joblines.fields.lbr_types.LAF") }, <Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
{ value: "LAG", label: t("joblines.fields.lbr_types.LAG") }, <Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
{ value: "LAM", label: t("joblines.fields.lbr_types.LAM") }, <Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
{ value: "LAR", label: t("joblines.fields.lbr_types.LAR") }, <Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
{ value: "LAS", label: t("joblines.fields.lbr_types.LAS") }, <Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
{ value: "LAU", label: t("joblines.fields.lbr_types.LAU") }, <Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
{ value: "LA1", label: t("joblines.fields.lbr_types.LA1") }, <Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
{ value: "LA2", label: t("joblines.fields.lbr_types.LA2") }, <Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
{ value: "LA3", label: t("joblines.fields.lbr_types.LA3") }, <Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
{ value: "LA4", label: t("joblines.fields.lbr_types.LA4") } <Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
]} /> </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 options={[ <Select allowClear>
{ value: "PAA", label: t("joblines.fields.part_types.PAA") }, <Select.Option value="PAA">{t("joblines.fields.part_types.PAA")}</Select.Option>
{ value: "PAC", label: t("joblines.fields.part_types.PAC") }, <Select.Option value="PAC">{t("joblines.fields.part_types.PAC")}</Select.Option>
{ value: "PAE", label: t("joblines.fields.part_types.PAE") }, <Select.Option value="PAE">{t("joblines.fields.part_types.PAE")}</Select.Option>
{ value: "PAL", label: t("joblines.fields.part_types.PAL") }, <Select.Option value="PAL">{t("joblines.fields.part_types.PAL")}</Select.Option>
{ value: "PAM", label: t("joblines.fields.part_types.PAM") }, <Select.Option value="PAM">{t("joblines.fields.part_types.PAM")}</Select.Option>
{ value: "PAN", label: t("joblines.fields.part_types.PAN") }, <Select.Option value="PAN">{t("joblines.fields.part_types.PAN")}</Select.Option>
{ value: "PAO", label: t("joblines.fields.part_types.PAO") }, <Select.Option value="PAO">{t("joblines.fields.part_types.PAO")}</Select.Option>
{ value: "PAR", label: t("joblines.fields.part_types.PAR") }, <Select.Option value="PAR">{t("joblines.fields.part_types.PAR")}</Select.Option>
{ value: "PAS", label: t("joblines.fields.part_types.PAS") } <Select.Option value="PAS">{t("joblines.fields.part_types.PAS")}</Select.Option>
]} /> </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,6 +1,5 @@
import { EditFilled } from "@ant-design/icons"; import { EditFilled } from "@ant-design/icons";
import { Button, Card, Space } from "antd"; import { Button, Card, Space, 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";
@@ -187,9 +186,8 @@ export function JobPayments({ job, bodyshop, setPaymentContext, setCardPaymentCo
</Space> </Space>
} }
> >
<ResponsiveTable <Table
columns={columns} columns={columns}
mobileColumnKeys={["date", "amount", "actions", "payer"]}
rowKey="id" rowKey="id"
pagination={false} pagination={false}
onChange={handleTableChange} onChange={handleTableChange}
@@ -201,18 +199,18 @@ export function JobPayments({ job, bodyshop, setPaymentContext, setCardPaymentCo
expandedRowRender: (record) => <PaymentExpandedRowComponent record={record} bodyshop={bodyshop} /> expandedRowRender: (record) => <PaymentExpandedRowComponent record={record} bodyshop={bodyshop} />
}} }}
summary={() => ( summary={() => (
<ResponsiveTable.Summary.Row> <Table.Summary.Row>
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>
<strong>{t("payments.labels.totalpayments")}</strong> <strong>{t("payments.labels.totalpayments")}</strong>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell /> <Table.Summary.Cell />
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>
<strong>{total.toFormat()}</strong> <strong>{total.toFormat()}</strong>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell /> <Table.Summary.Cell />
<ResponsiveTable.Summary.Cell /> <Table.Summary.Cell />
<ResponsiveTable.Summary.Cell /> <Table.Summary.Cell />
</ResponsiveTable.Summary.Row> </Table.Summary.Row>
)} )}
/> />
</Card> </Card>

View File

@@ -1,5 +1,4 @@
import { Checkbox, Typography } from "antd"; import { Checkbox, Table, 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";
@@ -80,12 +79,11 @@ 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>
<ResponsiveTable <Table
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,5 +1,4 @@
import { Typography } from "antd"; import { Table, 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";
@@ -97,10 +96,9 @@ 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>
<ResponsiveTable <Table
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,6 +8,8 @@ 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,
@@ -85,24 +87,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) => ({ >
key: o.id, {theOptions
value: o.id, ? theOptions.map((o) => (
status: o.status, <Option key={o.id} value={o.id} status={o.status}>
label: ( <Space align="center">
<Space align="center"> <span>
<span> {`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${o.ro_number || t("general.labels.na")} | ${OwnerNameDisplayFunction(
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${o.ro_number || t("general.labels.na")} | ${OwnerNameDisplayFunction( o
o )} | ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${o.v_model_desc || ""}`}
)} | ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${o.v_model_desc || ""}`} </span>
</span> <Tag>
<Tag> <strong>{o.status}</strong>
<strong>{o.status}</strong> </Tag>
</Tag> </Space>
</Space> </Option>
) ))
}))} : 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,5 +1,4 @@
import { Space } from "antd"; import { Space, 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";
@@ -105,9 +104,8 @@ export default function JobTotalsTableLabor({ job }) {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
return ( return (
<ResponsiveTable <Table
columns={columns} columns={columns}
mobileColumnKeys={["total", "profitcenter_labor", "rate", "mod_lb_hrs"]}
rowKey="id" rowKey="id"
pagination={false} pagination={false}
onChange={handleTableChange} onChange={handleTableChange}
@@ -117,29 +115,29 @@ export default function JobTotalsTableLabor({ job }) {
}} }}
summary={() => ( summary={() => (
<> <>
<ResponsiveTable.Summary.Row> <Table.Summary.Row>
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>
<strong>{t("jobs.labels.labor_rates_subtotal")}</strong> <strong>{t("jobs.labels.labor_rates_subtotal")}</strong>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell /> <Table.Summary.Cell />
<ResponsiveTable.Summary.Cell> <Table.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)}
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
{InstanceRenderManager({ {InstanceRenderManager({
imex: null, imex: null,
rome: ( rome: (
<> <>
<ResponsiveTable.Summary.Cell /> <Table.Summary.Cell />
<ResponsiveTable.Summary.Cell /> <Table.Summary.Cell />
</> </>
) )
})} })}
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}</strong> <strong>{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}</strong>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
</ResponsiveTable.Summary.Row> </Table.Summary.Row>
<ResponsiveTable.Summary.Row> <Table.Summary.Row>
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>
<Space> <Space>
{t("jobs.labels.mapa")} {t("jobs.labels.mapa")}
{InstanceRenderManager({ {InstanceRenderManager({
@@ -158,34 +156,34 @@ export default function JobTotalsTableLabor({ job }) {
}) })
})} })}
</Space> </Space>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
<CurrencyFormatter>{job.job_totals.rates.mapa.rate}</CurrencyFormatter> <CurrencyFormatter>{job.job_totals.rates.mapa.rate}</CurrencyFormatter>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell>{job.job_totals.rates.mapa.hours.toFixed(1)}</ResponsiveTable.Summary.Cell> <Table.Summary.Cell>{job.job_totals.rates.mapa.hours.toFixed(1)}</Table.Summary.Cell>
{InstanceRenderManager({ {InstanceRenderManager({
imex: ( imex: (
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.total).toFormat()} {Dinero(job.job_totals.rates.mapa.total).toFormat()}
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
), ),
rome: ( rome: (
<> <>
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.base).toFormat()} {Dinero(job.job_totals.rates.mapa.base).toFormat()}
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.adjustment).toFormat()} {Dinero(job.job_totals.rates.mapa.adjustment).toFormat()}
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.total).toFormat()} {Dinero(job.job_totals.rates.mapa.total).toFormat()}
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
</> </>
) )
})} })}
</ResponsiveTable.Summary.Row> </Table.Summary.Row>
<ResponsiveTable.Summary.Row> <Table.Summary.Row>
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>
<Space wrap> <Space wrap>
{t("jobs.labels.mash")} {t("jobs.labels.mash")}
{InstanceRenderManager({ {InstanceRenderManager({
@@ -204,51 +202,51 @@ export default function JobTotalsTableLabor({ job }) {
}) })
})} })}
</Space> </Space>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
<CurrencyFormatter>{job.job_totals.rates.mash.rate}</CurrencyFormatter> <CurrencyFormatter>{job.job_totals.rates.mash.rate}</CurrencyFormatter>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell>{job.job_totals.rates.mash.hours.toFixed(1)}</ResponsiveTable.Summary.Cell> <Table.Summary.Cell>{job.job_totals.rates.mash.hours.toFixed(1)}</Table.Summary.Cell>
{InstanceRenderManager({ {InstanceRenderManager({
imex: ( imex: (
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.total).toFormat()} {Dinero(job.job_totals.rates.mash.total).toFormat()}
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
), ),
rome: ( rome: (
<> <>
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.base).toFormat()} {Dinero(job.job_totals.rates.mash.base).toFormat()}
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.adjustment).toFormat()} {Dinero(job.job_totals.rates.mash.adjustment).toFormat()}
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.total).toFormat()} {Dinero(job.job_totals.rates.mash.total).toFormat()}
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
</> </>
) )
})} })}
</ResponsiveTable.Summary.Row> </Table.Summary.Row>
<ResponsiveTable.Summary.Row> <Table.Summary.Row>
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>
<strong>{t("jobs.labels.rates_subtotal")}</strong> <strong>{t("jobs.labels.rates_subtotal")}</strong>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell /> <Table.Summary.Cell />
<ResponsiveTable.Summary.Cell /> <Table.Summary.Cell />
{InstanceRenderManager({ {InstanceRenderManager({
imex: null, imex: null,
rome: ( rome: (
<> <>
<ResponsiveTable.Summary.Cell /> <Table.Summary.Cell />
<ResponsiveTable.Summary.Cell /> <Table.Summary.Cell />
</> </>
) )
})} })}
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.rates.subtotal).toFormat()}</strong> <strong>{Dinero(job.job_totals.rates.subtotal).toFormat()}</strong>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
</ResponsiveTable.Summary.Row> </Table.Summary.Row>
</> </>
)} )}
/> />

View File

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

View File

@@ -1,4 +1,4 @@
import ResponsiveTable from "../responsive-table/responsive-table.component"; import { Table } from "antd";
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,9 +64,8 @@ export default function JobTotalsTableParts({ job }) {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
return ( return (
<ResponsiveTable <Table
columns={columns} columns={columns}
mobileColumnKeys={["total", "id"]}
rowKey="id" rowKey="id"
pagination={false} pagination={false}
onChange={handleTableChange} onChange={handleTableChange}
@@ -76,38 +75,36 @@ export default function JobTotalsTableParts({ job }) {
}} }}
summary={() => ( summary={() => (
<> <>
<ResponsiveTable.Summary.Row> <Table.Summary.Row>
<ResponsiveTable.Summary.Cell>{t("jobs.labels.prt_dsmk_total")}</ResponsiveTable.Summary.Cell> <Table.Summary.Cell>{t("jobs.labels.prt_dsmk_total")}</Table.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
{Dinero(job.job_totals.parts.parts.prt_dsmk_total).toFormat()} {Dinero(job.job_totals.parts.parts.prt_dsmk_total).toFormat()}
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
</ResponsiveTable.Summary.Row> </Table.Summary.Row>
<ResponsiveTable.Summary.Row> <Table.Summary.Row>
<ResponsiveTable.Summary.Cell> <Table.Summary.Cell>
<strong>{t("jobs.labels.partstotal")}</strong> <strong>{t("jobs.labels.partstotal")}</strong>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right"> <Table.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.parts.parts.total).toFormat()}</strong> <strong>{Dinero(job.job_totals.parts.parts.total).toFormat()}</strong>
</ResponsiveTable.Summary.Cell> </Table.Summary.Cell>
</ResponsiveTable.Summary.Row> </Table.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 && (
<ResponsiveTable.Summary.Row> <Table.Summary.Row>
<ResponsiveTable.Summary.Cell colSpan={24}> <Table.Summary.Cell colSpan={24}>{t("jobs.labels.profileadjustments")}</Table.Summary.Cell>
{t("jobs.labels.profileadjustments")} </Table.Summary.Row>
</ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
) )
} }
{insuranceAdjustments.map((adj, idx) => ( {insuranceAdjustments.map((adj, idx) => (
<ResponsiveTable.Summary.Row key={idx}> <Table.Summary.Row key={idx}>
<ResponsiveTable.Summary.Cell>{t(`jobs.fields.${adj.id.toLowerCase()}`)}</ResponsiveTable.Summary.Cell> <Table.Summary.Cell>{t(`jobs.fields.${adj.id.toLowerCase()}`)}</Table.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right">{adj.amount.toFormat()}</ResponsiveTable.Summary.Cell> <Table.Summary.Cell align="right">{adj.amount.toFormat()}</Table.Summary.Cell>
</ResponsiveTable.Summary.Row> </Table.Summary.Row>
))} ))}
</> </>
)} )}

View File

@@ -1,4 +1,4 @@
import ResponsiveTable from "../responsive-table/responsive-table.component"; import { Table } from "antd";
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,9 +245,8 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
]; ];
return ( return (
<ResponsiveTable <Table
columns={columns} columns={columns}
mobileColumnKeys={["total", "key"]}
rowKey="key" rowKey="key"
showHeader={false} showHeader={false}
pagination={false} pagination={false}

View File

@@ -59,12 +59,13 @@ export function JobsAdminClass({ bodyshop, job }) {
} }
]} ]}
> >
<Select <Select>
options={bodyshop.md_classes.map((s) => ({ {bodyshop.md_classes.map((s) => (
value: s, <Select.Option key={s} value={s}>
label: s {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 } from "antd"; import { Button, Card, Input, Space, Table } 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,7 +9,6 @@ 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
@@ -142,11 +141,10 @@ export function JobsAvailableScan({ partnerVersion, refetch }) {
</Space> </Space>
} }
> >
<ResponsiveTable <Table
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 } from "antd"; import { Alert, Button, Card, Input, Space, Table } 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,7 +13,6 @@ 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
@@ -210,14 +209,7 @@ export function JobsAvailableComponent({ bodyshop, loading, data, refetch, addJo
</Space> </Space>
} }
> >
<ResponsiveTable <Table loading={loading} columns={columns} rowKey="id" dataSource={availableJobs} onChange={handleTableChange} />
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,11 +141,13 @@ 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) => ({ >
value: p.name, {bodyshop.md_responsibility_centers.profits.map((p) => (
label: p.name <Select.Option key={p.name} value={p.name}>
}))} {p.name}
/> </Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
</td> </td>
<td> <td>
@@ -169,11 +171,13 @@ 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) => ({ >
value: p.name, {bodyshop.md_responsibility_centers.profits.map((p) => (
label: p.name <Select.Option key={p.name} value={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, useMemo, useState } from "react"; import { useCallback, 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,17 +37,16 @@ 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 [earlyRoCreated, setEarlyRoCreated] = useState(!!job?.dms_id); const [earlyRoCreatedThisSession, setEarlyRoCreatedThisSession] = useState(false); // Track if created in THIS modal session
const [earlyRoCreatedThisSession, setEarlyRoCreatedThisSession] = useState(false);
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO); const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const notification = useNotification(); const notification = useNotification();
const allFormValues = Form.useWatch([], form); const allFormValues = Form.useWatch([], form);
const { socket } = useSocket(); const { socket } = useSocket(); // Extract socket from context
// Get Fortellis treatment for proper DMS mode detection
const { const {
treatments: { Fortellis } treatments: { Fortellis }
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
@@ -56,64 +55,16 @@ 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,
@@ -127,11 +78,13 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
}); });
if (values.ca_gst_registrant) { if (values.ca_gst_registrant) {
await axios.post("/job/totalsssu", { id: job.id }); await axios.post("/job/totalsssu", {
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")
}); });
@@ -144,20 +97,19 @@ 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); setEarlyRoCreated(true); // Mark early RO as created
setEarlyRoCreatedThisSession(true); setEarlyRoCreatedThisSession(true); // Mark as created in this session
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);
@@ -178,28 +130,29 @@ 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); setEarlyRoCreated(!!job?.dms_id); // Initialize state based on current job
setEarlyRoCreatedThisSession(false); setEarlyRoCreatedThisSession(false); // Reset session state when opening modal
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)} closable={!(earlyRoCreatedThisSession && !job.converted)} // Only restrict if created in THIS session
mask={{ closable: !(earlyRoCreatedThisSession && !job.converted) }} maskClosable={!(earlyRoCreatedThisSession && !job.converted)} // Only restrict if created in THIS session
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,
@@ -211,6 +164,7 @@ 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
@@ -227,78 +181,127 @@ 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={[{ required: true }]} rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
> >
<Select <Select showSearch>
showSearch={{ {bodyshop.md_ins_cos.map((s, i) => (
optionFilterProp:'label' <Select.Option key={i} value={s.name}>
}} {s.name}
options={insuranceOptions} </Select.Option>
/> ))}
</Select>
</Form.Item> </Form.Item>
{bodyshop.enforce_class && ( {bodyshop.enforce_class && (
<Form.Item name="class" label={t("jobs.fields.class")} rules={[{ required: bodyshop.enforce_class }]}> <Form.Item
<Select options={classOptions} /> name={"class"}
label={t("jobs.fields.class")}
rules={[
{
required: bodyshop.enforce_class
//message: t("general.validation.required"),
}
]}
>
<Select>
{bodyshop.md_classes.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item> </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={[{ required: bodyshop.enforce_referral }]} rules={[
{
required: bodyshop.enforce_referral
//message: t("general.validation.required"),
}
]}
> >
<Select options={referralOptions} /> <Select>
{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={[{ required: bodyshop.enforce_conversion_csr }]} rules={[
{
required: bodyshop.enforce_conversion_csr
//message: t("general.validation.required"),
}
]}
> >
<Select <Select
showSearch={{ showSearch={{
optionFilterProp: 'label', optionFilterProp: "children",
filterOption: (input, option) => filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}} }}
style={{ width: 200 }} style={{ width: 200 }}
>
options={csrOptions} {bodyshop.employees
/> .filter((emp) => emp.active)
.map((emp) => (
<Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
)} )}
{bodyshop.enforce_conversion_category && ( {bodyshop.enforce_conversion_category && (
<Form.Item name="category" label={t("jobs.fields.category")} rules={[{ required: bodyshop.enforce_conversion_category }]}> <Form.Item
<Select allowClear options={categoryOptions} /> name={"category"}
label={t("jobs.fields.category")}
rules={[
{
required: bodyshop.enforce_conversion_category
//message: t("general.validation.required"),
}
]}
>
<Select allowClear>
{bodyshop.md_categories.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item> </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>
@@ -313,7 +316,6 @@ 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 <Select onChange={handleInsCoChange}>
onChange={handleInsCoChange} {bodyshop.md_ins_cos.map((s) => (
options={bodyshop.md_ins_cos.map((s) => ({ <Select.Option key={s.name} value={s.name}>
value: s.name, {s.name}
label: 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 /> <Input />
@@ -192,12 +192,13 @@ 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>
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 /> <Input />
@@ -220,13 +221,10 @@ 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 <Select allowClear>
allowClear <Select.Option value="W">{t("jobs.labels.deductible.waived")}</Select.Option>
options={[ <Select.Option value="Y">{t("jobs.labels.deductible.stands")}</Select.Option>
{ value: "W", label: t("jobs.labels.deductible.waived") }, </Select>
{ 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,5 +1,4 @@
import { Card, Input } from "antd"; import { Card, Input, Table } 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";
@@ -96,12 +95,11 @@ export default function JobsCreateOwnerInfoSearchComponent({ loading, owners })
/> />
} }
> >
<ResponsiveTable <Table
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,6 +1,5 @@
import { PlusOutlined, SearchOutlined } from "@ant-design/icons"; import { PlusOutlined, SearchOutlined } from "@ant-design/icons";
import { Button, Input, Popover } from "antd"; import { Button, Input, Popover, 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 PredefinedVehicles from "./predefined-vehicles.js"; import PredefinedVehicles from "./predefined-vehicles.js";
@@ -23,9 +22,9 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
const popContent = () => ( const popContent = () => (
<div> <div>
<ResponsiveTable <Table
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={[
{ {
@@ -62,7 +61,6 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
) )
} }
]} ]}
mobileColumnKeys={["make", "model", "select"]}
/> />
</div> </div>
); );

View File

@@ -1,5 +1,4 @@
import { Card, Input, Space } from "antd"; import { Card, Input, Space, Table } 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";
@@ -69,12 +68,11 @@ export default function JobsCreateVehicleInfoSearchComponent({ loading, vehicles
</Space> </Space>
} }
> >
<ResponsiveTable <Table
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,19 +43,20 @@ 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} options={[ <Select disabled={jobRO}>
{ value: "W", label: t("jobs.labels.deductible.waived") }, <Select.Option value="W">{t("jobs.labels.deductible.waived")}</Select.Option>
{ value: "Y", label: t("jobs.labels.deductible.stands") } <Select.Option value="Y">{t("jobs.labels.deductible.stands")}</Select.Option>
]} /> </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} options={bodyshop.md_ded_notes.map((n) => ({ <Select disabled={jobRO}>
value: n, {bodyshop.md_ded_notes.map((n, index) => (
label: n <Select.Option key={index}>{n}</Select.Option>
}))} /> ))}
</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} />
@@ -65,10 +66,13 @@ 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} options={bodyshop.md_ins_cos.map((s) => ({ <Select disabled={jobRO} onChange={handleInsCoChange}>
value: s.name, {bodyshop.md_ins_cos.map((s) => (
label: s.name <Select.Option key={s.name} value={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} />
@@ -119,19 +123,25 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
} }
]} ]}
> >
<Select disabled={jobRO} allowClear options={bodyshop.md_referral_sources.map((s) => ({ <Select disabled={jobRO} allowClear>
value: s, {bodyshop.md_referral_sources.map((s) => (
label: 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 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 options={bodyshop.appt_alt_transport.map((s) => ({ <Select disabled={jobRO} allowClear>
value: s, {bodyshop.appt_alt_transport.map((s) => (
label: s <Select.Option key={s} value={s}>
}))} /> {s}
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
</FormRow> </FormRow>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
@@ -233,10 +243,13 @@ 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 options={bodyshop.md_categories.map((s) => ({ <Select disabled={jobRO} allowClear>
value: s, {bodyshop.md_categories.map((s) => (
label: s <Select.Option key={s} value={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,
mask: { closable: false }, maskClosable: false,
onCancel: () => { onCancel: () => {
closeConfirmById(id); closeConfirmById(id);
onCancel?.(); onCancel?.();
@@ -714,12 +714,13 @@ 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>
options={bodyshop.appt_colors.map((col) => ({ {bodyshop.appt_colors.map((col, idx) => (
value: col.color.hex, <Select.Option key={idx} value={col.color.hex}>
label: col.label {col.label}
}))} </Select.Option>
/> ))}
</Select>
</Form.Item> </Form.Item>
<Space wrap> <Space wrap>

View File

@@ -133,16 +133,14 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
</FormRow> </FormRow>
) )
})} })}
<Divider titlePlacement="left" orientation="horizontal" style={{ marginTop: ".8rem", float: "right" }}>
<FormRow {t("jobs.forms.laborrates")}
extra={ </Divider>
<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>

View File

@@ -1,6 +1,5 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { Button, Checkbox, Divider, Input, Space } from "antd"; import { Button, Checkbox, Divider, Input, Space, 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 { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -172,7 +171,7 @@ export default function JobsFindModalComponent({
return ( return (
<div> <div>
<ResponsiveTable <Table
title={() => ( title={() => (
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
{t("jobs.labels.existing_jobs")} {t("jobs.labels.existing_jobs")}
@@ -192,7 +191,6 @@ export default function JobsFindModalComponent({
)} )}
pagination={{ placement: "bottom" }} pagination={{ placement: "bottom" }}
columns={columns} columns={columns}
mobileColumnKeys={["ro_number", "owner", "status", "vehicle"]}
rowKey="id" rowKey="id"
loading={jobsListLoading} loading={jobsListLoading}
dataSource={jobsList} dataSource={jobsList}

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