Compare commits
147 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a182ea0869 | ||
|
|
d92bab113e | ||
|
|
93c6e2b601 | ||
|
|
19a90571f6 | ||
|
|
953e70efef | ||
|
|
a6bae390e5 | ||
|
|
cf9d8d649d | ||
|
|
a25051c4c2 | ||
|
|
d5c3152631 | ||
|
|
66c425bf96 | ||
|
|
ffad0dfbf7 | ||
|
|
17285fc029 | ||
|
|
401e3cff73 | ||
|
|
865680e019 | ||
|
|
9f97ca0336 | ||
|
|
5df38f8612 | ||
|
|
63c5719420 | ||
|
|
d6c80f1420 | ||
|
|
fade927c9e | ||
|
|
9f472ce1d0 | ||
|
|
47a56e32b9 | ||
|
|
f13f79acb6 | ||
|
|
bfa9fddb9e | ||
|
|
28abd9707e | ||
|
|
5f621e1ae0 | ||
|
|
624414799e | ||
|
|
72091e9eae | ||
|
|
9cfacdd025 | ||
|
|
d5c63b798a | ||
|
|
655e516246 | ||
|
|
7b12f0a3b9 | ||
|
|
e0b937474d | ||
|
|
5c4267f3ef | ||
|
|
4dcfb382a9 | ||
|
|
cf181dfd0a | ||
|
|
1127864ba9 | ||
|
|
79e379b61a | ||
|
|
e79e512291 | ||
|
|
f0064abfbe | ||
|
|
4a30a5bc64 | ||
|
|
32bdea559e | ||
|
|
d4215b7aee | ||
|
|
2494399993 | ||
|
|
34f62a8858 | ||
|
|
9e5689b06f | ||
|
|
5d69d37db2 | ||
|
|
9ab2fdc868 | ||
|
|
fbd6766dcd | ||
|
|
9ace531edb | ||
|
|
2e3944099b | ||
|
|
9b53bd9b40 | ||
|
|
443ed717cb | ||
|
|
9845c1cea5 | ||
|
|
2061a49e0e | ||
|
|
f8a3d0f854 | ||
|
|
23901c0cc1 | ||
|
|
b99a212d75 | ||
|
|
a4963922da | ||
|
|
3ae41b7016 | ||
|
|
9c59fd4c00 | ||
|
|
a9f959cced | ||
|
|
414897bba0 | ||
|
|
7467a31d76 | ||
|
|
894f6bf6d2 | ||
|
|
744dfa8163 | ||
|
|
2293119518 | ||
|
|
bd529a0dfa | ||
|
|
57ad89747f | ||
|
|
3ae8f38adb | ||
|
|
dc5ed1a39c | ||
|
|
aa6e6b8980 | ||
|
|
1dc80c068b | ||
|
|
bd0c4ceae2 | ||
|
|
30b58c6ea5 | ||
|
|
a55e9224f8 | ||
|
|
0c80abb3ca | ||
|
|
7137e611cd | ||
|
|
6f9d291d36 | ||
|
|
f2a2653eae | ||
|
|
73c25ab91f | ||
|
|
780449bac6 | ||
|
|
2509a1ecf3 | ||
|
|
16075f7ddd | ||
|
|
27d28e7ffc | ||
|
|
66b87e5c45 | ||
|
|
c1e1dff7d2 | ||
|
|
f76eb7abf5 | ||
|
|
25ea2a80a3 | ||
|
|
633d5668f0 | ||
|
|
00cc47553b | ||
|
|
3c360130a3 | ||
|
|
13e4143eeb | ||
|
|
68c7b184d2 | ||
|
|
9b85d15ff1 | ||
|
|
e7cf49a2ec | ||
|
|
04b29b6970 | ||
|
|
f5bc79cba7 | ||
|
|
2ae18681cb | ||
|
|
fda763476a | ||
|
|
999cbd80f4 | ||
|
|
ad2a5fe95b | ||
|
|
d835021069 | ||
|
|
c4b303aee1 | ||
|
|
e2c5a4cba4 | ||
|
|
fd04125ed1 | ||
|
|
a0566e76ab | ||
|
|
87e8b2ce27 | ||
|
|
5e24404e82 | ||
|
|
64a280b111 | ||
|
|
cf393e8f9e | ||
|
|
909a21023a | ||
|
|
0402156b4d | ||
|
|
94bdc6c43f | ||
|
|
9466d36e69 | ||
|
|
412efb06e5 | ||
|
|
da7e637183 | ||
|
|
2e95fa25af | ||
|
|
f6c63bbd74 | ||
|
|
0a654082c2 | ||
|
|
2c20b731d2 | ||
|
|
8a22897cdd | ||
|
|
677da61b52 | ||
|
|
6513434bd7 | ||
|
|
fe2600029f | ||
|
|
c5b4efedfb | ||
|
|
310321d0ab | ||
|
|
7e884c42ea | ||
|
|
e279bf41a4 | ||
|
|
4a060ab51c | ||
|
|
62c1c77a18 | ||
|
|
db19ecb28c | ||
|
|
51748ce28d | ||
|
|
4bbfd8a9da | ||
|
|
d4d2db2cac | ||
|
|
23483144e1 | ||
|
|
67d5dcb062 | ||
|
|
901a49e571 | ||
|
|
49ae107fde | ||
|
|
0135281bcd | ||
|
|
99cf95daf0 | ||
|
|
8c1758ae49 | ||
|
|
2d764921ff | ||
|
|
858a11f8b4 | ||
|
|
4859239f55 | ||
|
|
5c64d7185e | ||
|
|
152479bc08 | ||
|
|
3fe0e3a33c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -130,3 +130,5 @@ test-output.txt
|
|||||||
server/job/test/fixtures
|
server/job/test/fixtures
|
||||||
|
|
||||||
.github
|
.github
|
||||||
|
_reference/ragmate/.ragmate.env
|
||||||
|
docker_data
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
client_max_body_size 50M;
|
client_max_body_size 50M;
|
||||||
client_body_buffer_size 5M;
|
client_body_buffer_size 5M;
|
||||||
@@ -12791,27 +12791,6 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
<concept_node>
|
|
||||||
<name>allow_text_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>
|
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>checklist</name>
|
<name>checklist</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -42614,27 +42593,6 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
<concept_node>
|
|
||||||
<name>allow_text_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>
|
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>name</name>
|
<name>name</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
|
|||||||
548
client/package-lock.json
generated
548
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,18 +13,18 @@
|
|||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||||
"@firebase/analytics": "^0.10.16",
|
"@firebase/analytics": "^0.10.16",
|
||||||
"@firebase/app": "^0.13.0",
|
"@firebase/app": "^0.13.1",
|
||||||
"@firebase/auth": "^1.10.5",
|
"@firebase/auth": "^1.10.6",
|
||||||
"@firebase/firestore": "^4.7.15",
|
"@firebase/firestore": "^4.7.17",
|
||||||
"@firebase/messaging": "^0.12.21",
|
"@firebase/messaging": "^0.12.21",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.8.2",
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"@sentry/cli": "^2.45.0",
|
"@sentry/cli": "^2.47.1",
|
||||||
"@sentry/react": "^9.22.0",
|
"@sentry/react": "^9.38.0",
|
||||||
"@sentry/vite-plugin": "^3.5.0",
|
"@sentry/vite-plugin": "^3.5.0",
|
||||||
"@splitsoftware/splitio-react": "^2.1.1",
|
"@splitsoftware/splitio-react": "^2.3.1",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"antd": "^5.25.2",
|
"antd": "^5.25.4",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^4.3.0",
|
"apollo-link-sentry": "^4.3.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
@@ -41,24 +41,25 @@
|
|||||||
"i18next": "^24.2.3",
|
"i18next": "^24.2.3",
|
||||||
"i18next-browser-languagedetector": "^8.1.0",
|
"i18next-browser-languagedetector": "^8.1.0",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.12.8",
|
"libphonenumber-js": "^1.12.10",
|
||||||
"logrocket": "^9.0.2",
|
"logrocket": "^9.0.2",
|
||||||
"markerjs2": "^2.32.4",
|
"markerjs2": "^2.32.4",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-url": "^8.0.1",
|
"normalize-url": "^8.0.2",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
|
"phone": "^3.1.59",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.2.0",
|
"query-string": "^9.2.0",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.18.0",
|
"react-big-calendar": "^1.19.2",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^8.0.1",
|
"react-cookie": "^8.0.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-drag-listview": "^2.0.0",
|
"react-drag-listview": "^2.0.0",
|
||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "1.3.4",
|
"react-grid-layout": "1.3.4",
|
||||||
"react-i18next": "^15.4.1",
|
"react-i18next": "^15.5.2",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^6.30.0",
|
"react-router-dom": "^6.30.0",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtuoso": "^4.12.7",
|
"react-virtuoso": "^4.12.8",
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.89.0",
|
"sass": "^1.89.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.18",
|
"styled-components": "^6.1.18",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
@@ -130,17 +131,17 @@
|
|||||||
"@ant-design/icons": "^6.0.0",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.27.1",
|
"@babel/preset-react": "^7.27.1",
|
||||||
"@dotenvx/dotenvx": "^1.44.1",
|
"@dotenvx/dotenvx": "^1.47.5",
|
||||||
"@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.27.0",
|
"@eslint/js": "^9.31.0",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.54.1",
|
||||||
"@sentry/webpack-plugin": "^3.5.0",
|
"@sentry/webpack-plugin": "^3.5.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"browserslist": "^4.24.5",
|
"browserslist": "^4.25.0",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
@@ -150,7 +151,7 @@
|
|||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"memfs": "^4.17.2",
|
"memfs": "^4.17.2",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"playwright": "^1.51.1",
|
"playwright": "^1.54.1",
|
||||||
"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",
|
||||||
@@ -160,7 +161,7 @@
|
|||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
"vite-plugin-pwa": "^1.0.0",
|
"vite-plugin-pwa": "^1.0.0",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"vitest": "^3.1.4",
|
"vitest": "^3.2.3",
|
||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { useApolloClient, useMutation } from "@apollo/client";
|
|||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } 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 React, { useEffect, useMemo, 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 { 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";
|
||||||
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||||
@@ -24,7 +25,7 @@ import BillFormContainer from "../bill-form/bill-form.container";
|
|||||||
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||||
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
||||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { handleUpload as handleUploadToImageProxy } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
billEnterModal: selectBillEnterModal,
|
billEnterModal: selectBillEnterModal,
|
||||||
@@ -53,10 +54,10 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treatments: { Enhanced_Payroll }
|
treatments: { Enhanced_Payroll, Imgproxy }
|
||||||
} = useSplitTreatments({
|
} = useSplitTreatments({
|
||||||
attributes: {},
|
attributes: {},
|
||||||
names: ["Enhanced_Payroll"],
|
names: ["Enhanced_Payroll", "Imgproxy"],
|
||||||
splitKey: bodyshop.imexshopid
|
splitKey: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,7 +197,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
job: { lbr_adjustments: newAdjustments }
|
job: { lbr_adjustments: newAdjustments }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!!jobUpdate.errors) {
|
if (jobUpdate.errors) {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("jobs.errors.saving", {
|
message: t("jobs.errors.saving", {
|
||||||
message: JSON.stringify(jobUpdate.errors)
|
message: JSON.stringify(jobUpdate.errors)
|
||||||
@@ -213,7 +214,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
variables: { partsLineIds: markPolReceived.map((p) => p.id) },
|
variables: { partsLineIds: markPolReceived.map((p) => p.id) },
|
||||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"]
|
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"]
|
||||||
});
|
});
|
||||||
if (!!r2.errors) {
|
if (r2.errors) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setEnterAgain(false);
|
setEnterAgain(false);
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
@@ -224,7 +225,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!r1.errors) {
|
if (r1.errors) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setEnterAgain(false);
|
setEnterAgain(false);
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
@@ -244,7 +245,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
consumedbybillid: billId
|
consumedbybillid: billId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!!r2.errors) {
|
if (r2.errors) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setEnterAgain(false);
|
setEnterAgain(false);
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
@@ -298,20 +299,39 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
upload.forEach((u) => {
|
//Check if using Imgproxy or cloudinary
|
||||||
handleUpload(
|
|
||||||
{ file: u.originFileObj },
|
if (Imgproxy.treatment === "on") {
|
||||||
{
|
upload.forEach((u) => {
|
||||||
bodyshop: bodyshop,
|
handleUploadToImageProxy(
|
||||||
uploaded_by: currentUser.email,
|
{ file: u.originFileObj },
|
||||||
jobId: values.jobid,
|
{
|
||||||
billId: billId,
|
bodyshop: bodyshop,
|
||||||
tagsArray: null,
|
uploaded_by: currentUser.email,
|
||||||
callback: null
|
jobId: values.jobid,
|
||||||
},
|
billId: billId,
|
||||||
notification
|
tagsArray: null,
|
||||||
);
|
callback: null
|
||||||
});
|
},
|
||||||
|
notification
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
upload.forEach((u) => {
|
||||||
|
handleUpload(
|
||||||
|
{ file: u.originFileObj },
|
||||||
|
{
|
||||||
|
bodyshop: bodyshop,
|
||||||
|
uploaded_by: currentUser.email,
|
||||||
|
jobId: values.jobid,
|
||||||
|
billId: billId,
|
||||||
|
tagsArray: null,
|
||||||
|
callback: null
|
||||||
|
},
|
||||||
|
notification
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
///////////////////////////
|
///////////////////////////
|
||||||
@@ -396,7 +416,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
{t("bills.labels.generatepartslabel")}
|
{t("bills.labels.generatepartslabel")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
||||||
<Button loading={loading} onClick={() => form.submit()}>
|
<Button loading={loading} onClick={() => form.submit()} id="save-bill-enter-modal">
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
{billEnterModal.context && billEnterModal.context.id ? null : (
|
{billEnterModal.context && billEnterModal.context.id ? null : (
|
||||||
@@ -406,6 +426,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEnterAgain(true);
|
setEnterAgain(true);
|
||||||
}}
|
}}
|
||||||
|
id="save-and-new-bill-enter-modal"
|
||||||
>
|
>
|
||||||
{t("general.actions.saveandnew")}
|
{t("general.actions.saveandnew")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Select } from "antd";
|
import { Select } from "antd";
|
||||||
import React, { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
|||||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
|
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
|
||||||
label: (
|
label: (
|
||||||
<div style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
|
<div style={{ whiteSpace: "normal", wordBreak: "break-word" }}>
|
||||||
<span>
|
<span>
|
||||||
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
||||||
import React, { 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";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -209,6 +209,7 @@ export function BillsListTableComponent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
id="reconcile-bills-button"
|
||||||
>
|
>
|
||||||
<LockerWrapperComponent featureName="bills"> {t("jobs.actions.reconcile")}</LockerWrapperComponent>
|
<LockerWrapperComponent featureName="bills"> {t("jobs.actions.reconcile")}</LockerWrapperComponent>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Button, Form, InputNumber, Popover, Space } from "antd";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
|
||||||
export default function CABCpvrtCalculator({ disabled, form }) {
|
export default function CABCpvrtCalculator({ disabled, form }) {
|
||||||
const [visibility, setVisibility] = useState(false);
|
const [visibility, setVisibility] = useState(false);
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
|
<Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
|
||||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||||
<CalculatorFilled />
|
<CalculatorFilled />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -202,8 +202,6 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
text: message.text
|
text: message.text
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add cases for other known message types as needed
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Log a warning for unhandled message types
|
// Log a warning for unhandled message types
|
||||||
logLocal("handleMessageChanged - Unhandled message type", { type: message.type });
|
logLocal("handleMessageChanged - Unhandled message type", { type: message.type });
|
||||||
@@ -211,7 +209,7 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageRef; // Keep other messages unchanged
|
return messageRef;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,11 +243,8 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updatedList = existingList?.conversations
|
const updatedList = existingList?.conversations
|
||||||
? [
|
? [newConversation, ...existingList.conversations.filter((conv) => conv.id !== newConversation.id)]
|
||||||
newConversation,
|
: [newConversation]; // Prevent duplicates
|
||||||
...existingList.conversations.filter((conv) => conv.id !== newConversation.id) // Prevent duplicates
|
|
||||||
]
|
|
||||||
: [newConversation];
|
|
||||||
|
|
||||||
client.cache.writeQuery({
|
client.cache.writeQuery({
|
||||||
query: CONVERSATION_LIST_QUERY,
|
query: CONVERSATION_LIST_QUERY,
|
||||||
@@ -403,6 +398,7 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logLocal("handleConversationChanged - Unhandled type", { type });
|
logLocal("handleConversationChanged - Unhandled type", { type });
|
||||||
client.cache.modify({
|
client.cache.modify({
|
||||||
@@ -419,10 +415,95 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Existing handler for phone number opt-out
|
||||||
|
const handlePhoneNumberOptedOut = async (data) => {
|
||||||
|
const { bodyshopid, phone_number } = data;
|
||||||
|
logLocal("handlePhoneNumberOptedOut - Start", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.cache.modify({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fields: {
|
||||||
|
phone_number_opt_out(existing = [], { readField }) {
|
||||||
|
const phoneNumberExists = existing.some(
|
||||||
|
(ref) => readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid
|
||||||
|
);
|
||||||
|
|
||||||
|
if (phoneNumberExists) {
|
||||||
|
logLocal("handlePhoneNumberOptedOut - Phone number already in cache", { phone_number, bodyshopid });
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newOptOut = {
|
||||||
|
__typename: "phone_number_opt_out",
|
||||||
|
id: `temporary-${phone_number}-${Date.now()}`,
|
||||||
|
bodyshopid,
|
||||||
|
phone_number,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
return [...existing, newOptOut];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcast: true
|
||||||
|
});
|
||||||
|
|
||||||
|
client.cache.evict({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fieldName: "phone_number_opt_out",
|
||||||
|
args: { bodyshopid, search: phone_number }
|
||||||
|
});
|
||||||
|
client.cache.gc();
|
||||||
|
|
||||||
|
logLocal("handlePhoneNumberOptedOut - Cache updated successfully", data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating cache for phone number opt-out:", error);
|
||||||
|
logLocal("handlePhoneNumberOptedOut - Error", { error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// New handler for phone number opt-in
|
||||||
|
const handlePhoneNumberOptedIn = async (data) => {
|
||||||
|
const { bodyshopid, phone_number } = data;
|
||||||
|
logLocal("handlePhoneNumberOptedIn - Start", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update the Apollo cache for GET_PHONE_NUMBER_OPT_OUTS by removing the phone number
|
||||||
|
client.cache.modify({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fields: {
|
||||||
|
phone_number_opt_out(existing = [], { readField }) {
|
||||||
|
// Filter out the phone number from the opt-out list
|
||||||
|
return existing.filter(
|
||||||
|
(ref) => !(readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcast: true // Trigger UI updates
|
||||||
|
});
|
||||||
|
|
||||||
|
// Evict the cache entry to force a refetch on next query
|
||||||
|
client.cache.evict({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fieldName: "phone_number_opt_out",
|
||||||
|
args: { bodyshopid, search: phone_number }
|
||||||
|
});
|
||||||
|
client.cache.gc();
|
||||||
|
|
||||||
|
logLocal("handlePhoneNumberOptedIn - Cache updated successfully", data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating cache for phone number opt-in:", error);
|
||||||
|
logLocal("handlePhoneNumberOptedIn - Error", { error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
socket.on("new-message-summary", handleNewMessageSummary);
|
socket.on("new-message-summary", handleNewMessageSummary);
|
||||||
socket.on("new-message-detailed", handleNewMessageDetailed);
|
socket.on("new-message-detailed", handleNewMessageDetailed);
|
||||||
socket.on("message-changed", handleMessageChanged);
|
socket.on("message-changed", handleMessageChanged);
|
||||||
socket.on("conversation-changed", handleConversationChanged);
|
socket.on("conversation-changed", handleConversationChanged);
|
||||||
|
socket.on("phone-number-opted-out", handlePhoneNumberOptedOut);
|
||||||
|
socket.on("phone-number-opted-in", handlePhoneNumberOptedIn);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unregisterMessagingHandlers = ({ socket }) => {
|
export const unregisterMessagingHandlers = ({ socket }) => {
|
||||||
@@ -431,4 +512,6 @@ export const unregisterMessagingHandlers = ({ socket }) => {
|
|||||||
socket.off("new-message-detailed");
|
socket.off("new-message-detailed");
|
||||||
socket.off("message-changed");
|
socket.off("message-changed");
|
||||||
socket.off("conversation-changed");
|
socket.off("conversation-changed");
|
||||||
|
socket.off("phone-number-opted-out");
|
||||||
|
socket.off("phone-number-opted-in");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Badge, Card, List, Space, Tag } from "antd";
|
import { Badge, Card, List, Space, Tag, Tooltip } from "antd";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
@@ -9,6 +9,7 @@ import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
|||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||||
import "./chat-conversation-list.styles.scss";
|
import "./chat-conversation-list.styles.scss";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries.js";
|
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries.js";
|
||||||
@@ -88,7 +89,13 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
const cardExtra = (
|
const cardExtra = (
|
||||||
<>
|
<>
|
||||||
<Badge count={item.messages_aggregate.aggregate.count} />
|
<Badge count={item.messages_aggregate.aggregate.count} />
|
||||||
{hasOptOutEntry && <Tag color="red">{t("messaging.labels.no_consent")}</Tag>}
|
{hasOptOutEntry && (
|
||||||
|
<Tooltip title={t("consent.text_body")}>
|
||||||
|
<Tag color="red" icon={<ExclamationCircleOutlined />}>
|
||||||
|
{t("messaging.labels.no_consent")}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
|||||||
userid
|
userid
|
||||||
created_at
|
created_at
|
||||||
read
|
read
|
||||||
|
is_system
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
data: message
|
data: message
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-document
|
|||||||
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
|
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
|
||||||
import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
import "./chat-media-selector.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
});
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
||||||
|
|
||||||
export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
|
export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
|
||||||
@@ -37,9 +38,8 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
variables: {
|
variables: {
|
||||||
jobId: conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid
|
jobId: conversation.job_conversations[0]?.jobid
|
||||||
},
|
},
|
||||||
|
|
||||||
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
|
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,11 +56,11 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
//If Imageproxy is on, rely only on the LMS selector
|
//If Imageproxy is on, rely only on the LMS selector
|
||||||
//If not on, use the old methods.
|
//If not on, use the old methods.
|
||||||
const content = (
|
const content = (
|
||||||
<div>
|
<div className="media-selector-content">
|
||||||
{loading && <LoadingSpinner />}
|
{loading && <LoadingSpinner />}
|
||||||
{error && <AlertComponent message={error.message} type="error" />}
|
{error && <AlertComponent message={error.message} type="error" />}
|
||||||
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
||||||
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
<div className="error-message">{t("messaging.labels.maxtenimages")}</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{Imgproxy.treatment === "on" ? (
|
{Imgproxy.treatment === "on" ? (
|
||||||
@@ -74,7 +74,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
{bodyshop.uselocalmediaserver && open && (
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
<JobDocumentsLocalGalleryExternal
|
<JobDocumentsLocalGalleryExternal
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid}
|
jobId={conversation.job_conversations[0]?.jobid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -89,7 +89,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
{bodyshop.uselocalmediaserver && open && (
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
<JobDocumentsLocalGalleryExternal
|
<JobDocumentsLocalGalleryExternal
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid}
|
jobId={conversation.job_conversations[0]?.jobid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -100,12 +100,17 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
content={
|
content={
|
||||||
conversation.job_conversations.length === 0 ? <div>{t("messaging.errors.noattachedjobs")}</div> : content
|
conversation.job_conversations.length === 0 ? (
|
||||||
|
<div className="no-jobs-message">{t("messaging.errors.noattachedjobs")}</div>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)
|
||||||
}
|
}
|
||||||
title={t("messaging.labels.selectmedia")}
|
title={t("messaging.labels.selectmedia")}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={handleVisibleChange}
|
onOpenChange={handleVisibleChange}
|
||||||
|
classNames={{ root: "media-selector-popover" }}
|
||||||
>
|
>
|
||||||
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
||||||
<PictureFilled style={{ margin: "0 .5rem" }} />
|
<PictureFilled style={{ margin: "0 .5rem" }} />
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
.media-selector-popover {
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
position: relative;
|
||||||
|
max-width: 640px;
|
||||||
|
max-height: 480px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-selector-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: red;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-jobs-message {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style images within gallery components */
|
||||||
|
.media-selector-content img {
|
||||||
|
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid layout for gallery components */
|
||||||
|
.media-selector-content .ant-image, /* Assuming gallery components use Ant Design's Image */
|
||||||
|
.media-selector-content .gallery-container { /* Fallback for custom gallery classes */
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
@@ -4,13 +4,16 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.archive-button {
|
.archive-button {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-title {
|
.chat-title {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages {
|
.messages {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -37,11 +40,13 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.chat-send-message-button{
|
|
||||||
|
.chat-send-message-button {
|
||||||
margin: 0.3rem;
|
margin: 0.3rem;
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-icon {
|
.message-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0.1rem;
|
bottom: 0.1rem;
|
||||||
@@ -125,6 +130,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.system {
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.5rem 10%;
|
||||||
|
|
||||||
|
.message {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
color: #555;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.virtuoso-container {
|
.virtuoso-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@@ -2,17 +2,29 @@ import Icon from "@ant-design/icons";
|
|||||||
import { Tooltip } from "antd";
|
import { Tooltip } from "antd";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import { MdDone, MdDoneAll } from "react-icons/md";
|
import { MdClose, MdDone, MdDoneAll } from "react-icons/md";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
export const renderMessage = (messages, index) => {
|
export const renderMessage = (messages, index) => {
|
||||||
const message = messages[index];
|
const message = messages[index];
|
||||||
|
const isSystem = message.is_system;
|
||||||
|
|
||||||
|
// Determine message class
|
||||||
|
const messageClass = isSystem ? "system messages" : message.isoutbound ? "mine messages" : "yours messages";
|
||||||
|
|
||||||
|
// Tooltip content based on message type
|
||||||
|
const tooltipTitle = isSystem ? (
|
||||||
|
i18n.t("consent.text_body")
|
||||||
|
) : (
|
||||||
|
<DateTimeFormatter>{message.created_at}</DateTimeFormatter>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className={`${message.isoutbound ? "mine messages" : "yours messages"}`}>
|
<div key={index} className={messageClass}>
|
||||||
<div className="message msgmargin">
|
<div className="message msgmargin">
|
||||||
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
<Tooltip title={tooltipTitle}>
|
||||||
<div>
|
<div>
|
||||||
|
{isSystem && <span className="system-label">System</span>}
|
||||||
{/* Render images if available */}
|
{/* Render images if available */}
|
||||||
{message.image && message.image_path?.length > 0 && (
|
{message.image && message.image_path?.length > 0 && (
|
||||||
<div className="message-images">
|
<div className="message-images">
|
||||||
@@ -26,20 +38,31 @@ export const renderMessage = (messages, index) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Render text if available */}
|
{/* Render text if available */}
|
||||||
{message.text && <div>{message.text}</div>}
|
{message.text && <div className="message-text">{message.text}</div>}
|
||||||
|
{/* Render date for system messages */}
|
||||||
|
{isSystem && (
|
||||||
|
<div className="system-date">
|
||||||
|
<DateTimeFormatter>{message.created_at}</DateTimeFormatter>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Message status icons */}
|
{/* Message status icons for non-system messages */}
|
||||||
{message.status && (message.status === "sent" || message.status === "delivered") && (
|
{!isSystem &&
|
||||||
<div className="message-status">
|
message.status &&
|
||||||
<Icon component={message.status === "sent" ? MdDone : MdDoneAll} className="message-icon" />
|
(message.status === "sent" || message.status === "delivered" || message.status === "failed") && (
|
||||||
</div>
|
<div className="message-status">
|
||||||
)}
|
<Icon
|
||||||
|
component={message.status === "sent" ? MdDone : message.status === "delivered" ? MdDoneAll : MdClose}
|
||||||
|
className="message-icon"
|
||||||
|
style={message.status === "failed" ? { color: "#ff0000" } : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Outbound message metadata for non-system messages */}
|
||||||
{/* Outbound message metadata */}
|
{!isSystem && message.isoutbound && (
|
||||||
{message.isoutbound && (
|
|
||||||
<div style={{ fontSize: 10 }}>
|
<div style={{ fontSize: 10 }}>
|
||||||
{i18n.t("messaging.labels.sentby", {
|
{i18n.t("messaging.labels.sentby", {
|
||||||
by: message.userid,
|
by: message.userid,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
import { ExclamationCircleOutlined, LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
||||||
import { Alert, Input, Spin } from "antd";
|
import { Alert, Input, Space, Spin, Tooltip } from "antd";
|
||||||
import React, { 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";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -68,48 +68,67 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="imex-flex-row" style={{ width: "100%" }}>
|
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
||||||
{isOptedOut && <Alert message={t("messaging.errors.no_consent")} type="warning" style={{ marginBottom: 8 }} />}
|
{isOptedOut && (
|
||||||
<ChatPresetsComponent className="imex-flex-row__margin" />
|
<Tooltip title={t("consent.text_body")}>
|
||||||
<ChatMediaSelector
|
<Alert
|
||||||
conversation={conversation}
|
showIcon={true}
|
||||||
selectedMedia={selectedMedia}
|
icon={<ExclamationCircleOutlined />}
|
||||||
setSelectedMedia={setSelectedMedia}
|
message={t("messaging.errors.no_consent")}
|
||||||
/>
|
type="error"
|
||||||
<span style={{ flex: 1 }}>
|
|
||||||
<Input.TextArea
|
|
||||||
className="imex-flex-row__margin imex-flex-row__grow"
|
|
||||||
allowClear
|
|
||||||
autoFocus
|
|
||||||
ref={inputArea}
|
|
||||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
|
||||||
value={message}
|
|
||||||
disabled={isSending || isOptedOut}
|
|
||||||
placeholder={t("messaging.labels.typeamessage")}
|
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
onPressEnter={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (!event.shiftKey && !isOptedOut) handleEnter();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<SendOutlined
|
|
||||||
className="chat-send-message-button"
|
|
||||||
disabled={isOptedOut || message === "" || !message}
|
|
||||||
onClick={handleEnter}
|
|
||||||
/>
|
|
||||||
<Spin
|
|
||||||
style={{ display: `${isSending ? "" : "none"}` }}
|
|
||||||
indicator={
|
|
||||||
<LoadingOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: 24
|
|
||||||
}}
|
|
||||||
spin
|
|
||||||
/>
|
/>
|
||||||
}
|
</Tooltip>
|
||||||
/>
|
)}
|
||||||
</div>
|
<div className="imex-flex-row" style={{ width: "100%" }}>
|
||||||
|
{!isOptedOut && (
|
||||||
|
<>
|
||||||
|
<ChatPresetsComponent disabled={isSending} className="imex-flex-row__margin" />
|
||||||
|
<ChatMediaSelector
|
||||||
|
disabled={isSending}
|
||||||
|
conversation={conversation}
|
||||||
|
selectedMedia={selectedMedia}
|
||||||
|
setSelectedMedia={setSelectedMedia}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<span style={{ flex: 1 }}>
|
||||||
|
<Input.TextArea
|
||||||
|
className="imex-flex-row__margin imex-flex-row__grow"
|
||||||
|
allowClear
|
||||||
|
autoFocus
|
||||||
|
ref={inputArea}
|
||||||
|
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||||
|
value={message}
|
||||||
|
disabled={isSending || isOptedOut}
|
||||||
|
placeholder={t("messaging.labels.typeamessage")}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
onPressEnter={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!event.shiftKey && !isOptedOut) handleEnter();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{!isOptedOut && (
|
||||||
|
<SendOutlined
|
||||||
|
className="chat-send-message-button"
|
||||||
|
disabled={isSending || message === "" || !message}
|
||||||
|
onClick={handleEnter}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Spin
|
||||||
|
style={{ display: `${isSending ? "" : "none"}` }}
|
||||||
|
indicator={
|
||||||
|
<LoadingOutlined
|
||||||
|
style={{
|
||||||
|
fontSize: 24
|
||||||
|
}}
|
||||||
|
spin
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { forwardRef, useEffect, useState } from "react";
|
import { forwardRef, 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 { Option } = Select;
|
||||||
|
|
||||||
const ContractStatusComponent = ({ value, onChange }, ref) => {
|
const ContractStatusComponent = ({ value, onChange }) => {
|
||||||
const [option, setOption] = useState(value);
|
const [option, setOption] = useState(value);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Slider } from "antd";
|
import { Slider } from "antd";
|
||||||
import React, { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const CourtesyCarFuelComponent = (props, ref) => {
|
const CourtesyCarFuelComponent = (props, ref) => {
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
||||||
|
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { TimeFormatter } from "../../../utils/DateFormatter";
|
||||||
|
import { onlyUnique } from "../../../utils/arrayHelper";
|
||||||
|
import dayjs from "../../../utils/day";
|
||||||
|
import { alphaSort, dateSort } from "../../../utils/sorters";
|
||||||
|
import useLocalStorage from "../../../utils/useLocalStorage";
|
||||||
|
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||||
|
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../../owner-name-display/owner-name-display.component";
|
||||||
|
import DashboardRefreshRequired from "../refresh-required.component";
|
||||||
|
|
||||||
|
export default function DashboardScheduledDeliveryToday({ data, ...cardProps }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: {}
|
||||||
|
});
|
||||||
|
const [isTvModeScheduledDelivery, setIsTvModeScheduledDelivery] = useLocalStorage("isTvModeScheduledDelivery", false);
|
||||||
|
if (!data) return null;
|
||||||
|
if (!data.scheduled_delivery_today) return <DashboardRefreshRequired {...cardProps} />;
|
||||||
|
|
||||||
|
const scheduledDeliveryToday = data.scheduled_delivery_today.map((item) => {
|
||||||
|
const joblines_body = item.joblines
|
||||||
|
? item.joblines.filter((l) => l.mod_lbr_ty !== "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0)
|
||||||
|
: 0;
|
||||||
|
const joblines_ref = item.joblines
|
||||||
|
? item.joblines.filter((l) => l.mod_lbr_ty === "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0)
|
||||||
|
: 0;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
joblines_body,
|
||||||
|
joblines_ref
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const tvFontSize = 18;
|
||||||
|
const tvFontWeight = "bold";
|
||||||
|
|
||||||
|
const tvColumns = [
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.scheduled_delivery"),
|
||||||
|
dataIndex: "scheduled_delivery",
|
||||||
|
key: "scheduled_delivery",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => dateSort(a.scheduled_delivery, b.scheduled_delivery),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "scheduled_delivery" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||||
|
<TimeFormatter>{record.scheduled_delivery}</TimeFormatter>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ro_number"),
|
||||||
|
dataIndex: "ro_number",
|
||||||
|
key: "ro_number",
|
||||||
|
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<Link to={"/manage/jobs/" + record.jobid} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Space>
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||||
|
{record.ro_number || t("general.labels.na")}
|
||||||
|
{record.production_vars && record.production_vars.alert ? (
|
||||||
|
<ExclamationCircleFilled className="production-alert" />
|
||||||
|
) : null}
|
||||||
|
{record.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||||
|
{record.iouparent && (
|
||||||
|
<Tooltip title={t("jobs.labels.iou")}>
|
||||||
|
<BranchesOutlined style={{ color: "orangered" }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.owner"),
|
||||||
|
dataIndex: "owner",
|
||||||
|
key: "owner",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.ownerid ? (
|
||||||
|
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||||
|
<OwnerNameDisplay ownerObject={record} />
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||||
|
<OwnerNameDisplay ownerObject={record} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.vehicle"),
|
||||||
|
dataIndex: "vehicle",
|
||||||
|
key: "vehicle",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) =>
|
||||||
|
alphaSort(
|
||||||
|
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`,
|
||||||
|
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||||
|
),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.vehicleid ? (
|
||||||
|
<Link to={"/manage/vehicles/" + record.vehicleid} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||||
|
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
|
||||||
|
record.v_model_yr || ""
|
||||||
|
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("appointments.fields.alt_transport"),
|
||||||
|
dataIndex: "alt_transport",
|
||||||
|
key: "alt_transport",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order,
|
||||||
|
filters:
|
||||||
|
(scheduledDeliveryToday &&
|
||||||
|
scheduledDeliveryToday
|
||||||
|
.map((j) => j.alt_transport)
|
||||||
|
.filter(onlyUnique)
|
||||||
|
.map((s) => {
|
||||||
|
return {
|
||||||
|
text: s || t("dashboard.errors.atp"),
|
||||||
|
value: [s]
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||||
|
[],
|
||||||
|
onFilter: (value, record) => value.includes(record.alt_transport),
|
||||||
|
render: (text, record) => (
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.alt_transport}</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.status"),
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
|
filters:
|
||||||
|
(scheduledDeliveryToday &&
|
||||||
|
scheduledDeliveryToday
|
||||||
|
.map((j) => j.status)
|
||||||
|
.filter(onlyUnique)
|
||||||
|
.map((s) => {
|
||||||
|
return {
|
||||||
|
text: s || t("dashboard.errors.status"),
|
||||||
|
value: [s]
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||||
|
[],
|
||||||
|
onFilter: (value, record) => value.includes(record.status),
|
||||||
|
render: (text, record) => <span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.status}</span>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.lab"),
|
||||||
|
dataIndex: "joblines_body",
|
||||||
|
key: "joblines_body",
|
||||||
|
sorter: (a, b) => a.joblines_body - b.joblines_body,
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "joblines_body" && state.sortedInfo.order,
|
||||||
|
align: "right",
|
||||||
|
render: (text, record) => (
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.joblines_body.toFixed(1)}</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.lar"),
|
||||||
|
dataIndex: "joblines_ref",
|
||||||
|
key: "joblines_ref",
|
||||||
|
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
|
||||||
|
align: "right",
|
||||||
|
render: (text, record) => (
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.joblines_ref.toFixed(1)}</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.scheduled_delivery"),
|
||||||
|
dataIndex: "scheduled_delivery",
|
||||||
|
key: "scheduled_delivery",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => dateSort(a.scheduled_delivery, b.scheduled_delivery),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "scheduled_delivery" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => <TimeFormatter>{record.scheduled_delivery}</TimeFormatter>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ro_number"),
|
||||||
|
dataIndex: "ro_number",
|
||||||
|
key: "ro_number",
|
||||||
|
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<Link to={"/manage/jobs/" + record.jobid} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Space>
|
||||||
|
{record.ro_number || t("general.labels.na")}
|
||||||
|
{record.production_vars && record.production_vars.alert ? (
|
||||||
|
<ExclamationCircleFilled className="production-alert" />
|
||||||
|
) : null}
|
||||||
|
{record.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||||
|
{record.iouparent && (
|
||||||
|
<Tooltip title={t("jobs.labels.iou")}>
|
||||||
|
<BranchesOutlined style={{ color: "orangered" }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.owner"),
|
||||||
|
dataIndex: "owner",
|
||||||
|
key: "owner",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.ownerid ? (
|
||||||
|
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<OwnerNameDisplay ownerObject={record} />
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
<OwnerNameDisplay ownerObject={record} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("dashboard.labels.phone"),
|
||||||
|
dataIndex: "ownr_ph",
|
||||||
|
key: "ownr_ph",
|
||||||
|
ellipsis: true,
|
||||||
|
responsive: ["md"],
|
||||||
|
render: (text, record) => (
|
||||||
|
<Space size="small" wrap>
|
||||||
|
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||||
|
|
||||||
|
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ownr_ea"),
|
||||||
|
dataIndex: "ownr_ea",
|
||||||
|
key: "ownr_ea",
|
||||||
|
ellipsis: true,
|
||||||
|
responsive: ["md"],
|
||||||
|
render: (text, record) => <a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.vehicle"),
|
||||||
|
dataIndex: "vehicle",
|
||||||
|
key: "vehicle",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) =>
|
||||||
|
alphaSort(
|
||||||
|
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`,
|
||||||
|
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||||
|
),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.vehicleid ? (
|
||||||
|
<Link to={"/manage/vehicles/" + record.vehicleid} onClick={(e) => e.stopPropagation()}>
|
||||||
|
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ins_co_nm"),
|
||||||
|
dataIndex: "ins_co_nm",
|
||||||
|
key: "ins_co_nm",
|
||||||
|
ellipsis: true,
|
||||||
|
responsive: ["md"],
|
||||||
|
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
|
||||||
|
filters:
|
||||||
|
(scheduledDeliveryToday &&
|
||||||
|
scheduledDeliveryToday
|
||||||
|
.map((j) => j.ins_co_nm)
|
||||||
|
.filter(onlyUnique)
|
||||||
|
.map((s) => {
|
||||||
|
return {
|
||||||
|
text: s || t("dashboard.errors.insco"),
|
||||||
|
value: [s]
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||||
|
[],
|
||||||
|
onFilter: (value, record) => value.includes(record.ins_co_nm)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("appointments.fields.alt_transport"),
|
||||||
|
dataIndex: "alt_transport",
|
||||||
|
key: "alt_transport",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order,
|
||||||
|
filters:
|
||||||
|
(scheduledDeliveryToday &&
|
||||||
|
scheduledDeliveryToday
|
||||||
|
.map((j) => j.alt_transport)
|
||||||
|
.filter(onlyUnique)
|
||||||
|
.map((s) => {
|
||||||
|
return {
|
||||||
|
text: s || t("dashboard.errors.atp"),
|
||||||
|
value: [s]
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||||
|
[],
|
||||||
|
onFilter: (value, record) => value.includes(record.alt_transport)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={t("dashboard.titles.scheduleddeliverydate", {
|
||||||
|
date: dayjs().startOf("day").format("MM/DD/YYYY")
|
||||||
|
})}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
onClick={() => setIsTvModeScheduledDelivery(!isTvModeScheduledDelivery)}
|
||||||
|
defaultChecked={isTvModeScheduledDelivery}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
{...cardProps}
|
||||||
|
>
|
||||||
|
<div style={{ height: "100%" }}>
|
||||||
|
<Table
|
||||||
|
onChange={handleTableChange}
|
||||||
|
pagination={false}
|
||||||
|
columns={isTvModeScheduledDelivery ? tvColumns : columns}
|
||||||
|
scroll={{ x: true, y: "calc(100% - 2em)" }}
|
||||||
|
rowKey="id"
|
||||||
|
style={{ height: "85%" }}
|
||||||
|
dataSource={scheduledDeliveryToday}
|
||||||
|
size={isTvModeScheduledDelivery ? "small" : "middle"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DashboardScheduledDeliveryTodayGql = `
|
||||||
|
scheduled_delivery_today: jobs(where: {
|
||||||
|
date_invoiced: {_is_null: true},
|
||||||
|
ro_number: {_is_null: false},
|
||||||
|
voided: {_eq: false},
|
||||||
|
scheduled_delivery: {_gte: "${dayjs().startOf("day").toISOString()}",
|
||||||
|
_lte: "${dayjs().endOf("day").toISOString()}"}}) {
|
||||||
|
alt_transport
|
||||||
|
clm_no
|
||||||
|
jobid: id
|
||||||
|
joblines(where: {removed: {_eq: false}}) {
|
||||||
|
mod_lb_hrs
|
||||||
|
mod_lbr_ty
|
||||||
|
}
|
||||||
|
ins_co_nm
|
||||||
|
iouparent
|
||||||
|
ownerid
|
||||||
|
ownr_co_nm
|
||||||
|
ownr_ea
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_ph1
|
||||||
|
ownr_ph2
|
||||||
|
production_vars
|
||||||
|
ro_number
|
||||||
|
scheduled_delivery
|
||||||
|
status
|
||||||
|
suspended
|
||||||
|
v_make_desc
|
||||||
|
v_model_desc
|
||||||
|
v_model_yr
|
||||||
|
v_vin
|
||||||
|
vehicleid
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
||||||
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||||
import dayjs from "../../../utils/day";
|
import { useState } from "react";
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { TimeFormatter } from "../../../utils/DateFormatter";
|
import { TimeFormatter } from "../../../utils/DateFormatter";
|
||||||
import { onlyUnique } from "../../../utils/arrayHelper";
|
import { onlyUnique } from "../../../utils/arrayHelper";
|
||||||
|
import dayjs from "../../../utils/day";
|
||||||
import { alphaSort, dateSort } from "../../../utils/sorters";
|
import { alphaSort, dateSort } from "../../../utils/sorters";
|
||||||
import useLocalStorage from "../../../utils/useLocalStorage";
|
import useLocalStorage from "../../../utils/useLocalStorage";
|
||||||
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||||
@@ -169,7 +169,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Alt. Transport",
|
text: s || t("dashboard.errors.atp"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -313,7 +313,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Ins. Co.*",
|
text: s || t("dashboard.errors.insco"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -335,7 +335,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Alt. Transport",
|
text: s || t("dashboard.errors.atp"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
||||||
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||||
import dayjs from "../../../utils/day";
|
import { useState } from "react";
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { TimeFormatter } from "../../../utils/DateFormatter";
|
import { TimeFormatter } from "../../../utils/DateFormatter";
|
||||||
import { onlyUnique } from "../../../utils/arrayHelper";
|
import { onlyUnique } from "../../../utils/arrayHelper";
|
||||||
|
import dayjs from "../../../utils/day";
|
||||||
import { alphaSort, dateSort } from "../../../utils/sorters";
|
import { alphaSort, dateSort } from "../../../utils/sorters";
|
||||||
import useLocalStorage from "../../../utils/useLocalStorage";
|
import useLocalStorage from "../../../utils/useLocalStorage";
|
||||||
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||||
@@ -138,7 +138,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Alt. Transport*",
|
text: s || t("dashboard.errors.atp"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -154,7 +154,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
|||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
key: "status",
|
key: "status",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
filters:
|
filters:
|
||||||
(scheduledOutToday &&
|
(scheduledOutToday &&
|
||||||
@@ -163,7 +163,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Status*",
|
text: s || t("dashboard.errors.status"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -306,7 +306,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Ins. Co.*",
|
text: s || t("dashboard.errors.insco"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -328,7 +328,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Alt. Transport*",
|
text: s || t("dashboard.errors.atp"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,30 +1,33 @@
|
|||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component.jsx";
|
import JobLifecycleDashboardComponent, {
|
||||||
import {
|
JobLifecycleDashboardGQL
|
||||||
DashboardTotalProductionHours,
|
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx";
|
||||||
DashboardTotalProductionHoursGql
|
|
||||||
} from "../dashboard-components/total-production-hours/total-production-hours.component.jsx";
|
|
||||||
import DashboardProjectedMonthlySales, {
|
|
||||||
DashboardProjectedMonthlySalesGql
|
|
||||||
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx";
|
|
||||||
import DashboardMonthlyRevenueGraph, {
|
|
||||||
DashboardMonthlyRevenueGraphGql
|
|
||||||
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx";
|
|
||||||
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx";
|
|
||||||
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx";
|
|
||||||
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx";
|
|
||||||
import DashboardMonthlyEmployeeEfficiency, {
|
import DashboardMonthlyEmployeeEfficiency, {
|
||||||
DashboardMonthlyEmployeeEfficiencyGql
|
DashboardMonthlyEmployeeEfficiencyGql
|
||||||
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component.jsx";
|
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component.jsx";
|
||||||
|
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx";
|
||||||
|
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx";
|
||||||
|
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx";
|
||||||
|
import DashboardMonthlyRevenueGraph, {
|
||||||
|
DashboardMonthlyRevenueGraphGql
|
||||||
|
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx";
|
||||||
|
import DashboardProjectedMonthlySales, {
|
||||||
|
DashboardProjectedMonthlySalesGql
|
||||||
|
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx";
|
||||||
|
import DashboardScheduledDeliveryToday, {
|
||||||
|
DashboardScheduledDeliveryTodayGql
|
||||||
|
} from "../dashboard-components/scheduled-delivery-today/scheduled-delivery-today.component.jsx";
|
||||||
import DashboardScheduledInToday, {
|
import DashboardScheduledInToday, {
|
||||||
DashboardScheduledInTodayGql
|
DashboardScheduledInTodayGql
|
||||||
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx";
|
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx";
|
||||||
import DashboardScheduledOutToday, {
|
import DashboardScheduledOutToday, {
|
||||||
DashboardScheduledOutTodayGql
|
DashboardScheduledOutTodayGql
|
||||||
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx";
|
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx";
|
||||||
import JobLifecycleDashboardComponent, {
|
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component.jsx";
|
||||||
JobLifecycleDashboardGQL
|
import {
|
||||||
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx";
|
DashboardTotalProductionHours,
|
||||||
|
DashboardTotalProductionHoursGql
|
||||||
|
} from "../dashboard-components/total-production-hours/total-production-hours.component.jsx";
|
||||||
|
|
||||||
const componentList = {
|
const componentList = {
|
||||||
ProductionDollars: {
|
ProductionDollars: {
|
||||||
@@ -118,6 +121,15 @@ const componentList = {
|
|||||||
w: 10,
|
w: 10,
|
||||||
h: 3
|
h: 3
|
||||||
},
|
},
|
||||||
|
ScheduleDeliveryToday: {
|
||||||
|
label: i18next.t("dashboard.titles.scheduleddeliverytoday"),
|
||||||
|
component: DashboardScheduledDeliveryToday,
|
||||||
|
gqlFragment: DashboardScheduledDeliveryTodayGql,
|
||||||
|
minW: 6,
|
||||||
|
minH: 2,
|
||||||
|
w: 10,
|
||||||
|
h: 3
|
||||||
|
},
|
||||||
JobLifecycle: {
|
JobLifecycle: {
|
||||||
label: i18next.t("dashboard.titles.joblifecycle"),
|
label: i18next.t("dashboard.titles.joblifecycle"),
|
||||||
component: JobLifecycleDashboardComponent,
|
component: JobLifecycleDashboardComponent,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
Typography
|
Typography
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import React 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";
|
||||||
@@ -24,14 +23,14 @@ import i18n from "../../translations/i18n";
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
|
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
|
||||||
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
|
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
|
||||||
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
|
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
|
||||||
@@ -93,7 +92,9 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
|||||||
})
|
})
|
||||||
: ""
|
: ""
|
||||||
}`.slice(0, 239),
|
}`.slice(0, 239),
|
||||||
inservicedate: dayjs("2019-01-01")
|
inservicedate: dayjs(
|
||||||
|
`${(job.v_model_yr && (job.v_model_yr < 100 ? (job.v_model_yr >= (dayjs().year() + 1) % 100 ? 1900 + parseInt(job.v_model_yr) : 2000 + parseInt(job.v_model_yr)) : job.v_model_yr)) || 2019}-01-01`
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LayoutFormRow grow>
|
<LayoutFormRow grow>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DatePicker, Space, TimePicker } from "antd";
|
import { DatePicker, Space, TimePicker } from "antd";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import React, { useCallback, 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";
|
||||||
@@ -94,7 +94,24 @@ const DateTimePicker = ({
|
|||||||
showTime={false}
|
showTime={false}
|
||||||
format="MM/DD/YYYY"
|
format="MM/DD/YYYY"
|
||||||
value={value ? dayjs(value) : null}
|
value={value ? dayjs(value) : null}
|
||||||
onChange={handleChange}
|
onChange={(dateValue) => {
|
||||||
|
if (dateValue) {
|
||||||
|
// When date changes, preserve the existing time if it exists
|
||||||
|
if (value && dayjs(value).isValid()) {
|
||||||
|
const existingTime = dayjs(value);
|
||||||
|
const newDateTime = dayjs(dateValue)
|
||||||
|
.hour(existingTime.hour())
|
||||||
|
.minute(existingTime.minute())
|
||||||
|
.second(existingTime.second());
|
||||||
|
handleChange(newDateTime);
|
||||||
|
} else {
|
||||||
|
// If no existing time, just set the date without time
|
||||||
|
handleChange(dateValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleChange(dateValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder={t("general.labels.date")}
|
placeholder={t("general.labels.date")}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
disabledDate={handleDisabledDate}
|
disabledDate={handleDisabledDate}
|
||||||
@@ -105,13 +122,25 @@ const DateTimePicker = ({
|
|||||||
<TimePicker
|
<TimePicker
|
||||||
format="hh:mm a"
|
format="hh:mm a"
|
||||||
minuteStep={15}
|
minuteStep={15}
|
||||||
|
value={value && dayjs(value).hour() === 0 && dayjs(value).minute() === 0 ? null : dayjs(value)}
|
||||||
defaultOpenValue={dayjs(value)
|
defaultOpenValue={dayjs(value)
|
||||||
.hour(dayjs().hour())
|
.hour(dayjs().hour())
|
||||||
.minute(Math.floor(dayjs().minute() / 15) * 15)
|
.minute(Math.floor(dayjs().minute() / 15) * 15)
|
||||||
.second(0)}
|
.second(0)}
|
||||||
onChange={(value) => {
|
onChange={(timeValue) => {
|
||||||
handleChange(value);
|
if (timeValue) {
|
||||||
onBlur();
|
// When time changes, combine it with the existing date
|
||||||
|
const existingDate = dayjs(value);
|
||||||
|
const newDateTime = existingDate
|
||||||
|
.hour(timeValue.hour())
|
||||||
|
.minute(timeValue.minute())
|
||||||
|
.second(0);
|
||||||
|
handleChange(newDateTime);
|
||||||
|
} else {
|
||||||
|
// If time is cleared, just update with null time but keep date
|
||||||
|
handleChange(timeValue);
|
||||||
|
}
|
||||||
|
if (onBlur) onBlur();
|
||||||
}}
|
}}
|
||||||
placeholder={t("general.labels.time")}
|
placeholder={t("general.labels.time")}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|||||||
190
client/src/components/header/buildAccountingChildren.jsx
Normal file
190
client/src/components/header/buildAccountingChildren.jsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { FaCreditCard, FaFileInvoiceDollar } from "react-icons/fa";
|
||||||
|
import { GiPayMoney, GiPlayerTime } from "react-icons/gi";
|
||||||
|
import { BankFilled, ExportOutlined, FieldTimeOutlined } from "@ant-design/icons";
|
||||||
|
import LockWrapper from "../../components/lock-wrapper/lock-wrapper.component.jsx";
|
||||||
|
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
||||||
|
|
||||||
|
// --- Menu Item Builders ---
|
||||||
|
const buildAccountingChildren = ({
|
||||||
|
t,
|
||||||
|
bodyshop,
|
||||||
|
currentUser,
|
||||||
|
setBillEnterContext,
|
||||||
|
setPaymentContext,
|
||||||
|
setCardPaymentContext,
|
||||||
|
setTimeTicketContext,
|
||||||
|
ImEXPay,
|
||||||
|
DmsAp,
|
||||||
|
Simple_Inventory
|
||||||
|
}) => [
|
||||||
|
{
|
||||||
|
key: "bills",
|
||||||
|
id: "header-accounting-bills",
|
||||||
|
icon: <FaFileInvoiceDollar />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/bills">
|
||||||
|
<LockWrapper featureName="bills" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.bills")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "enterbills",
|
||||||
|
id: "header-accounting-enterbills",
|
||||||
|
icon: <GiPayMoney />,
|
||||||
|
label: (
|
||||||
|
<LockWrapper featureName="bills" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.enterbills")}
|
||||||
|
</LockWrapper>
|
||||||
|
),
|
||||||
|
onClick: () =>
|
||||||
|
HasFeatureAccess({ featureName: "bills", bodyshop }) && setBillEnterContext({ actions: {}, context: {} })
|
||||||
|
},
|
||||||
|
...(Simple_Inventory.treatment === "on"
|
||||||
|
? [
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "inventory",
|
||||||
|
id: "header-accounting-inventory",
|
||||||
|
icon: <FaFileInvoiceDollar />,
|
||||||
|
label: <Link to="/manage/inventory">{t("menus.header.inventory")}</Link>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "allpayments",
|
||||||
|
id: "header-accounting-allpayments",
|
||||||
|
icon: <BankFilled />,
|
||||||
|
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "enterpayments",
|
||||||
|
id: "header-accounting-enterpayments",
|
||||||
|
icon: <FaCreditCard />,
|
||||||
|
label: t("menus.header.enterpayment"),
|
||||||
|
onClick: () => setPaymentContext({ actions: {}, context: null })
|
||||||
|
},
|
||||||
|
...(ImEXPay.treatment === "on"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "entercardpayments",
|
||||||
|
id: "header-accounting-entercardpayments",
|
||||||
|
icon: <FaCreditCard />,
|
||||||
|
label: t("menus.header.entercardpayment"),
|
||||||
|
onClick: () => setCardPaymentContext({ actions: {}, context: {} })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "timetickets",
|
||||||
|
id: "header-accounting-timetickets",
|
||||||
|
icon: <FieldTimeOutlined />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/timetickets">
|
||||||
|
<LockWrapper featureName="timetickets" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.timetickets")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
...(bodyshop?.md_tasks_presets?.use_approvals
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "ttapprovals",
|
||||||
|
id: "header-accounting-ttapprovals",
|
||||||
|
icon: <FieldTimeOutlined />,
|
||||||
|
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
key: "entertimetickets",
|
||||||
|
id: "header-accounting-entertimetickets",
|
||||||
|
icon: <GiPlayerTime />,
|
||||||
|
label: (
|
||||||
|
<LockWrapper featureName="timetickets" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.entertimeticket")}
|
||||||
|
</LockWrapper>
|
||||||
|
),
|
||||||
|
onClick: () =>
|
||||||
|
HasFeatureAccess({ featureName: "timetickets", bodyshop }) &&
|
||||||
|
setTimeTicketContext({
|
||||||
|
actions: {},
|
||||||
|
context: {
|
||||||
|
created_by: currentUser.displayName ? `${currentUser.email} | ${currentUser.displayName}` : currentUser.email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "accountingexport",
|
||||||
|
id: "header-accounting-export",
|
||||||
|
icon: <ExportOutlined />,
|
||||||
|
label: (
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.export")}
|
||||||
|
</LockWrapper>
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "receivables",
|
||||||
|
id: "header-accounting-receivables",
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/accounting/receivables">
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.accounting-receivables")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "payables",
|
||||||
|
id: "header-accounting-payables",
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/accounting/payables">
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.accounting-payables")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "payments",
|
||||||
|
id: "header-accounting-payments",
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/accounting/payments">
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.accounting-payments")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "exportlogs",
|
||||||
|
id: "header-accounting-exportlogs",
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/accounting/exportlogs">
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.export-logs")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default buildAccountingChildren;
|
||||||
390
client/src/components/header/buildLeftMenuItems.jsx
Normal file
390
client/src/components/header/buildLeftMenuItems.jsx
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
BarChartOutlined,
|
||||||
|
CarFilled,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
ClockCircleFilled,
|
||||||
|
DashboardFilled,
|
||||||
|
DollarCircleFilled,
|
||||||
|
FileAddFilled,
|
||||||
|
FileAddOutlined,
|
||||||
|
FileFilled,
|
||||||
|
HomeFilled,
|
||||||
|
ImportOutlined,
|
||||||
|
LineChartOutlined,
|
||||||
|
OneToOneOutlined,
|
||||||
|
PaperClipOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
PlusCircleOutlined,
|
||||||
|
QuestionCircleFilled,
|
||||||
|
ScheduleOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
ToolFilled,
|
||||||
|
UnorderedListOutlined,
|
||||||
|
UsergroupAddOutlined,
|
||||||
|
UserOutlined
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { FaCalendarAlt, FaCarCrash, FaTasks } from "react-icons/fa";
|
||||||
|
import { BsKanban } from "react-icons/bs";
|
||||||
|
import { FiLogOut } from "react-icons/fi";
|
||||||
|
import { GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
||||||
|
import { RiSurveyLine } from "react-icons/ri";
|
||||||
|
import { IoBusinessOutline } from "react-icons/io5";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
import LockWrapper from "../../components/lock-wrapper/lock-wrapper.component.jsx";
|
||||||
|
|
||||||
|
const buildLeftMenuItems = ({
|
||||||
|
t,
|
||||||
|
bodyshop,
|
||||||
|
recentItems,
|
||||||
|
setTaskUpsertContext,
|
||||||
|
setReportCenterContext,
|
||||||
|
signOutStart,
|
||||||
|
accountingChildren
|
||||||
|
}) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "home",
|
||||||
|
id: "header-home",
|
||||||
|
icon: <HomeFilled />,
|
||||||
|
label: <Link to="/manage/">{t("menus.header.home")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "schedule",
|
||||||
|
id: "header-schedule",
|
||||||
|
icon: <FaCalendarAlt />,
|
||||||
|
label: <Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "jobssubmenu",
|
||||||
|
id: "header-jobs",
|
||||||
|
icon: <FaCarCrash />,
|
||||||
|
label: t("menus.header.jobs"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "activejobs",
|
||||||
|
id: "header-active-jobs",
|
||||||
|
icon: <FileFilled />,
|
||||||
|
label: <Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "readyjobs",
|
||||||
|
id: "header-ready-jobs",
|
||||||
|
icon: <CheckCircleOutlined />,
|
||||||
|
label: <Link to="/manage/jobs/ready">{t("menus.header.readyjobs")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "parts-queue",
|
||||||
|
id: "header-parts-queue",
|
||||||
|
icon: <ToolFilled />,
|
||||||
|
label: <Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "availablejobs",
|
||||||
|
id: "header-jobs-available",
|
||||||
|
icon: <ImportOutlined />,
|
||||||
|
label: <Link to="/manage/available">{t("menus.header.availablejobs")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "newjob",
|
||||||
|
id: "header-new-job",
|
||||||
|
icon: <FileAddOutlined />,
|
||||||
|
label: <Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
|
||||||
|
},
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "alljobs",
|
||||||
|
id: "header-all-jobs",
|
||||||
|
icon: <UnorderedListOutlined />,
|
||||||
|
label: <Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||||
|
},
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "productionlist",
|
||||||
|
id: "header-production-list",
|
||||||
|
icon: <ScheduleOutlined />,
|
||||||
|
label: <Link to="/manage/production/list">{t("menus.header.productionlist")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "productionboard",
|
||||||
|
id: "header-production-board",
|
||||||
|
icon: <BsKanban />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/production/board">
|
||||||
|
<LockWrapper featureName="visualboard" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.productionboard")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "scoreboard",
|
||||||
|
id: "header-scoreboard",
|
||||||
|
icon: <LineChartOutlined />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/scoreboard">
|
||||||
|
<LockWrapper featureName="scoreboard" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.scoreboard")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "customers",
|
||||||
|
id: "header-customers",
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: t("menus.header.customers"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "owners",
|
||||||
|
id: "header-owners",
|
||||||
|
icon: <TeamOutlined />,
|
||||||
|
label: <Link to="/manage/owners">{t("menus.header.owners")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "vehicles",
|
||||||
|
id: "header-vehicles",
|
||||||
|
icon: <CarFilled />,
|
||||||
|
label: <Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ccs",
|
||||||
|
id: "header-css",
|
||||||
|
icon: <CarFilled />,
|
||||||
|
label: (
|
||||||
|
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.courtesycars")}
|
||||||
|
</LockWrapper>
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "courtesycarsall",
|
||||||
|
id: "header-courtesycars-all",
|
||||||
|
icon: <CarFilled />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/courtesycars">
|
||||||
|
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.courtesycars-all")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "contracts",
|
||||||
|
id: "header-contracts",
|
||||||
|
icon: <FileFilled />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/courtesycars/contracts">
|
||||||
|
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.courtesycars-contracts")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "newcontract",
|
||||||
|
id: "header-newcontract",
|
||||||
|
icon: <FileAddFilled />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/courtesycars/contracts/new">
|
||||||
|
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.courtesycars-newcontract")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
...(accountingChildren.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "accounting",
|
||||||
|
id: "header-accounting",
|
||||||
|
icon: <DollarCircleFilled />,
|
||||||
|
label: t("menus.header.accounting"),
|
||||||
|
children: accountingChildren
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
key: "phonebook",
|
||||||
|
id: "header-phonebook",
|
||||||
|
icon: <PhoneOutlined />,
|
||||||
|
label: <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "temporarydocs",
|
||||||
|
id: "header-temporarydocs",
|
||||||
|
icon: <PaperClipOutlined />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/temporarydocs">
|
||||||
|
<LockWrapper featureName="media" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.temporarydocs")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "tasks",
|
||||||
|
id: "tasks",
|
||||||
|
icon: <FaTasks />,
|
||||||
|
label: t("menus.header.tasks"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "createTask",
|
||||||
|
id: "header-create-task",
|
||||||
|
icon: <PlusCircleOutlined />,
|
||||||
|
label: t("menus.header.create_task"),
|
||||||
|
onClick: () => setTaskUpsertContext({ actions: {}, context: {} })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "mytasks",
|
||||||
|
id: "header-my-tasks",
|
||||||
|
icon: <FaTasks />,
|
||||||
|
label: <Link to="/manage/tasks/mytasks">{t("menus.header.my_tasks")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "all_tasks",
|
||||||
|
id: "header-all-tasks",
|
||||||
|
icon: <FaTasks />,
|
||||||
|
label: <Link to="/manage/tasks/alltasks">{t("menus.header.all_tasks")}</Link>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "shopsubmenu",
|
||||||
|
id: "header-shopsubmenu",
|
||||||
|
icon: <SettingOutlined />,
|
||||||
|
label: t("menus.header.shop"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "shop",
|
||||||
|
id: "header-shop",
|
||||||
|
icon: <GiSettingsKnobs />,
|
||||||
|
label: <Link to="/manage/shop?tab=info">{t("menus.header.shop_config")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "dashboard",
|
||||||
|
id: "header-dashboard",
|
||||||
|
icon: <DashboardFilled />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/dashboard">
|
||||||
|
<LockWrapper featureName="bills">{t("menus.header.dashboard")}</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "reportcenter",
|
||||||
|
id: "header-reportcenter",
|
||||||
|
icon: <BarChartOutlined />,
|
||||||
|
label: t("menus.header.reportcenter"),
|
||||||
|
onClick: () => setReportCenterContext({ actions: {}, context: {} })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "shop-vendors",
|
||||||
|
id: "header-shop-vendors",
|
||||||
|
icon: <IoBusinessOutline />,
|
||||||
|
label: <Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "shop-csi",
|
||||||
|
id: "header-shop-csi",
|
||||||
|
icon: <RiSurveyLine />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/shop/csi">
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.shop_csi")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "recent",
|
||||||
|
id: "header-recent",
|
||||||
|
icon: <ClockCircleFilled />,
|
||||||
|
label: t("menus.header.recent"),
|
||||||
|
children: recentItems.map((i, idx) => ({
|
||||||
|
key: idx,
|
||||||
|
id: `header-recent-${idx}`,
|
||||||
|
label: <Link to={i.url}>{i.label}</Link>
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "user",
|
||||||
|
id: "header-user",
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: t("menus.currentuser.profile"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "signout",
|
||||||
|
id: "header-signout",
|
||||||
|
icon: <FiLogOut />,
|
||||||
|
danger: true,
|
||||||
|
label: t("user.actions.signout"),
|
||||||
|
onClick: () => signOutStart()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "help",
|
||||||
|
id: "header-help",
|
||||||
|
icon: <QuestionCircleFilled />,
|
||||||
|
label: t("menus.header.help"),
|
||||||
|
onClick: () => window.open("https://help.imex.online/", "_blank")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "remoteassist",
|
||||||
|
id: "header-remote-assist",
|
||||||
|
icon: <OneToOneOutlined />,
|
||||||
|
label: t("menus.header.remoteassist"),
|
||||||
|
children: [
|
||||||
|
...(InstanceRenderManager({ imex: true, rome: false })
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "rescue",
|
||||||
|
id: "header-rescue",
|
||||||
|
icon: <PlusCircleOutlined />,
|
||||||
|
label: t("menus.header.rescueme"),
|
||||||
|
onClick: () => window.open("https://imexrescue.com/", "_blank")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
key: "rescue-zoho",
|
||||||
|
id: "header-rescue-zoho",
|
||||||
|
icon: <UsergroupAddOutlined />,
|
||||||
|
label: t("menus.header.rescuemezoho"),
|
||||||
|
onClick: () => window.open("https://join.zoho.com/", "_blank")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "shiftclock",
|
||||||
|
id: "header-shiftclock",
|
||||||
|
icon: <GiPlayerTime />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/shiftclock">
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.shiftclock")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "profile",
|
||||||
|
id: "header-profile",
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: <Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default buildLeftMenuItems;
|
||||||
@@ -1,59 +1,29 @@
|
|||||||
import {
|
// noinspection RegExpAnonymousGroup
|
||||||
BankFilled,
|
|
||||||
BarChartOutlined,
|
import { BellFilled } from "@ant-design/icons";
|
||||||
BellFilled,
|
|
||||||
CarFilled,
|
|
||||||
CheckCircleOutlined,
|
|
||||||
ClockCircleFilled,
|
|
||||||
DashboardFilled,
|
|
||||||
DollarCircleFilled,
|
|
||||||
ExportOutlined,
|
|
||||||
FieldTimeOutlined,
|
|
||||||
FileAddFilled,
|
|
||||||
FileAddOutlined,
|
|
||||||
FileFilled,
|
|
||||||
HomeFilled,
|
|
||||||
ImportOutlined,
|
|
||||||
LineChartOutlined,
|
|
||||||
PaperClipOutlined,
|
|
||||||
PhoneOutlined,
|
|
||||||
PlusCircleOutlined,
|
|
||||||
QuestionCircleFilled,
|
|
||||||
ScheduleOutlined,
|
|
||||||
SettingOutlined,
|
|
||||||
TeamOutlined,
|
|
||||||
ToolFilled,
|
|
||||||
UnorderedListOutlined,
|
|
||||||
UserOutlined
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Badge, Layout, Menu, Spin } from "antd";
|
import { Badge, Layout, Menu, Spin, Tooltip } from "antd";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { BsKanban } from "react-icons/bs";
|
import { FaTasks } from "react-icons/fa";
|
||||||
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
|
||||||
import { FiLogOut } from "react-icons/fi";
|
|
||||||
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
|
||||||
import { IoBusinessOutline } from "react-icons/io5";
|
|
||||||
import { RiSurveyLine } from "react-icons/ri";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { TASKS_CENTER_POLL_INTERVAL, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
||||||
|
import { QUERY_MY_TASKS_COUNT } from "../../graphql/tasks.queries.js";
|
||||||
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { signOutStart } from "../../redux/user/user.actions";
|
import { signOutStart } from "../../redux/user/user.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
|
||||||
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
|
||||||
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
|
||||||
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
||||||
|
import TaskCenterContainer from "../task-center/task-center.container.jsx";
|
||||||
|
import buildAccountingChildren from "./buildAccountingChildren.jsx";
|
||||||
|
import buildLeftMenuItems from "./buildLeftMenuItems.jsx";
|
||||||
|
|
||||||
// Redux mappings
|
// --- Redux mappings ---
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
recentItems: selectRecentItems,
|
recentItems: selectRecentItems,
|
||||||
@@ -71,36 +41,8 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||||
});
|
});
|
||||||
|
|
||||||
function Header({
|
// --- Utility Hooks ---
|
||||||
handleMenuClick,
|
function useUnreadNotifications(userAssociationId, isConnected, scenarioNotificationsOn) {
|
||||||
currentUser,
|
|
||||||
bodyshop,
|
|
||||||
selectedHeader,
|
|
||||||
signOutStart,
|
|
||||||
setBillEnterContext,
|
|
||||||
setTimeTicketContext,
|
|
||||||
setPaymentContext,
|
|
||||||
setReportCenterContext,
|
|
||||||
recentItems,
|
|
||||||
setCardPaymentContext,
|
|
||||||
setTaskUpsertContext
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
treatments: { ImEXPay, DmsAp, Simple_Inventory }
|
|
||||||
} = useSplitTreatments({
|
|
||||||
attributes: {},
|
|
||||||
names: ["ImEXPay", "DmsAp", "Simple_Inventory"],
|
|
||||||
splitKey: bodyshop && bodyshop.imexshopid
|
|
||||||
});
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { isConnected, scenarioNotificationsOn } = useSocket();
|
|
||||||
const [notificationVisible, setNotificationVisible] = useState(false);
|
|
||||||
const baseTitleRef = useRef(document.title || "");
|
|
||||||
const lastSetTitleRef = useRef("");
|
|
||||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
|
||||||
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: unreadData,
|
data: unreadData,
|
||||||
refetch: refetchUnread,
|
refetch: refetchUnread,
|
||||||
@@ -126,618 +68,286 @@ function Header({
|
|||||||
}
|
}
|
||||||
}, [isConnected, unreadLoading, refetchUnread, userAssociationId]);
|
}, [isConnected, unreadLoading, refetchUnread, userAssociationId]);
|
||||||
|
|
||||||
// Keep The unread count in the title.
|
return { unreadCount, unreadLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
function useIncompleteTaskCount(assignedToId, bodyshopId, isEmployee, isConnected) {
|
||||||
|
const { data: taskCountData, loading: taskCountLoading } = useQuery(QUERY_MY_TASKS_COUNT, {
|
||||||
|
variables: { assigned_to: assignedToId, bodyshopid: bodyshopId },
|
||||||
|
skip: !assignedToId || !bodyshopId || !isEmployee,
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
pollInterval: isConnected ? 0 : TASKS_CENTER_POLL_INTERVAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const incompleteTaskCount = taskCountData?.tasks_aggregate?.aggregate?.count ?? 0;
|
||||||
|
return { incompleteTaskCount, taskCountLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main Component ---
|
||||||
|
function Header(props) {
|
||||||
|
const {
|
||||||
|
handleMenuClick,
|
||||||
|
currentUser,
|
||||||
|
bodyshop,
|
||||||
|
selectedHeader,
|
||||||
|
signOutStart,
|
||||||
|
setBillEnterContext,
|
||||||
|
setTimeTicketContext,
|
||||||
|
setPaymentContext,
|
||||||
|
setReportCenterContext,
|
||||||
|
recentItems,
|
||||||
|
setCardPaymentContext,
|
||||||
|
setTaskUpsertContext
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
// Feature flags
|
||||||
|
const {
|
||||||
|
treatments: { ImEXPay, DmsAp, Simple_Inventory }
|
||||||
|
} = useSplitTreatments({
|
||||||
|
attributes: {},
|
||||||
|
names: ["ImEXPay", "DmsAp", "Simple_Inventory"],
|
||||||
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
|
});
|
||||||
|
|
||||||
|
// Contexts and hooks
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isConnected, scenarioNotificationsOn } = useSocket();
|
||||||
|
const [notificationVisible, setNotificationVisible] = useState(false);
|
||||||
|
const [taskCenterVisible, setTaskCenterVisible] = useState(false);
|
||||||
|
const baseTitleRef = useRef(document.title || "");
|
||||||
|
const lastSetTitleRef = useRef("");
|
||||||
|
const taskCenterRef = useRef(null);
|
||||||
|
const notificationRef = useRef(null);
|
||||||
|
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||||
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
|
||||||
|
// Data hooks
|
||||||
|
const { unreadCount, unreadLoading } = useUnreadNotifications(
|
||||||
|
userAssociationId,
|
||||||
|
isConnected,
|
||||||
|
scenarioNotificationsOn
|
||||||
|
);
|
||||||
|
const assignedToId = bodyshop?.employees?.find((e) => e.user_email === currentUser.email)?.id;
|
||||||
|
const { incompleteTaskCount, taskCountLoading } = useIncompleteTaskCount(
|
||||||
|
assignedToId,
|
||||||
|
bodyshop?.id,
|
||||||
|
isEmployee,
|
||||||
|
isConnected
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Effects ---
|
||||||
|
|
||||||
|
// Update document title with unread count
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateTitle = () => {
|
const updateTitle = () => {
|
||||||
const currentTitle = document.title;
|
const currentTitle = document.title;
|
||||||
// Check if the current title differs from what we last set
|
|
||||||
if (currentTitle !== lastSetTitleRef.current) {
|
if (currentTitle !== lastSetTitleRef.current) {
|
||||||
// Extract base title by removing any unread count prefix
|
|
||||||
const baseTitleMatch = currentTitle.match(/^\(\d+\)\s*(.*)$/);
|
const baseTitleMatch = currentTitle.match(/^\(\d+\)\s*(.*)$/);
|
||||||
baseTitleRef.current = baseTitleMatch ? baseTitleMatch[1] : currentTitle;
|
baseTitleRef.current = baseTitleMatch ? baseTitleMatch[1] : currentTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply unread count to the base title
|
|
||||||
const newTitle = unreadCount > 0 ? `(${unreadCount}) ${baseTitleRef.current}` : baseTitleRef.current;
|
const newTitle = unreadCount > 0 ? `(${unreadCount}) ${baseTitleRef.current}` : baseTitleRef.current;
|
||||||
|
|
||||||
// Only update if the title has changed to avoid unnecessary DOM writes
|
|
||||||
if (document.title !== newTitle) {
|
if (document.title !== newTitle) {
|
||||||
document.title = newTitle;
|
document.title = newTitle;
|
||||||
lastSetTitleRef.current = newTitle; // Store what we set
|
lastSetTitleRef.current = newTitle;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updateTitle();
|
||||||
|
const interval = setInterval(updateTitle, 100);
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
document.title = baseTitleRef.current;
|
||||||
|
};
|
||||||
|
}, [unreadCount]);
|
||||||
|
|
||||||
|
// Handle outside clicks for popovers
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
const isNotificationClick = event.target.closest("#header-notifications");
|
||||||
|
const isTaskCenterClick = event.target.closest("#header-taskcenter");
|
||||||
|
|
||||||
|
if (isNotificationClick && scenarioNotificationsOn) {
|
||||||
|
setTaskCenterVisible(false); // Close task center
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTaskCenterClick) {
|
||||||
|
setNotificationVisible(scenarioNotificationsOn ? false : notificationVisible); // Close notification center if enabled
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskCenterVisible && taskCenterRef.current && !taskCenterRef.current.contains(event.target)) {
|
||||||
|
setTaskCenterVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
scenarioNotificationsOn &&
|
||||||
|
notificationVisible &&
|
||||||
|
notificationRef.current &&
|
||||||
|
!notificationRef.current.contains(event.target)
|
||||||
|
) {
|
||||||
|
setNotificationVisible(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial update
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
updateTitle();
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, [taskCenterVisible, notificationVisible, scenarioNotificationsOn]);
|
||||||
|
|
||||||
// Poll every 100ms to catch child component changes
|
// --- Event Handlers ---
|
||||||
const interval = setInterval(updateTitle, 100);
|
const handleTaskCenterClick = useCallback(
|
||||||
|
(e) => {
|
||||||
|
setTaskCenterVisible((prev) => {
|
||||||
|
if (prev) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (handleMenuClick) handleMenuClick(e);
|
||||||
|
},
|
||||||
|
[handleMenuClick]
|
||||||
|
);
|
||||||
|
|
||||||
// Cleanup
|
const handleNotificationClick = useCallback(
|
||||||
return () => {
|
(e) => {
|
||||||
clearInterval(interval);
|
setNotificationVisible((prev) => {
|
||||||
document.title = baseTitleRef.current; // Reset to base title on unmount
|
if (prev) return false;
|
||||||
};
|
return true;
|
||||||
}, [unreadCount]); // Re-run when unreadCount changes
|
});
|
||||||
|
if (handleMenuClick) handleMenuClick(e);
|
||||||
|
},
|
||||||
|
[handleMenuClick]
|
||||||
|
);
|
||||||
|
|
||||||
const handleNotificationClick = (e) => {
|
// --- Menu Items ---
|
||||||
setNotificationVisible(!notificationVisible);
|
|
||||||
if (handleMenuClick) handleMenuClick(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
const accountingChildren = [
|
// built externally to keep the component clean, but on this level to prevent unnecessary re-renders
|
||||||
{
|
const accountingChildren = useMemo(
|
||||||
key: "bills",
|
() =>
|
||||||
id: "header-accounting-bills",
|
buildAccountingChildren({
|
||||||
icon: <FaFileInvoiceDollar />,
|
t,
|
||||||
label: (
|
bodyshop,
|
||||||
<Link to="/manage/bills">
|
currentUser,
|
||||||
<LockWrapper featureName="bills" bodyshop={bodyshop}>
|
setBillEnterContext,
|
||||||
{t("menus.header.bills")}
|
setPaymentContext,
|
||||||
</LockWrapper>
|
setCardPaymentContext,
|
||||||
</Link>
|
setTimeTicketContext,
|
||||||
)
|
ImEXPay,
|
||||||
},
|
DmsAp,
|
||||||
{
|
Simple_Inventory
|
||||||
key: "enterbills",
|
}),
|
||||||
id: "header-accounting-enterbills",
|
[
|
||||||
icon: <GiPayMoney />,
|
t,
|
||||||
label: (
|
bodyshop,
|
||||||
<LockWrapper featureName="bills" bodyshop={bodyshop}>
|
currentUser,
|
||||||
{t("menus.header.enterbills")}
|
setBillEnterContext,
|
||||||
</LockWrapper>
|
setPaymentContext,
|
||||||
),
|
setCardPaymentContext,
|
||||||
onClick: () =>
|
setTimeTicketContext,
|
||||||
HasFeatureAccess({ featureName: "bills", bodyshop }) &&
|
ImEXPay,
|
||||||
setBillEnterContext({
|
DmsAp,
|
||||||
actions: {},
|
Simple_Inventory
|
||||||
context: {}
|
]
|
||||||
})
|
);
|
||||||
},
|
|
||||||
...(Simple_Inventory.treatment === "on"
|
// Built externally to keep the component clean
|
||||||
? [
|
const leftMenuItems = useMemo(
|
||||||
{ type: "divider" },
|
() =>
|
||||||
{
|
buildLeftMenuItems({
|
||||||
key: "inventory",
|
t,
|
||||||
id: "header-accounting-inventory",
|
bodyshop,
|
||||||
icon: <FaFileInvoiceDollar />,
|
recentItems,
|
||||||
label: <Link to="/manage/inventory">{t("menus.header.inventory")}</Link>
|
setTaskUpsertContext,
|
||||||
}
|
setReportCenterContext,
|
||||||
]
|
signOutStart,
|
||||||
: []),
|
accountingChildren
|
||||||
{ type: "divider" },
|
}),
|
||||||
{
|
[t, bodyshop, recentItems, setTaskUpsertContext, setReportCenterContext, signOutStart, accountingChildren]
|
||||||
key: "allpayments",
|
);
|
||||||
id: "header-accounting-allpayments",
|
|
||||||
icon: <BankFilled />,
|
const rightMenuItems = useMemo(() => {
|
||||||
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
const items = [];
|
||||||
},
|
if (scenarioNotificationsOn) {
|
||||||
{
|
items.push({
|
||||||
key: "enterpayments",
|
key: "notifications",
|
||||||
id: "header-accounting-enterpayments",
|
id: "header-notifications",
|
||||||
icon: <FaCreditCard />,
|
icon: unreadLoading ? (
|
||||||
label: t("menus.header.enterpayment"),
|
<Spin size="small" />
|
||||||
onClick: () =>
|
) : (
|
||||||
setPaymentContext({
|
<Badge offset={[8, 0]} size="small" count={isEmployee ? unreadCount : 0}>
|
||||||
actions: {},
|
<BellFilled />
|
||||||
context: null
|
</Badge>
|
||||||
})
|
),
|
||||||
},
|
onClick: handleNotificationClick
|
||||||
...(ImEXPay.treatment === "on"
|
});
|
||||||
? [
|
|
||||||
{
|
|
||||||
key: "entercardpayments",
|
|
||||||
id: "header-accounting-entercardpayments",
|
|
||||||
icon: <FaCreditCard />,
|
|
||||||
label: t("menus.header.entercardpayment"),
|
|
||||||
onClick: () => setCardPaymentContext({ actions: {}, context: {} })
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{ type: "divider" },
|
|
||||||
{
|
|
||||||
key: "timetickets",
|
|
||||||
id: "header-accounting-timetickets",
|
|
||||||
icon: <FieldTimeOutlined />,
|
|
||||||
label: (
|
|
||||||
<Link to="/manage/timetickets">
|
|
||||||
<LockWrapper featureName="timetickets" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.timetickets")}
|
|
||||||
</LockWrapper>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
...(bodyshop?.md_tasks_presets?.use_approvals
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
key: "ttapprovals",
|
|
||||||
id: "header-accounting-ttapprovals",
|
|
||||||
icon: <FieldTimeOutlined />,
|
|
||||||
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
key: "entertimetickets",
|
|
||||||
id: "header-accounting-entertimetickets",
|
|
||||||
icon: <GiPlayerTime />,
|
|
||||||
label: (
|
|
||||||
<LockWrapper featureName="timetickets" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.entertimeticket")}
|
|
||||||
</LockWrapper>
|
|
||||||
),
|
|
||||||
onClick: () =>
|
|
||||||
HasFeatureAccess({ featureName: "timetickets", bodyshop }) &&
|
|
||||||
setTimeTicketContext({
|
|
||||||
actions: {},
|
|
||||||
context: {
|
|
||||||
created_by: currentUser.displayName
|
|
||||||
? `${currentUser.email} | ${currentUser.displayName}`
|
|
||||||
: currentUser.email
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ type: "divider" },
|
|
||||||
{
|
|
||||||
key: "accountingexport",
|
|
||||||
id: "header-accounting-export",
|
|
||||||
icon: <ExportOutlined />,
|
|
||||||
label: (
|
|
||||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.export")}
|
|
||||||
</LockWrapper>
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: "receivables",
|
|
||||||
id: "header-accounting-receivables",
|
|
||||||
label: (
|
|
||||||
<Link to="/manage/accounting/receivables">
|
|
||||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.accounting-receivables")}
|
|
||||||
</LockWrapper>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) ||
|
|
||||||
DmsAp.treatment === "on"
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
key: "payables",
|
|
||||||
id: "header-accounting-payables",
|
|
||||||
label: (
|
|
||||||
<Link to="/manage/accounting/payables">
|
|
||||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.accounting-payables")}
|
|
||||||
</LockWrapper>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
key: "payments",
|
|
||||||
id: "header-accounting-payments",
|
|
||||||
label: (
|
|
||||||
<Link to="/manage/accounting/payments">
|
|
||||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.accounting-payments")}
|
|
||||||
</LockWrapper>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{ type: "divider" },
|
|
||||||
{
|
|
||||||
key: "exportlogs",
|
|
||||||
id: "header-accounting-exportlogs",
|
|
||||||
label: (
|
|
||||||
<Link to="/manage/accounting/exportlogs">
|
|
||||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.export-logs")}
|
|
||||||
</LockWrapper>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
];
|
items.push({
|
||||||
|
key: "taskcenter",
|
||||||
// Left menu items (includes original navigation items)
|
id: "header-taskcenter",
|
||||||
const leftMenuItems = [
|
icon: taskCountLoading ? (
|
||||||
{
|
<Spin size="small" />
|
||||||
key: "home",
|
) : (
|
||||||
id: "header-home",
|
<Badge offset={[8, 0]} size="small" count={incompleteTaskCount > 0 ? incompleteTaskCount : 0} showZero={false}>
|
||||||
icon: <HomeFilled />,
|
<Tooltip title={t("menus.header.tasks")}>
|
||||||
label: <Link to="/manage/">{t("menus.header.home")}</Link>
|
<FaTasks />
|
||||||
},
|
</Tooltip>
|
||||||
{
|
</Badge>
|
||||||
key: "schedule",
|
|
||||||
id: "header-schedule",
|
|
||||||
icon: <FaCalendarAlt />,
|
|
||||||
label: <Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "jobssubmenu",
|
|
||||||
id: "header-jobs",
|
|
||||||
icon: <FaCarCrash />,
|
|
||||||
label: t("menus.header.jobs"),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: "activejobs",
|
|
||||||
id: "header-active-jobs",
|
|
||||||
icon: <FileFilled />,
|
|
||||||
label: <Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "readyjobs",
|
|
||||||
id: "header-ready-jobs",
|
|
||||||
icon: <CheckCircleOutlined />,
|
|
||||||
label: <Link to="/manage/jobs/ready">{t("menus.header.readyjobs")}</Link>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "parts-queue",
|
|
||||||
id: "header-parts-queue",
|
|
||||||
icon: <ToolFilled />,
|
|
||||||
label: <Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "availablejobs",
|
|
||||||
id: "header-jobs-available",
|
|
||||||
icon: <ImportOutlined />,
|
|
||||||
label: <Link to="/manage/available">{t("menus.header.availablejobs")}</Link>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "newjob",
|
|
||||||
id: "header-new-job",
|
|
||||||
icon: <FileAddOutlined />,
|
|
||||||
label: <Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
|
|
||||||
},
|
|
||||||
{ type: "divider" },
|
|
||||||
{
|
|
||||||
key: "alljobs",
|
|
||||||
id: "header-all-jobs",
|
|
||||||
icon: <UnorderedListOutlined />,
|
|
||||||
label: <Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
|
||||||
},
|
|
||||||
{ type: "divider" },
|
|
||||||
{
|
|
||||||
key: "productionlist",
|
|
||||||
id: "header-production-list",
|
|
||||||
icon: <ScheduleOutlined />,
|
|
||||||
label: <Link to="/manage/production/list">{t("menus.header.productionlist")}</Link>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "productionboard",
|
|
||||||
id: "header-production-board",
|
|
||||||
icon: <BsKanban />,
|
|
||||||
label: (
|
|
||||||
<Link to="/manage/production/board">
|
|
||||||
<LockWrapper featureName="visualboard" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.productionboard")}
|
|
||||||
</LockWrapper>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{ type: "divider" },
|
|
||||||
{
|
|
||||||
key: "scoreboard",
|
|
||||||
id: "header-scoreboard",
|
|
||||||
icon: <LineChartOutlined />,
|
|
||||||
label: (
|
|
||||||
<Link to="/manage/scoreboard">
|
|
||||||
<LockWrapper featureName="scoreboard" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.scoreboard")}
|
|
||||||
</LockWrapper>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "customers",
|
|
||||||
id: "header-customers",
|
|
||||||
icon: <UserOutlined />,
|
|
||||||
label: t("menus.header.customers"),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: "owners",
|
|
||||||
id: "header-owners",
|
|
||||||
icon: <TeamOutlined />,
|
|
||||||
label: <Link to="/manage/owners">{t("menus.header.owners")}</Link>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "vehicles",
|
|
||||||
id: "header-vehicles",
|
|
||||||
icon: <CarFilled />,
|
|
||||||
label: <Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link>
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "ccs",
|
|
||||||
id: "header-css",
|
|
||||||
icon: <CarFilled />,
|
|
||||||
label: (
|
|
||||||
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.courtesycars")}
|
|
||||||
</LockWrapper>
|
|
||||||
),
|
),
|
||||||
children: [
|
onClick: handleTaskCenterClick
|
||||||
{
|
});
|
||||||
key: "courtesycarsall",
|
return items;
|
||||||
id: "header-courtesycars-all",
|
}, [
|
||||||
icon: <CarFilled />,
|
scenarioNotificationsOn,
|
||||||
label: (
|
unreadLoading,
|
||||||
<Link to="/manage/courtesycars">
|
unreadCount,
|
||||||
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
|
taskCountLoading,
|
||||||
{t("menus.header.courtesycars-all")}
|
incompleteTaskCount,
|
||||||
</LockWrapper>
|
isEmployee,
|
||||||
</Link>
|
handleNotificationClick,
|
||||||
)
|
handleTaskCenterClick,
|
||||||
},
|
t
|
||||||
{
|
]);
|
||||||
key: "contracts",
|
|
||||||
id: "header-contracts",
|
|
||||||
icon: <FileFilled />,
|
|
||||||
label: (
|
|
||||||
<Link to="/manage/courtesycars/contracts">
|
|
||||||
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.courtesycars-contracts")}
|
|
||||||
</LockWrapper>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "newcontract",
|
|
||||||
id: "header-newcontract",
|
|
||||||
icon: <FileAddFilled />,
|
|
||||||
label: (
|
|
||||||
<Link to="/manage/courtesycars/contracts/new">
|
|
||||||
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.courtesycars-newcontract")}
|
|
||||||
</LockWrapper>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
...(accountingChildren.length > 0
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
key: "accounting",
|
|
||||||
id: "header-accounting",
|
|
||||||
icon: <DollarCircleFilled />,
|
|
||||||
label: t("menus.header.accounting"),
|
|
||||||
children: accountingChildren
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
key: "phonebook",
|
|
||||||
id: "header-phonebook",
|
|
||||||
icon: <PhoneOutlined />,
|
|
||||||
label: <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "temporarydocs",
|
|
||||||
id: "header-temporarydocs",
|
|
||||||
icon: <PaperClipOutlined />,
|
|
||||||
label: (
|
|
||||||
<Link to="/manage/temporarydocs">
|
|
||||||
<LockWrapper featureName="media" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.temporarydocs")}
|
|
||||||
</LockWrapper>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "tasks",
|
|
||||||
id: "tasks",
|
|
||||||
icon: <FaTasks />,
|
|
||||||
label: t("menus.header.tasks"),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: "createTask",
|
|
||||||
id: "header-create-task",
|
|
||||||
icon: <PlusCircleOutlined />,
|
|
||||||
label: t("menus.header.create_task"),
|
|
||||||
onClick: () => setTaskUpsertContext({ actions: {}, context: {} })
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "mytasks",
|
|
||||||
id: "header-my-tasks",
|
|
||||||
icon: <FaTasks />,
|
|
||||||
label: <Link to="/manage/tasks/mytasks">{t("menus.header.my_tasks")}</Link>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "all_tasks",
|
|
||||||
id: "header-all-tasks",
|
|
||||||
icon: <FaTasks />,
|
|
||||||
label: <Link to="/manage/tasks/alltasks">{t("menus.header.all_tasks")}</Link>
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "shopsubmenu",
|
|
||||||
id: "header-shopsubmenu",
|
|
||||||
icon: <SettingOutlined />,
|
|
||||||
label: t("menus.header.shop"),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: "shop",
|
|
||||||
id: "header-shop",
|
|
||||||
icon: <GiSettingsKnobs />,
|
|
||||||
label: <Link to="/manage/shop?tab=info">{t("menus.header.shop_config")}</Link>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "dashboard",
|
|
||||||
id: "header-dashboard",
|
|
||||||
icon: <DashboardFilled />,
|
|
||||||
label: (
|
|
||||||
<Link to="/manage/dashboard">
|
|
||||||
<LockWrapper featureName="bills">{t("menus.header.dashboard")}</LockWrapper>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "reportcenter",
|
|
||||||
id: "header-reportcenter",
|
|
||||||
icon: <BarChartOutlined />,
|
|
||||||
label: t("menus.header.reportcenter"),
|
|
||||||
onClick: () => setReportCenterContext({ actions: {}, context: {} })
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "shop-vendors",
|
|
||||||
id: "header-shop-vendors",
|
|
||||||
icon: <IoBusinessOutline />,
|
|
||||||
label: <Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "shop-csi",
|
|
||||||
id: "header-shop-csi",
|
|
||||||
icon: <RiSurveyLine />,
|
|
||||||
label: (
|
|
||||||
<Link to="/manage/shop/csi">
|
|
||||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.shop_csi")}
|
|
||||||
</LockWrapper>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "recent",
|
|
||||||
id: "header-recent",
|
|
||||||
icon: <ClockCircleFilled />,
|
|
||||||
label: t("menus.header.recent"),
|
|
||||||
children: recentItems.map((i, idx) => ({
|
|
||||||
key: idx,
|
|
||||||
id: `header-recent-${idx}`,
|
|
||||||
label: <Link to={i.url}>{i.label}</Link>
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "user",
|
|
||||||
id: "header-user",
|
|
||||||
icon: <UserOutlined />,
|
|
||||||
label: t("menus.currentuser.profile"),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: "signout",
|
|
||||||
id: "header-signout",
|
|
||||||
icon: <FiLogOut />,
|
|
||||||
danger: true,
|
|
||||||
label: t("user.actions.signout"),
|
|
||||||
onClick: () => signOutStart()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "help",
|
|
||||||
id: "header-help",
|
|
||||||
icon: <QuestionCircleFilled />,
|
|
||||||
label: t("menus.header.help"),
|
|
||||||
onClick: () => window.open("https://help.imex.online/", "_blank")
|
|
||||||
},
|
|
||||||
...(InstanceRenderManager({ imex: true, rome: false })
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
key: "rescue",
|
|
||||||
id: "header-rescue",
|
|
||||||
icon: <CarFilled />,
|
|
||||||
label: t("menus.header.rescueme"),
|
|
||||||
onClick: () => window.open("https://imexrescue.com/", "_blank")
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
key: "shiftclock",
|
|
||||||
id: "header-shiftclock",
|
|
||||||
icon: <GiPlayerTime />,
|
|
||||||
label: (
|
|
||||||
<Link to="/manage/shiftclock">
|
|
||||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
|
||||||
{t("menus.header.shiftclock")}
|
|
||||||
</LockWrapper>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "profile",
|
|
||||||
id: "header-profile",
|
|
||||||
icon: <UserOutlined />,
|
|
||||||
label: <Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Notifications item (always on the right)
|
|
||||||
const notificationItem = scenarioNotificationsOn
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
key: "notifications",
|
|
||||||
id: "header-notifications",
|
|
||||||
icon: unreadLoading ? (
|
|
||||||
<Spin size="small" />
|
|
||||||
) : (
|
|
||||||
<Badge offset={[8, 0]} size="small" count={isEmployee ? unreadCount : 0}>
|
|
||||||
<BellFilled />
|
|
||||||
</Badge>
|
|
||||||
),
|
|
||||||
onClick: handleNotificationClick
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
|
// --- Render ---
|
||||||
return (
|
return (
|
||||||
<Layout.Header style={{ padding: 0, background: "#001529" }}>
|
<Layout.Header style={{ padding: 0, background: "#001529" }}>
|
||||||
<div
|
<div style={{ display: "flex", alignItems: "center", height: "100%", overflow: "hidden" }}>
|
||||||
style={{
|
<div style={{ flexGrow: 1, overflowX: "auto", whiteSpace: "nowrap" }}>
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
height: "100%",
|
|
||||||
overflow: "hidden"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Menu
|
|
||||||
mode="horizontal"
|
|
||||||
theme="dark"
|
|
||||||
selectedKeys={[selectedHeader]}
|
|
||||||
onClick={handleMenuClick}
|
|
||||||
subMenuCloseDelay={0.3}
|
|
||||||
items={leftMenuItems}
|
|
||||||
style={{
|
|
||||||
flex: "1 1 auto",
|
|
||||||
minWidth: 0,
|
|
||||||
overflowX: "auto",
|
|
||||||
borderBottom: "none",
|
|
||||||
background: "transparent"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{scenarioNotificationsOn && (
|
|
||||||
<Menu
|
<Menu
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
theme="dark"
|
theme="dark"
|
||||||
selectedKeys={[selectedHeader]}
|
selectedKeys={[selectedHeader]}
|
||||||
onClick={handleMenuClick}
|
onClick={handleMenuClick}
|
||||||
subMenuCloseDelay={0.3}
|
subMenuCloseDelay={0.3}
|
||||||
items={notificationItem}
|
items={leftMenuItems}
|
||||||
style={{ flex: "0 0 auto", minWidth: 0, borderBottom: "none", background: "transparent" }}
|
style={{ borderBottom: "none", background: "transparent", minWidth: "100%" }}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
<div style={{ width: 120, flexShrink: 0 }}>
|
||||||
|
<Menu
|
||||||
|
mode="horizontal"
|
||||||
|
theme="dark"
|
||||||
|
selectedKeys={[selectedHeader]}
|
||||||
|
onClick={handleMenuClick}
|
||||||
|
subMenuCloseDelay={0.3}
|
||||||
|
items={rightMenuItems}
|
||||||
|
style={{ borderBottom: "none", background: "transparent", justifyContent: "flex-end" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{scenarioNotificationsOn && (
|
{scenarioNotificationsOn && (
|
||||||
<NotificationCenterContainer
|
<div ref={notificationRef}>
|
||||||
visible={notificationVisible}
|
<NotificationCenterContainer
|
||||||
onClose={() => setNotificationVisible(false)}
|
visible={notificationVisible}
|
||||||
unreadCount={unreadCount}
|
onClose={() => setNotificationVisible(false)}
|
||||||
/>
|
unreadCount={unreadCount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div ref={taskCenterRef}>
|
||||||
|
<TaskCenterContainer
|
||||||
|
incompleteTaskCount={incompleteTaskCount}
|
||||||
|
visible={taskCenterVisible}
|
||||||
|
onClose={() => setTaskCenterVisible(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Layout.Header>
|
</Layout.Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -385,7 +385,9 @@ export function ScheduleEventComponent({
|
|||||||
previousEvent: event.id,
|
previousEvent: event.id,
|
||||||
color: event.color,
|
color: event.color,
|
||||||
alt_transport: event.job && event.job.alt_transport,
|
alt_transport: event.job && event.job.alt_transport,
|
||||||
note: event.note
|
note: event.note,
|
||||||
|
scheduled_in: event.job && event.job.scheduled_in,
|
||||||
|
scheduled_completion: event.job && event.job.scheduled_completion
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -395,32 +397,33 @@ export function ScheduleEventComponent({
|
|||||||
) : (
|
) : (
|
||||||
<ScheduleManualEvent event={event} />
|
<ScheduleManualEvent event={event} />
|
||||||
)}
|
)}
|
||||||
{event.isintake && HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
{event.job &&
|
||||||
<Link
|
(HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
||||||
to={{
|
<Link
|
||||||
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
to={{
|
||||||
search: `?appointmentId=${event.id}`
|
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
||||||
}}
|
search: `?appointmentId=${event.id}`
|
||||||
>
|
}}
|
||||||
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
>
|
||||||
</Link>
|
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
||||||
) : (
|
</Link>
|
||||||
<Popover //open={open}
|
) : (
|
||||||
content={popMenu}
|
<Popover //open={open}
|
||||||
open={popOverVisible}
|
content={popMenu}
|
||||||
onOpenChange={setPopOverVisible}
|
open={popOverVisible}
|
||||||
onClick={(e) => {
|
onOpenChange={setPopOverVisible}
|
||||||
if (event.job?.id) {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
if (event.job?.id) {
|
||||||
getJobDetails();
|
e.stopPropagation();
|
||||||
}
|
getJobDetails();
|
||||||
}}
|
}
|
||||||
getPopupContainer={(trigger) => trigger.parentNode}
|
}}
|
||||||
trigger="click"
|
getPopupContainer={(trigger) => trigger.parentNode}
|
||||||
>
|
trigger="click"
|
||||||
<Button disabled={event.arrived}>{t("jobs.actions.intake_quick")}</Button>
|
>
|
||||||
</Popover>
|
<Button disabled={event.arrived}>{t("jobs.actions.intake_quick")}</Button>
|
||||||
)}
|
</Popover>
|
||||||
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Card, Form, Input, Switch } from "antd";
|
import { Button, Card, Form, Input, Switch } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { 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";
|
||||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
@@ -9,7 +9,6 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { logImEXEvent } from "../../../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../../../firebase/firebase.utils";
|
||||||
import { MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED } from "../../../../graphql/appointments.queries";
|
import { MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED } from "../../../../graphql/appointments.queries";
|
||||||
import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
|
import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
|
||||||
import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
|
|
||||||
import { insertAuditTrail } from "../../../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../../../redux/application/application.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
|
||||||
@@ -32,7 +31,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED);
|
const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED);
|
||||||
const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED);
|
const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED);
|
||||||
const [updateOwner] = useMutation(UPDATE_OWNER);
|
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const { jobId } = useParams();
|
const { jobId } = useParams();
|
||||||
@@ -129,24 +127,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "intake" && job.owner && job.owner.id) {
|
|
||||||
//Updae Owner Allow to Text
|
|
||||||
const updateOwnerResult = await updateOwner({
|
|
||||||
variables: {
|
|
||||||
ownerId: job.owner.id,
|
|
||||||
owner: { allow_text_message: values.allow_text_message }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!!updateOwnerResult.errors) {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("checklist.errors.complete", {
|
|
||||||
error: JSON.stringify(result.errors)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (!!!result.errors) {
|
if (!!!result.errors) {
|
||||||
@@ -189,7 +169,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
|||||||
initialValues={{
|
initialValues={{
|
||||||
...(type === "intake" && {
|
...(type === "intake" && {
|
||||||
addToProduction: true,
|
addToProduction: true,
|
||||||
allow_text_message: job.owner && job.owner.allow_text_message,
|
|
||||||
scheduled_completion:
|
scheduled_completion:
|
||||||
(job && job.scheduled_completion && dayjs(job.scheduled_completion)) ||
|
(job && job.scheduled_completion && dayjs(job.scheduled_completion)) ||
|
||||||
(job &&
|
(job &&
|
||||||
@@ -228,14 +207,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
|||||||
>
|
>
|
||||||
<Switch disabled={readOnly} />
|
<Switch disabled={readOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
|
||||||
name="allow_text_message"
|
|
||||||
valuePropName="checked"
|
|
||||||
label={t("checklist.labels.allow_text_message")}
|
|
||||||
disabled={readOnly}
|
|
||||||
>
|
|
||||||
<Switch disabled={readOnly} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="scheduled_completion"
|
name="scheduled_completion"
|
||||||
label={t("jobs.fields.scheduled_completion")}
|
label={t("jobs.fields.scheduled_completion")}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function JobEmployeeAssignments({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyTooltipOnHide content={popContent} open={visibility}>
|
<Popover destroyOnHidden content={popContent} open={visibility}>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<DataLabel label={t("jobs.fields.employee_body")}>
|
<DataLabel label={t("jobs.fields.employee_body")}>
|
||||||
{body ? (
|
{body ? (
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { DownloadOutlined, SyncOutlined } from "@ant-design/icons";
|
import { DownloadOutlined, SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Input, Space, Table } from "antd";
|
import { Button, Card, Input, Space, Table } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { 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";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
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 { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
partnerVersion: selectPartnerVersion
|
partnerVersion: selectPartnerVersion
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAvailableScan);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsAvailableScan);
|
||||||
@@ -126,6 +126,7 @@ export function JobsAvailableScan({ partnerVersion, refetch }) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
scanEstimates();
|
scanEstimates();
|
||||||
}}
|
}}
|
||||||
|
id="scan-estimates-button"
|
||||||
>
|
>
|
||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Form, Input, Switch } from "antd";
|
import { Form, Input } from "antd";
|
||||||
import React, { useContext } from "react";
|
import React, { useContext } 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";
|
||||||
@@ -129,13 +129,6 @@ export default function JobsCreateOwnerInfoNewComponent() {
|
|||||||
<Form.Item label={t("owners.fields.preferred_contact")} name={["owner", "data", "preferred_contact"]}>
|
<Form.Item label={t("owners.fields.preferred_contact")} name={["owner", "data", "preferred_contact"]}>
|
||||||
<Input disabled={!state.owner.new} />
|
<Input disabled={!state.owner.new} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
|
||||||
label={t("owners.fields.allow_text_message")}
|
|
||||||
valuePropName="checked"
|
|
||||||
name={["owner", "data", "allow_text_message"]}
|
|
||||||
>
|
|
||||||
<Switch disabled={!state.owner.new} />
|
|
||||||
</Form.Item>
|
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Card, Input, Table } from "antd";
|
import { Card, Input, Table } from "antd";
|
||||||
import React, { 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";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
@@ -91,6 +91,7 @@ export default function JobsCreateOwnerInfoSearchComponent({ loading, owners })
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
enterButton
|
enterButton
|
||||||
|
id="search-owner"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -112,9 +113,9 @@ export default function JobsCreateOwnerInfoSearchComponent({ loading, owners })
|
|||||||
type: "radio",
|
type: "radio",
|
||||||
selectedRowKeys: [state.owner.selectedid]
|
selectedRowKeys: [state.owner.selectedid]
|
||||||
}}
|
}}
|
||||||
onRow={(record, rowIndex) => {
|
onRow={(record) => {
|
||||||
return {
|
return {
|
||||||
onClick: (event) => {
|
onClick: () => {
|
||||||
if (record) {
|
if (record) {
|
||||||
if (record.id) {
|
if (record.id) {
|
||||||
setState({
|
setState({
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
|
|||||||
open={open}
|
open={open}
|
||||||
placement="left"
|
placement="left"
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
destroyTooltipOnHide
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<SearchOutlined style={{ cursor: "pointer" }} />
|
<SearchOutlined style={{ cursor: "pointer" }} />
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useContext, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Card, Input, Space, Table } from "antd";
|
import { Card, Input, Space, Table } from "antd";
|
||||||
|
import { useContext, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
|
||||||
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||||
|
|
||||||
export default function JobsCreateVehicleInfoSearchComponent({ loading, vehicles }) {
|
export default function JobsCreateVehicleInfoSearchComponent({ loading, vehicles }) {
|
||||||
@@ -63,6 +63,7 @@ export default function JobsCreateVehicleInfoSearchComponent({ loading, vehicles
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
enterButton
|
enterButton
|
||||||
|
id="search-vehicle"
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -91,9 +92,9 @@ export default function JobsCreateVehicleInfoSearchComponent({ loading, vehicles
|
|||||||
type: "radio",
|
type: "radio",
|
||||||
selectedRowKeys: [state.vehicle.selectedid]
|
selectedRowKeys: [state.vehicle.selectedid]
|
||||||
}}
|
}}
|
||||||
onRow={(record, rowIndex) => {
|
onRow={(record) => {
|
||||||
return {
|
return {
|
||||||
onClick: (event) => {
|
onClick: () => {
|
||||||
if (record) {
|
if (record) {
|
||||||
if (record.id) {
|
if (record.id) {
|
||||||
setState({
|
setState({
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Form, Statistic, Tooltip } from "antd";
|
import { Form, Statistic, Tooltip } from "antd";
|
||||||
import React, { useMemo } from "react";
|
import { useMemo } 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 { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import FormRow from "../layout-form-row/layout-form-row.component";
|
import FormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -43,14 +43,14 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
|
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
disabled={true}
|
disabled={jobRO}
|
||||||
value={job.estimate_sent_approval ? dayjs(job.estimate_sent_approval) : null}
|
value={job.estimate_sent_approval ? dayjs(job.estimate_sent_approval) : null}
|
||||||
placeholder={t("general.labels.na")}
|
placeholder={t("general.labels.na")}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.estimate_approved")} name="estimate_approved">
|
<Form.Item label={t("jobs.fields.estimate_approved")} name="estimate_approved">
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
disabled={true}
|
disabled={jobRO}
|
||||||
value={job.estimate_approved ? dayjs(job.estimate_approved) : null}
|
value={job.estimate_approved ? dayjs(job.estimate_approved) : null}
|
||||||
placeholder={t("general.labels.na")}
|
placeholder={t("general.labels.na")}
|
||||||
/>
|
/>
|
||||||
@@ -63,7 +63,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Tooltip title={t("jobs.labels.scheduledinchange")}>
|
<Tooltip title={t("jobs.labels.scheduledinchange")}>
|
||||||
<Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in">
|
<Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
||||||
@@ -110,16 +110,16 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow header={t("jobs.forms.admindates")}>
|
<FormRow header={t("jobs.forms.admindates")}>
|
||||||
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
|
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
|
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale">
|
<Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
||||||
@@ -32,7 +33,6 @@ import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
|||||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -673,7 +673,9 @@ export function JobsDetailHeaderActions({
|
|||||||
context: {
|
context: {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
job: job,
|
job: job,
|
||||||
alt_transport: job.alt_transport
|
alt_transport: job.alt_transport,
|
||||||
|
scheduled_in: job.scheduled_in,
|
||||||
|
scheduled_completion: job.scheduled_completion
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1078,17 +1080,22 @@ export function JobsDetailHeaderActions({
|
|||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "deletejob",
|
key: "deletejob",
|
||||||
id: "job-actions-deletejob",
|
id: "job-actions-deletejob",
|
||||||
label: (
|
label:
|
||||||
<Popconfirm
|
job.job_watchers.length === 0 ? (
|
||||||
title={t("jobs.labels.deleteconfirm")}
|
<Popconfirm
|
||||||
okText={t("general.labels.yes")}
|
title={t("jobs.labels.deleteconfirm")}
|
||||||
cancelText={t("general.labels.no")}
|
okText={t("general.labels.yes")}
|
||||||
onClick={(e) => e.stopPropagation()}
|
cancelText={t("general.labels.no")}
|
||||||
onConfirm={handleDeleteJob}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
onConfirm={handleDeleteJob}
|
||||||
{t("menus.jobsactions.deletejob")}
|
>
|
||||||
</Popconfirm>
|
{t("menus.jobsactions.deletejob")}
|
||||||
)
|
</Popconfirm>
|
||||||
|
) : (
|
||||||
|
<Popconfirm title={t("jobs.labels.deletewatchers")} onClick={(e) => e.stopPropagation()} showCancel={false}>
|
||||||
|
{t("menus.jobsactions.deletejob")}
|
||||||
|
</Popconfirm>
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1109,8 +1116,8 @@ export function JobsDetailHeaderActions({
|
|||||||
<RbacWrapper action="jobs:void" noauth>
|
<RbacWrapper action="jobs:void" noauth>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t("jobs.labels.voidjob")}
|
title={t("jobs.labels.voidjob")}
|
||||||
okText="Yes"
|
okText={t("general.labels.yes")}
|
||||||
cancelText="No"
|
cancelText={t("general.labels.no")}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onConfirm={handleVoidJob}
|
onConfirm={handleVoidJob}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -167,7 +167,18 @@ export function JobsDetailHeaderActionsToggleProduction({
|
|||||||
<FormDateTimePickerComponent disabled={jobRO} />
|
<FormDateTimePickerComponent disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name={["actual_delivery"]} label={t("jobs.fields.actual_delivery")}>
|
<Form.Item
|
||||||
|
name={["actual_delivery"]}
|
||||||
|
label={t("jobs.fields.actual_delivery")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: bodyshop.deliverchecklist.actual_delivery
|
||||||
|
? bodyshop.deliverchecklist.actual_delivery
|
||||||
|
: false
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
<FormDateTimePickerComponent disabled={jobRO} />
|
<FormDateTimePickerComponent disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled } from "@ant-design/icons";
|
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled } from "@ant-design/icons";
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
import { Card, Checkbox, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
|
import { Card, Checkbox, Col, Divider, Row, Space, Tag, Tooltip } 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";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useMutation } from "@apollo/client";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter, DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||||
@@ -24,7 +27,6 @@ import ProductionListColumnComment from "../production-list-columns/production-l
|
|||||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||||
import "./jobs-detail-header.styles.scss";
|
import "./jobs-detail-header.styles.scss";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -38,6 +40,14 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
context: context,
|
context: context,
|
||||||
modal: "printCenter"
|
modal: "printCenter"
|
||||||
})
|
})
|
||||||
|
),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,7 +59,7 @@ const colSpan = {
|
|||||||
xl: { span: 6 }
|
xl: { span: 6 }
|
||||||
};
|
};
|
||||||
|
|
||||||
export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { notification } = useNotification();
|
const { notification } = useNotification();
|
||||||
const [notesClamped, setNotesClamped] = useState(true);
|
const [notesClamped, setNotesClamped] = useState(true);
|
||||||
@@ -66,7 +76,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
const handleCheckboxChange = async (field, checked) => {
|
const handleCheckboxChange = async (field, checked) => {
|
||||||
const value = checked ? dayjs().toISOString() : null;
|
const value = checked ? dayjs().toISOString() : null;
|
||||||
try {
|
try {
|
||||||
await updateJob({
|
const ret = await updateJob({
|
||||||
variables: {
|
variables: {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
job: { [field]: value }
|
job: { [field]: value }
|
||||||
@@ -74,6 +84,16 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
refetchQueries: ["GET_JOB_BY_PK"],
|
refetchQueries: ["GET_JOB_BY_PK"],
|
||||||
awaitRefetchQueries: true
|
awaitRefetchQueries: true
|
||||||
});
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: job.id,
|
||||||
|
operation: AuditTrailMapping.jobfieldchange(
|
||||||
|
field,
|
||||||
|
ret.data.update_jobs.returning[0][field]
|
||||||
|
? DateTimeFormatterFunction(ret.data.update_jobs.returning[0][field])
|
||||||
|
: checked
|
||||||
|
),
|
||||||
|
type: "jobfieldchange"
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: t("jobs.errors.saving", { error: error.message })
|
message: t("jobs.errors.saving", { error: error.message })
|
||||||
@@ -315,7 +335,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...colSpan}>
|
<Col {...colSpan}>
|
||||||
<Card style={{ height: "100%" }} title={t("jobs.labels.employeeassignments")}>
|
<Card
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
title={<span id="job-employee-assignments-title">{t("jobs.labels.employeeassignments")}</span>}
|
||||||
|
id={"job-employee-assignments"}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<JobEmployeeAssignments job={job} />
|
<JobEmployeeAssignments job={job} />
|
||||||
<Divider style={{ margin: ".5rem" }} />
|
<Divider style={{ margin: ".5rem" }} />
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function JobsDocumentsContainer({
|
|||||||
variables: { jobId: jobId },
|
variables: { jobId: jobId },
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
skip: Imgproxy.treatment === "on" || !!billId
|
skip: !!billId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import axios from "axios";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import cleanAxios from "../../utils/CleanAxios";
|
|
||||||
import formatBytes from "../../utils/formatbytes";
|
import formatBytes from "../../utils/formatbytes";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -12,7 +11,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
|
||||||
|
|
||||||
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier, jobId }) {
|
export function JobsDocumentsImgproxyDownloadButton({ galleryImages, identifier, jobId }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [download, setDownload] = useState(null);
|
const [download, setDownload] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -46,32 +45,40 @@ export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, i
|
|||||||
}
|
}
|
||||||
|
|
||||||
function standardMediaDownload(bufferData) {
|
function standardMediaDownload(bufferData) {
|
||||||
const a = document.createElement("a");
|
try {
|
||||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||||
a.download = `${identifier || "documents"}.zip`;
|
a.href = url;
|
||||||
a.click();
|
a.download = `${identifier || "documents"}.zip`;
|
||||||
|
a.click();
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
setDownload(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
logImEXEvent("jobs_documents_download");
|
logImEXEvent("jobs_documents_download");
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const zipUrl = await axios({
|
try {
|
||||||
url: "/media/imgproxy/download",
|
const response = await axios({
|
||||||
method: "POST",
|
url: "/media/imgproxy/download",
|
||||||
data: { jobId, documentids: imagesToDownload.map((_) => _.id) }
|
method: "POST",
|
||||||
});
|
responseType: "blob",
|
||||||
|
data: { jobId, documentids: imagesToDownload.map((_) => _.id) },
|
||||||
|
onDownloadProgress: downloadProgress
|
||||||
|
});
|
||||||
|
|
||||||
const theDownloadedZip = await cleanAxios({
|
setLoading(false);
|
||||||
url: zipUrl.data.url,
|
setDownload(null);
|
||||||
method: "GET",
|
|
||||||
responseType: "arraybuffer",
|
|
||||||
onDownloadProgress: downloadProgress
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
setDownload(null);
|
|
||||||
|
|
||||||
standardMediaDownload(theDownloadedZip.data);
|
// Use the response data (Blob) to trigger download
|
||||||
|
standardMediaDownload(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
setDownload(null);
|
||||||
|
// handle error (optional)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -98,7 +98,13 @@ function JobsDocumentsImgproxyComponent({
|
|||||||
jobId={jobId}
|
jobId={jobId}
|
||||||
totalSize={totalSize}
|
totalSize={totalSize}
|
||||||
billId={billId}
|
billId={billId}
|
||||||
callbackAfterUpload={billsCallback || fetchThumbnails || refetch}
|
callbackAfterUpload={
|
||||||
|
billsCallback ||
|
||||||
|
function () {
|
||||||
|
isFunction(refetch) && refetch();
|
||||||
|
isFunction(fetchThumbnails) && fetchThumbnails();
|
||||||
|
}
|
||||||
|
}
|
||||||
ignoreSizeLimit={ignoreSizeLimit}
|
ignoreSizeLimit={ignoreSizeLimit}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Checkbox, Divider, Input, Space, Table } from "antd";
|
import { Button, Checkbox, Divider, Input, Space, Table } from "antd";
|
||||||
import dayjs from "../../utils/day";
|
import { useState } from "react";
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
@@ -223,9 +223,9 @@ export default function JobsFindModalComponent({
|
|||||||
type: "radio",
|
type: "radio",
|
||||||
selectedRowKeys: [selectedJob]
|
selectedRowKeys: [selectedJob]
|
||||||
}}
|
}}
|
||||||
onRow={(record, rowIndex) => {
|
onRow={(record) => {
|
||||||
return {
|
return {
|
||||||
onClick: (event) => {
|
onClick: () => {
|
||||||
handleOnRowClick(record);
|
handleOnRowClick(record);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -241,15 +241,17 @@ export default function JobsFindModalComponent({
|
|||||||
overrideHeaders: e.target.checked
|
overrideHeaders: e.target.checked
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
id="override_header"
|
||||||
>
|
>
|
||||||
{t("jobs.labels.override_header")}
|
{t("jobs.labels.override_header")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox checked={partsQueueToggle} onChange={(e) => setPartsQueueToggle(e.target.checked)}>
|
<Checkbox checked={partsQueueToggle} onChange={(e) => setPartsQueueToggle(e.target.checked)} id="parts_queue_toggle">
|
||||||
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
|
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={updateSchComp.checked}
|
checked={updateSchComp.checked}
|
||||||
onChange={(e) => setSchComp({ ...updateSchComp, checked: e.target.checked })}
|
onChange={(e) => setSchComp({ ...updateSchComp, checked: e.target.checked })}
|
||||||
|
id="update_scheduled_completion"
|
||||||
>
|
>
|
||||||
{t("jobs.labels.update_scheduled_completion")}
|
{t("jobs.labels.update_scheduled_completion")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
@@ -261,6 +263,7 @@ export default function JobsFindModalComponent({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSchComp({ ...updateSchComp, scheduled_completion: e });
|
setSchComp({ ...updateSchComp, scheduled_completion: e });
|
||||||
}}
|
}}
|
||||||
|
id="scheduled_completion_date_time_picker"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -273,6 +276,7 @@ export default function JobsFindModalComponent({
|
|||||||
automatic: true
|
automatic: true
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
id="calculate_scheduled_completion"
|
||||||
>
|
>
|
||||||
{t("jobs.labels.calc_scheuled_completion")}
|
{t("jobs.labels.calc_scheuled_completion")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { Modal } from "antd";
|
import { Modal } from "antd";
|
||||||
import React 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";
|
||||||
@@ -66,7 +65,7 @@ export default connect(
|
|||||||
title={t("jobs.labels.existing_jobs")}
|
title={t("jobs.labels.existing_jobs")}
|
||||||
width={"80%"}
|
width={"80%"}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
okButtonProps={{ disabled: selectedJob ? false : true }}
|
okButtonProps={{ disabled: selectedJob ? false : true, id: "jobs-find-modal-container-ok" }}
|
||||||
{...modalProps}
|
{...modalProps}
|
||||||
>
|
>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button, Card, Input, Space, Table, Typography } from "antd";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -20,7 +20,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,6 +203,8 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
id="all-jobs-list"
|
||||||
|
title={t("titles.bc.jobs-all")}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{search.search && (
|
{search.search && (
|
||||||
@@ -256,6 +258,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
|||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={search?.search ? openSearchResults : jobs}
|
dataSource={search?.search ? openSearchResults : jobs}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
id="all-jobs-list-table"
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, SyncOut
|
|||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";
|
import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { 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";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -22,7 +22,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({});
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
export function JobsList({ bodyshop }) {
|
export function JobsList({ bodyshop }) {
|
||||||
const searchParams = queryString.parse(useLocation().search);
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
@@ -342,13 +342,14 @@ export function JobsList({ bodyshop }) {
|
|||||||
type: "radio"
|
type: "radio"
|
||||||
}}
|
}}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
onRow={(record, rowIndex) => {
|
onRow={(record) => {
|
||||||
return {
|
return {
|
||||||
onClick: (event) => {
|
onClick: () => {
|
||||||
handleOnRowClick(record);
|
handleOnRowClick(record);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
|
id="active-jobs-list-table"
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -131,4 +131,6 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
NotificationCenterComponent.displayName = "NotificationCenterComponent";
|
||||||
|
|
||||||
export default NotificationCenterComponent;
|
export default NotificationCenterComponent;
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Form, Input, Switch } from "antd";
|
import { Form, Input, Tooltip } from "antd";
|
||||||
import React from "react";
|
import { CloseCircleFilled } from "@ant-design/icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
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 FormItemEmail from "../form-items-formatted/email-form-item.component";
|
import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
||||||
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
import { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||||
|
|
||||||
export default function OwnerDetailFormComponent({ form, loading }) {
|
export default function OwnerDetailFormComponent({ form, loading, isPhone1OptedOut, isPhone2OptedOut }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getFieldValue } = form;
|
const { getFieldValue } = form;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormFieldsChanged form={form} />
|
<FormFieldsChanged form={form} />
|
||||||
@@ -26,7 +27,7 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("owners.fields.accountingid")} name="accountingid">
|
<Form.Item label={t("owners.fields.accountingid")} name="accountingid">
|
||||||
<Input disabled/>
|
<Input disabled />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("owners.forms.address")}>
|
<LayoutFormRow header={t("owners.forms.address")}>
|
||||||
@@ -50,9 +51,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("owners.forms.contact")}>
|
<LayoutFormRow header={t("owners.forms.contact")}>
|
||||||
<Form.Item label={t("owners.fields.allow_text_message")} name="allow_text_message" valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("owners.fields.ownr_ea")}
|
label={t("owners.fields.ownr_ea")}
|
||||||
name="ownr_ea"
|
name="ownr_ea"
|
||||||
@@ -65,19 +63,55 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
>
|
>
|
||||||
<FormItemEmail email={getFieldValue("ownr_ea")} />
|
<FormItemEmail email={getFieldValue("ownr_ea")} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("owners.fields.ownr_ph1")} style={{ marginBottom: 0 }}>
|
||||||
label={t("owners.fields.ownr_ph1")}
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
name="ownr_ph1"
|
<Form.Item
|
||||||
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]}
|
name="ownr_ph1"
|
||||||
>
|
noStyle
|
||||||
<FormItemPhone />
|
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]}
|
||||||
|
>
|
||||||
|
<Input style={{ flex: 1, minWidth: "150px" }} />
|
||||||
|
</Form.Item>
|
||||||
|
{isPhone1OptedOut && (
|
||||||
|
<Tooltip title={t("consent.text_body")}>
|
||||||
|
<CloseCircleFilled
|
||||||
|
style={{
|
||||||
|
color: "#ff4d4f",
|
||||||
|
fontSize: 16,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: "100%"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("owners.fields.ownr_ph2")} style={{ marginBottom: 0 }}>
|
||||||
label={t("owners.fields.ownr_ph2")}
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
name="ownr_ph2"
|
<Form.Item
|
||||||
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]}
|
name="ownr_ph2"
|
||||||
>
|
noStyle
|
||||||
<FormItemPhone />
|
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]}
|
||||||
|
>
|
||||||
|
<Input style={{ flex: 1, minWidth: "150px" }} />
|
||||||
|
</Form.Item>
|
||||||
|
{isPhone2OptedOut && (
|
||||||
|
<Tooltip title={t("consent.text_body")}>
|
||||||
|
<CloseCircleFilled
|
||||||
|
style={{
|
||||||
|
color: "#ff4d4f",
|
||||||
|
fontSize: 16,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: "100%"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("owners.fields.preferred_contact")} name="preferred_contact">
|
<Form.Item label={t("owners.fields.preferred_contact")} name="preferred_contact">
|
||||||
<Input />
|
<Input />
|
||||||
|
|||||||
@@ -1,69 +1,115 @@
|
|||||||
import { Button, Form, Popconfirm } from "antd";
|
import { Button, Form, Popconfirm } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useApolloClient, useMutation } from "@apollo/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries";
|
import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors"; // Adjust path
|
||||||
|
import { phoneNumberOptOutService } from "../../utils/phoneOptOutService.js"; // Adjust path
|
||||||
import OwnerDetailFormComponent from "./owner-detail-form.component";
|
import OwnerDetailFormComponent from "./owner-detail-form.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { phone } from "phone"; // Import phone utility for formatting
|
||||||
|
|
||||||
function OwnerDetailFormContainer({ owner, refetch }) {
|
// Connect to Redux to access bodyshop
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
function OwnerDetailFormContainer({ owner, refetch, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const history = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [optedOutPhones, setOptedOutPhones] = useState(new Set());
|
||||||
const [updateOwner] = useMutation(UPDATE_OWNER);
|
const [updateOwner] = useMutation(UPDATE_OWNER);
|
||||||
const [deleteOwner] = useMutation(DELETE_OWNER);
|
const [deleteOwner] = useMutation(DELETE_OWNER);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
|
// Fetch opt-out status on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchOptOutStatus = async () => {
|
||||||
|
if (bodyshop?.id && bodyshop?.messagingservicesid && (owner?.ownr_ph1 || owner?.ownr_ph2)) {
|
||||||
|
const phoneNumbers = [owner.ownr_ph1, owner.ownr_ph2].filter(Boolean);
|
||||||
|
const optOutSet = await phoneNumberOptOutService(apolloClient, bodyshop.id, phoneNumbers);
|
||||||
|
setOptedOutPhones(optOutSet);
|
||||||
|
} else {
|
||||||
|
setOptedOutPhones(new Set());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchOptOutStatus();
|
||||||
|
}, [apolloClient, bodyshop?.id, bodyshop?.messagingservicesid, owner?.ownr_ph1, owner?.ownr_ph2]);
|
||||||
|
|
||||||
|
// Reset form fields when owner changes
|
||||||
|
useEffect(() => {
|
||||||
|
form.setFieldsValue({
|
||||||
|
ownr_ph1: owner?.ownr_ph1,
|
||||||
|
ownr_ph2: owner?.ownr_ph2,
|
||||||
|
...owner
|
||||||
|
});
|
||||||
|
}, [owner, form]);
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await deleteOwner({
|
try {
|
||||||
variables: { id: owner.id }
|
const result = await deleteOwner({
|
||||||
});
|
variables: { id: owner.id }
|
||||||
console.log(result);
|
});
|
||||||
if (result.errors) {
|
if (result.errors) {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
|
message: t("owners.errors.deleting", {
|
||||||
|
error: JSON.stringify(result.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification.success({
|
||||||
|
message: t("owners.successes.delete")
|
||||||
|
});
|
||||||
|
navigate(`/manage/owners`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
message: t("owners.errors.deleting", {
|
message: t("owners.errors.deleting", {
|
||||||
error: JSON.stringify(result.errors)
|
error: error.message
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else {
|
|
||||||
notification["success"]({
|
|
||||||
message: t("owners.successes.delete")
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
history(`/manage/owners`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await updateOwner({
|
try {
|
||||||
variables: { ownerId: owner.id, owner: values }
|
const result = await updateOwner({
|
||||||
});
|
variables: { ownerId: owner.id, owner: values }
|
||||||
|
});
|
||||||
if (!!result.errors) {
|
if (result.errors) {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
|
message: t("owners.errors.saving", {
|
||||||
|
error: JSON.stringify(result.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification.success({
|
||||||
|
message: t("owners.successes.save")
|
||||||
|
});
|
||||||
|
if (refetch) await refetch();
|
||||||
|
form.resetFields();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
message: t("owners.errors.saving", {
|
message: t("owners.errors.saving", {
|
||||||
error: JSON.stringify(result.errors)
|
error: error.message
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notification["success"]({
|
|
||||||
message: t("owners.successes.save")
|
|
||||||
});
|
|
||||||
|
|
||||||
if (refetch) await refetch();
|
|
||||||
form.resetFields();
|
|
||||||
form.resetFields();
|
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -72,6 +118,7 @@ function OwnerDetailFormContainer({ owner, refetch }) {
|
|||||||
title={t("menus.header.owners")}
|
title={t("menus.header.owners")}
|
||||||
extra={[
|
extra={[
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
|
key="delete"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
disabled={owner.jobs.length !== 0}
|
disabled={owner.jobs.length !== 0}
|
||||||
@@ -81,16 +128,29 @@ function OwnerDetailFormContainer({ owner, refetch }) {
|
|||||||
{t("general.actions.delete")}
|
{t("general.actions.delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
<Button type="primary" loading={loading} onClick={() => form.submit()}>
|
<Button key="save" type="primary" loading={loading} onClick={() => form.submit()}>
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Form form={form} onFinish={handleFinish} autoComplete="off" layout="vertical" initialValues={owner}>
|
<Form form={form} onFinish={handleFinish} autoComplete="off" layout="vertical" initialValues={owner}>
|
||||||
<OwnerDetailFormComponent loading={loading} form={form} />
|
<OwnerDetailFormComponent
|
||||||
|
loading={loading}
|
||||||
|
form={form}
|
||||||
|
isPhone1OptedOut={
|
||||||
|
bodyshop?.messagingservicesid &&
|
||||||
|
owner?.ownr_ph1 &&
|
||||||
|
optedOutPhones.has(phone(owner.ownr_ph1, "CA").phoneNumber?.replace(/^\+1/, ""))
|
||||||
|
}
|
||||||
|
isPhone2OptedOut={
|
||||||
|
bodyshop?.messagingservicesid &&
|
||||||
|
owner?.ownr_ph2 &&
|
||||||
|
optedOutPhones.has(phone(owner.ownr_ph2, "CA").phoneNumber?.replace(/^\+1/, ""))
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OwnerDetailFormContainer;
|
export default connect(mapStateToProps)(OwnerDetailFormContainer);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useLazyQuery } from "@apollo/client";
|
import { useLazyQuery } from "@apollo/client";
|
||||||
import { Input, Modal } from "antd";
|
import { Input, Modal } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { QUERY_SEARCH_OWNER_BY_IDX } from "../../graphql/owners.queries";
|
import { QUERY_SEARCH_OWNER_BY_IDX } from "../../graphql/owners.queries";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import OwnerFindModalComponent from "./owner-find-modal.component";
|
|
||||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
|
import OwnerFindModalComponent from "./owner-find-modal.component";
|
||||||
|
|
||||||
export default function OwnerFindModalContainer({
|
export default function OwnerFindModalContainer({
|
||||||
loading,
|
loading,
|
||||||
@@ -41,6 +41,7 @@ export default function OwnerFindModalContainer({
|
|||||||
<Modal
|
<Modal
|
||||||
title={<span id="owner-find-modal-title">{t("owners.labels.existing_owners")}</span>}
|
title={<span id="owner-find-modal-title">{t("owners.labels.existing_owners")}</span>}
|
||||||
width={"80%"}
|
width={"80%"}
|
||||||
|
okButtonProps={{ id: "owner-find-modal-ok-button" }}
|
||||||
{...modalProps}
|
{...modalProps}
|
||||||
>
|
>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function PartsOrderBackorderEta({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
|
<Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
|
||||||
<DateFormatter>{backordered_eta}</DateFormatter>
|
<DateFormatter>{backordered_eta}</DateFormatter>
|
||||||
{isAlreadyBackordered && <CalendarFilled style={{ cursor: "pointer" }} onClick={handlePopover} />}
|
{isAlreadyBackordered && <CalendarFilled style={{ cursor: "pointer" }} onClick={handlePopover} />}
|
||||||
{loading && <Spin />}
|
{loading && <Spin />}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function PartsOrderLineBackorderButton({ partsOrderStatus, partsLineId, j
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
|
<Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
|
||||||
<Button loading={loading} onClick={handlePopover}>
|
<Button loading={loading} onClick={handlePopover}>
|
||||||
{isAlreadyBackordered ? t("parts_orders.actions.receive") : t("parts_orders.actions.backordered")}
|
{isAlreadyBackordered ? t("parts_orders.actions.receive") : t("parts_orders.actions.backordered")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { Input, Table } from "antd";
|
import { Input, Table, Typography } from "antd";
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries";
|
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries";
|
||||||
|
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
|
||||||
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||||
|
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const { Paragraph } = Typography;
|
||||||
|
|
||||||
|
// Commented out Associated Owners section for now
|
||||||
|
//import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
//import { Link } from "react-router-dom";
|
||||||
|
//import { useMemo, useState } from "react";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -20,18 +26,95 @@ const mapDispatchToProps = () => ({});
|
|||||||
function PhoneNumberConsentList({ bodyshop, currentUser }) {
|
function PhoneNumberConsentList({ bodyshop, currentUser }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const { loading, data } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
|
|
||||||
|
// Fetch opt-out phone numbers
|
||||||
|
const { loading: optOutLoading, data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
|
||||||
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined },
|
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined },
|
||||||
fetchPolicy: "network-only"
|
fetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Commented out Associated Owners section for now
|
||||||
|
/*// Prepare phone numbers for owner query
|
||||||
|
const phoneNumbers = useMemo(() => {
|
||||||
|
return optOutData?.phone_number_opt_out?.map((item) => item.phone_number) || [];
|
||||||
|
}, [optOutData?.phone_number_opt_out]);
|
||||||
|
const allPhoneNumbers = useMemo(() => {
|
||||||
|
const normalized = phoneNumbers;
|
||||||
|
const withPlusOne = phoneNumbers.map((num) => `+1${num}`);
|
||||||
|
return [...normalized, ...withPlusOne].filter(Boolean);
|
||||||
|
}, [phoneNumbers]);
|
||||||
|
|
||||||
|
// Fetch owners for all phone numbers
|
||||||
|
const { loading: ownersLoading, data: ownersData } = useQuery(SEARCH_OWNERS_BY_PHONE_NUMBERS, {
|
||||||
|
variables: { bodyshopid: bodyshop.id, phone_numbers: allPhoneNumbers },
|
||||||
|
skip: allPhoneNumbers.length === 0 || !bodyshop.id,
|
||||||
|
fetchPolicy: "network-only"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map phone numbers to their associated owners and identify phone field
|
||||||
|
const getAssociatedOwners = (phoneNumber) => {
|
||||||
|
if (!ownersData?.owners) return [];
|
||||||
|
const normalizedPhone = phoneNumber.replace(/^\+1/, "");
|
||||||
|
return ownersData.owners
|
||||||
|
.filter(
|
||||||
|
(owner) =>
|
||||||
|
owner.ownr_ph1 === phoneNumber ||
|
||||||
|
owner.ownr_ph2 === phoneNumber ||
|
||||||
|
owner.ownr_ph1 === normalizedPhone ||
|
||||||
|
owner.ownr_ph2 === normalizedPhone ||
|
||||||
|
owner.ownr_ph1 === `+1${phoneNumber}` ||
|
||||||
|
owner.ownr_ph2 === `+1${phoneNumber}`
|
||||||
|
)
|
||||||
|
.map((owner) => ({
|
||||||
|
...owner,
|
||||||
|
phoneField:
|
||||||
|
[owner.ownr_ph1, owner.ownr_ph2].includes(phoneNumber) ||
|
||||||
|
[owner.ownr_ph1, owner.ownr_ph2].includes(normalizedPhone) ||
|
||||||
|
[owner.ownr_ph1, owner.ownr_ph2].includes(`+1${phoneNumber}`)
|
||||||
|
? owner.ownr_ph1 === phoneNumber ||
|
||||||
|
owner.ownr_ph1 === normalizedPhone ||
|
||||||
|
owner.ownr_ph1 === `+1${phoneNumber}`
|
||||||
|
? t("consent.phone_1")
|
||||||
|
: t("consent.phone_2")
|
||||||
|
: null
|
||||||
|
}));
|
||||||
|
};*/
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t("consent.phone_number"),
|
title: t("consent.phone_number"),
|
||||||
dataIndex: "phone_number",
|
dataIndex: "phone_number",
|
||||||
render: (text) => <PhoneNumberFormatter>{text}</PhoneNumberFormatter>,
|
render: (text) => <ChatOpenButton phone={text} />,
|
||||||
sorter: (a, b) => a.phone_number.localeCompare(b.phone_number)
|
sorter: (a, b) => a.phone_number.localeCompare(b.phone_number)
|
||||||
},
|
},
|
||||||
|
// Commented out Associated Owners section for now
|
||||||
|
/*{
|
||||||
|
title: t("consent.associated_owners"),
|
||||||
|
dataIndex: "phone_number",
|
||||||
|
render: (phoneNumber) => {
|
||||||
|
const owners = getAssociatedOwners(phoneNumber);
|
||||||
|
if (!owners || owners.length === 0) {
|
||||||
|
return t("consent.no_owners");
|
||||||
|
}
|
||||||
|
return owners.map((owner) => (
|
||||||
|
<div key={owner.id}>
|
||||||
|
<Space direction="horizontal">
|
||||||
|
<Link to={"/manage/owners/" + owner.id}>
|
||||||
|
<OwnerNameDisplay ownerObject={owner} />
|
||||||
|
</Link>
|
||||||
|
({owner.phoneField})
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
},
|
||||||
|
sorter: (a, b) => {
|
||||||
|
const aOwners = getAssociatedOwners(a.phone_number);
|
||||||
|
const bOwners = getAssociatedOwners(b.phone_number);
|
||||||
|
const aName = aOwners[0] ? `${aOwners[0].ownr_fn} ${aOwners[0].ownr_ln}` : "";
|
||||||
|
const bName = bOwners[0] ? `${bOwners[0].ownr_fn} ${bOwners[0].ownr_ln}` : "";
|
||||||
|
return aName.localeCompare(bName);
|
||||||
|
}
|
||||||
|
},*/
|
||||||
{
|
{
|
||||||
title: t("consent.created_at"),
|
title: t("consent.created_at"),
|
||||||
dataIndex: "created_at",
|
dataIndex: "created_at",
|
||||||
@@ -42,6 +125,7 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<Paragraph>{t("consent.text_body")}</Paragraph>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
onSearch={(value) => setSearch(value)}
|
onSearch={(value) => setSearch(value)}
|
||||||
@@ -50,8 +134,8 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
|
|||||||
|
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data?.phone_number_opt_out}
|
dataSource={optOutData?.phone_number_opt_out}
|
||||||
loading={loading}
|
loading={optOutLoading /* || ownersLoading*/}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
style={{ marginTop: 16 }}
|
style={{ marginTop: 16 }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const Board = ({ id, className, orientation, cardSettings, ...additionalProps })
|
|||||||
default:
|
default:
|
||||||
return cardSizesVertical.small;
|
return cardSizesVertical.small;
|
||||||
}
|
}
|
||||||
}, [cardSettings]);
|
}, [cardSettings?.cardSize]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -101,11 +101,33 @@ const BoardContainer = ({
|
|||||||
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
|
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
|
|
||||||
// Only update drag time if it's a valid drop with a different destination
|
// Validate drag type and source
|
||||||
if (type === "lane" && source && destination && !isEqual(source, destination)) {
|
if (type !== "lane" || !source) {
|
||||||
setDragTime(source.droppableId);
|
// Invalid drag type or missing source, attempt to revert if possible
|
||||||
setIsProcessing(true);
|
if (source) {
|
||||||
|
dispatch(
|
||||||
|
actions.moveCardAcrossLanes({
|
||||||
|
fromLaneId: source.droppableId,
|
||||||
|
toLaneId: source.droppableId,
|
||||||
|
cardId: draggableId,
|
||||||
|
index: source.index
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setIsProcessing(false);
|
||||||
|
try {
|
||||||
|
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in onLaneDrag for invalid drag type or source", err);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragTime(source.droppableId);
|
||||||
|
setIsProcessing(true);
|
||||||
|
|
||||||
|
// Handle valid drop to a different lane or position
|
||||||
|
if (destination && !isEqual(source, destination)) {
|
||||||
dispatch(
|
dispatch(
|
||||||
actions.moveCardAcrossLanes({
|
actions.moveCardAcrossLanes({
|
||||||
fromLaneId: source.droppableId,
|
fromLaneId: source.droppableId,
|
||||||
@@ -114,14 +136,33 @@ const BoardContainer = ({
|
|||||||
index: destination.index
|
index: destination.index
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// Same-lane drop or no destination, revert to original position
|
||||||
|
dispatch(
|
||||||
|
actions.moveCardAcrossLanes({
|
||||||
|
fromLaneId: source.droppableId,
|
||||||
|
toLaneId: source.droppableId,
|
||||||
|
cardId: draggableId,
|
||||||
|
index: source.index
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error in onLaneDrag", err);
|
console.error("Error in onLaneDrag", err);
|
||||||
} finally {
|
// Ensure revert on error
|
||||||
setIsProcessing(false);
|
dispatch(
|
||||||
}
|
actions.moveCardAcrossLanes({
|
||||||
|
fromLaneId: source.droppableId,
|
||||||
|
toLaneId: source.droppableId,
|
||||||
|
cardId: draggableId,
|
||||||
|
index: source.index
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, onDragEnd, setDragTime]
|
[dispatch, onDragEnd, setDragTime]
|
||||||
|
|||||||
@@ -133,7 +133,9 @@ const Lane = ({
|
|||||||
Item: ItemComponent
|
Item: ItemComponent
|
||||||
},
|
},
|
||||||
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>,
|
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>,
|
||||||
overscan: { main: 10, reverse: 10 }
|
overscan: { main: 10, reverse: 10 },
|
||||||
|
// Ensure a minimum height for empty lanes to allow dropping
|
||||||
|
style: renderedCards.length === 0 ? { minHeight: "5px" } : {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const horizontalProps = {
|
const horizontalProps = {
|
||||||
@@ -149,8 +151,6 @@ const Lane = ({
|
|||||||
|
|
||||||
const componentProps = orientation === "vertical" ? verticalProps : horizontalProps;
|
const componentProps = orientation === "vertical" ? verticalProps : horizontalProps;
|
||||||
|
|
||||||
// If the lane is collapsed, we want to render a div instead of the virtualized list, and we want to set the height to the max height of the lane so that
|
|
||||||
// the lane doesn't shrink when collapsed (in horizontal mode)
|
|
||||||
const finalComponentProps = collapsed
|
const finalComponentProps = collapsed
|
||||||
? orientation === "horizontal"
|
? orientation === "horizontal"
|
||||||
? {
|
? {
|
||||||
@@ -161,9 +161,8 @@ const Lane = ({
|
|||||||
: {}
|
: {}
|
||||||
: componentProps;
|
: componentProps;
|
||||||
|
|
||||||
// If the lane is horizontal and collapsed, we want to render a placeholder so that the lane doesn't shrink to 0 height and grows when
|
// Always render placeholder for empty lanes in vertical mode to ensure droppable area
|
||||||
// a card is dragged over it
|
const shouldRenderPlaceholder = orientation === "vertical" ? collapsed || renderedCards.length === 0 : collapsed;
|
||||||
const shouldRenderPlaceholder = orientation !== "horizontal" && (collapsed || renderedCards.length === 0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeightMemoryWrapper
|
<HeightMemoryWrapper
|
||||||
@@ -178,8 +177,8 @@ const Lane = ({
|
|||||||
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
|
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={laneRef} // Ensure laneRef is set here
|
ref={laneRef}
|
||||||
style={{ height: "100%", width: "100%" }} // Make it scrollable
|
style={{ height: "100%", width: "100%" }}
|
||||||
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
|
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
|
||||||
>
|
>
|
||||||
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...provided.droppableProps.style }}>
|
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...provided.droppableProps.style }}>
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
|||||||
if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]);
|
if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyTooltipOnHide content={popContent} open={visibility}>
|
<Popover destroyOnHidden content={popContent} open={visibility}>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
{record[type] ? (
|
{record[type] ? (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Icon from "@ant-design/icons";
|
import Icon from "@ant-design/icons";
|
||||||
import { Card, Popover, Space } from "antd";
|
import { Card, Popover, Space } from "antd";
|
||||||
import _ from "lodash";
|
import { groupBy } from "lodash";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
|
|
||||||
import React, { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MdFileDownload, MdFileUpload } from "react-icons/md";
|
import { MdFileDownload, MdFileUpload } from "react-icons/md";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -26,21 +26,12 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
calculating: selectScheduleLoadCalculating
|
calculating: selectScheduleLoadCalculating
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({});
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
export function ScheduleCalendarHeaderComponent({
|
export function ScheduleCalendarHeaderComponent({ bodyshop, label, refetch, date, load, calculating, events }) {
|
||||||
bodyshop,
|
|
||||||
label,
|
|
||||||
refetch,
|
|
||||||
date,
|
|
||||||
load,
|
|
||||||
calculating,
|
|
||||||
events,
|
|
||||||
...otherProps
|
|
||||||
}) {
|
|
||||||
const ATSToday = useMemo(() => {
|
const ATSToday = useMemo(() => {
|
||||||
if (!events) return [];
|
if (!events) return [];
|
||||||
return _.groupBy(
|
return groupBy(
|
||||||
events.filter((e) => !e.vacation && e.isintake && dayjs(date).isSame(dayjs(e.start), "day")),
|
events.filter((e) => !e.vacation && e.isintake && dayjs(date).isSame(dayjs(e.start), "day")),
|
||||||
"job.alt_transport"
|
"job.alt_transport"
|
||||||
);
|
);
|
||||||
@@ -155,7 +146,11 @@ export function ScheduleCalendarHeaderComponent({
|
|||||||
<Space size="small">
|
<Space size="small">
|
||||||
<Icon component={MdFileDownload} style={{ color: "green" }} />
|
<Icon component={MdFileDownload} style={{ color: "green" }} />
|
||||||
<BlurWrapper featureName="smartscheduling">
|
<BlurWrapper featureName="smartscheduling">
|
||||||
<span>{`${(loadData.allHoursInBody || 0) && loadData.allHoursInBody.toFixed(1)}/${(loadData.allHoursInRefinish || 0) && loadData.allHoursInRefinish.toFixed(1)}/${(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(1)}`}</span>
|
<span>
|
||||||
|
{`${(loadData.allHoursInBody || 0) && loadData.allHoursInBody.toFixed(1)}/${
|
||||||
|
(loadData.allHoursInRefinish || 0) && loadData.allHoursInRefinish.toFixed(1)
|
||||||
|
}/${(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(1)}`}
|
||||||
|
</span>
|
||||||
</BlurWrapper>
|
</BlurWrapper>
|
||||||
</Space>
|
</Space>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { Form, Modal } from "antd";
|
import { Form, Modal } from "antd";
|
||||||
import dayjs from "../../utils/day";
|
import { useEffect, useState } from "react";
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import {
|
import {
|
||||||
CANCEL_APPOINTMENT_BY_ID,
|
CANCEL_APPOINTMENT_BY_ID,
|
||||||
@@ -19,9 +19,9 @@ import { selectSchedule } from "../../redux/modals/modals.selectors";
|
|||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import { DateTimeFormat } from "../../utils/DateFormatter";
|
import { DateTimeFormat } from "../../utils/DateFormatter";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import ScheduleJobModalComponent from "./schedule-job-modal.component";
|
import ScheduleJobModalComponent from "./schedule-job-modal.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -72,7 +72,7 @@ export function ScheduleJobModalContainer({
|
|||||||
variables: { jobid: jobId },
|
variables: { jobid: jobId },
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
skip: !open || !!!jobId
|
skip: !open || !jobId
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -93,12 +93,12 @@ export function ScheduleJobModalContainer({
|
|||||||
logImEXEvent("schedule_new_appointment");
|
logImEXEvent("schedule_new_appointment");
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (!!previousEvent) {
|
if (previousEvent) {
|
||||||
const cancelAppt = await cancelAppointment({
|
const cancelAppt = await cancelAppointment({
|
||||||
variables: { appid: previousEvent }
|
variables: { appid: previousEvent }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!!cancelAppt.errors) {
|
if (cancelAppt.errors) {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("appointments.errors.canceling", {
|
message: t("appointments.errors.canceling", {
|
||||||
message: JSON.stringify(cancelAppt.errors)
|
message: JSON.stringify(cancelAppt.errors)
|
||||||
@@ -146,7 +146,7 @@ export function ScheduleJobModalContainer({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!appt.errors) {
|
if (appt.errors) {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("appointments.errors.saving", {
|
message: t("appointments.errors.saving", {
|
||||||
message: JSON.stringify(appt.errors)
|
message: JSON.stringify(appt.errors)
|
||||||
@@ -172,7 +172,7 @@ export function ScheduleJobModalContainer({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!!jobUpdate.errors) {
|
if (jobUpdate.errors) {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("appointments.errors.saving", {
|
message: t("appointments.errors.saving", {
|
||||||
message: JSON.stringify(jobUpdate.errors)
|
message: JSON.stringify(jobUpdate.errors)
|
||||||
@@ -222,9 +222,9 @@ export function ScheduleJobModalContainer({
|
|||||||
initialValues={{
|
initialValues={{
|
||||||
notifyCustomer: !!(job && job.ownr_ea),
|
notifyCustomer: !!(job && job.ownr_ea),
|
||||||
email: (job && job.ownr_ea) || "",
|
email: (job && job.ownr_ea) || "",
|
||||||
start: null,
|
|
||||||
// smartDates: [],
|
// smartDates: [],
|
||||||
scheduled_completion: null,
|
start: context.scheduled_in,
|
||||||
|
scheduled_completion: context.scheduled_completion ,
|
||||||
color: context.color,
|
color: context.color,
|
||||||
alt_transport: context.alt_transport,
|
alt_transport: context.alt_transport,
|
||||||
note: context.note
|
note: context.note
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Card } from "antd";
|
import { Card } from "antd";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import _ from "lodash";
|
import { round } from "lodash";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
@@ -29,7 +28,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardChart);
|
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardChart);
|
||||||
@@ -40,7 +39,7 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
|
|||||||
const data = listOfBusDays.reduce((acc, val) => {
|
const data = listOfBusDays.reduce((acc, val) => {
|
||||||
//Sum up the current day.
|
//Sum up the current day.
|
||||||
let dayhrs;
|
let dayhrs;
|
||||||
if (!!sbEntriesByDate[val]) {
|
if (sbEntriesByDate[val]) {
|
||||||
dayhrs = sbEntriesByDate[val].reduce(
|
dayhrs = sbEntriesByDate[val].reduce(
|
||||||
(dayAcc, dayVal) => {
|
(dayAcc, dayVal) => {
|
||||||
return {
|
return {
|
||||||
@@ -61,9 +60,9 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
|
|||||||
|
|
||||||
const theValue = {
|
const theValue = {
|
||||||
date: dayjs(val).format("D ddd"),
|
date: dayjs(val).format("D ddd"),
|
||||||
paintHrs: _.round(dayhrs.painthrs, 1),
|
paintHrs: round(dayhrs.painthrs, 1),
|
||||||
bodyHrs: _.round(dayhrs.bodyhrs, 1),
|
bodyHrs: round(dayhrs.bodyhrs, 1),
|
||||||
accTargetHrs: _.round(
|
accTargetHrs: round(
|
||||||
Utils.AsOfDateTargetHours(
|
Utils.AsOfDateTargetHours(
|
||||||
bodyshop.scoreboard_target.dailyBodyTarget + bodyshop.scoreboard_target.dailyPaintTarget,
|
bodyshop.scoreboard_target.dailyBodyTarget + bodyshop.scoreboard_target.dailyPaintTarget,
|
||||||
val
|
val
|
||||||
@@ -72,14 +71,14 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
|
|||||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||||
1
|
1
|
||||||
),
|
),
|
||||||
accHrs: _.round(
|
accHrs: round(
|
||||||
acc.length > 0
|
acc.length > 0
|
||||||
? acc[acc.length - 1].accHrs + dayhrs.painthrs + dayhrs.bodyhrs
|
? acc[acc.length - 1].accHrs + dayhrs.painthrs + dayhrs.bodyhrs
|
||||||
: dayhrs.painthrs + dayhrs.bodyhrs,
|
: dayhrs.painthrs + dayhrs.bodyhrs,
|
||||||
1
|
1
|
||||||
),
|
),
|
||||||
sales: _.round(dayhrs.sales, 2),
|
sales: round(dayhrs.sales, 2),
|
||||||
accSales: _.round(acc.length > 0 ? acc[acc.length - 1].accSales + dayhrs.sales : dayhrs.sales, 2)
|
accSales: round(acc.length > 0 ? acc[acc.length - 1].accSales + dayhrs.sales : dayhrs.sales, 2)
|
||||||
};
|
};
|
||||||
|
|
||||||
return [...acc, theValue];
|
return [...acc, theValue];
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import { Col, Row } from "antd";
|
import { Col, Row, Spin } from "antd";
|
||||||
import React, { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ScoreboardChart from "../scoreboard-chart/scoreboard-chart.component";
|
import ScoreboardChart from "../scoreboard-chart/scoreboard-chart.component";
|
||||||
import ScoreboardLastDays from "../scoreboard-last-days/scoreboard-last-days.component";
|
import ScoreboardLastDays from "../scoreboard-last-days/scoreboard-last-days.component";
|
||||||
import ScoreboardTargetsTable from "../scoreboard-targets-table/scoreboard-targets-table.component";
|
import ScoreboardTargetsTable from "../scoreboard-targets-table/scoreboard-targets-table.component";
|
||||||
|
|
||||||
import { useApolloClient, useQuery } from "@apollo/client";
|
import { useApolloClient, useQuery } from "@apollo/client";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { GET_BLOCKED_DAYS, QUERY_SCOREBOARD } from "../../graphql/scoreboard.queries";
|
import { GET_BLOCKED_DAYS, QUERY_SCOREBOARD } from "../../graphql/scoreboard.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 {
|
||||||
|
clearHolidays,
|
||||||
|
clearWorkingWeekdays,
|
||||||
|
setHolidays,
|
||||||
|
setWorkingWeekdays
|
||||||
|
} from "../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({});
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
||||||
});
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardDisplayComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardDisplayComponent);
|
||||||
|
|
||||||
export function ScoreboardDisplayComponent({ bodyshop }) {
|
export function ScoreboardDisplayComponent({ bodyshop }) {
|
||||||
@@ -26,63 +28,76 @@ export function ScoreboardDisplayComponent({ bodyshop }) {
|
|||||||
start: dayjs().startOf("month"),
|
start: dayjs().startOf("month"),
|
||||||
end: dayjs().endOf("month")
|
end: dayjs().endOf("month")
|
||||||
},
|
},
|
||||||
pollInterval: 60000*5
|
pollInterval: 60000 * 5
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data } = scoreboardSubscription;
|
const { data } = scoreboardSubscription;
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const scoreBoardlist = (data && data.scoreboard) || [];
|
const scoreBoardlist = data?.scoreboard || [];
|
||||||
|
|
||||||
const sbEntriesByDate = {};
|
const sbEntriesByDate = {};
|
||||||
|
|
||||||
scoreBoardlist.forEach((i) => {
|
scoreBoardlist.forEach((i) => {
|
||||||
const entryDate = i.date;
|
const entryDate = i.date;
|
||||||
if (!!!sbEntriesByDate[entryDate]) {
|
if (!sbEntriesByDate[entryDate]) {
|
||||||
sbEntriesByDate[entryDate] = [];
|
sbEntriesByDate[entryDate] = [];
|
||||||
}
|
}
|
||||||
sbEntriesByDate[entryDate].push(i);
|
sbEntriesByDate[entryDate].push(i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true); // Loading state
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//Update the locals.
|
|
||||||
async function setDayJSSettings() {
|
async function setDayJSSettings() {
|
||||||
let appointments;
|
try {
|
||||||
|
let appointments;
|
||||||
|
|
||||||
if (!bodyshop.scoreboard_target.ignoreblockeddays) {
|
if (!bodyshop.scoreboard_target.ignoreblockeddays) {
|
||||||
const { data } = await client.query({
|
const { data } = await client.query({
|
||||||
query: GET_BLOCKED_DAYS,
|
query: GET_BLOCKED_DAYS,
|
||||||
variables: {
|
variables: {
|
||||||
start: dayjs().startOf("month"),
|
start: dayjs().startOf("month"),
|
||||||
end: dayjs().endOf("month")
|
end: dayjs().endOf("month")
|
||||||
}
|
|
||||||
});
|
|
||||||
appointments = data.appointments;
|
|
||||||
}
|
|
||||||
|
|
||||||
dayjs.updateLocale("ca", {
|
|
||||||
workingWeekdays: translateSettingsToWorkingDays(bodyshop.workingdays),
|
|
||||||
...(appointments
|
|
||||||
? {
|
|
||||||
holidays: appointments.map((h) => dayjs(h.start).format("MM-DD-YYYY"))
|
|
||||||
}
|
}
|
||||||
: {}),
|
});
|
||||||
holidayFormat: "MM-DD-YYYY"
|
appointments = data.appointments;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
const holidays = appointments ? appointments.map((h) => dayjs(h.start).format("MM-DD-YYYY")) : [];
|
||||||
|
const workingWeekdays = translateSettingsToWorkingDays(bodyshop.workingdays);
|
||||||
|
|
||||||
|
// Set holidays and working weekdays
|
||||||
|
setHolidays(holidays);
|
||||||
|
setWorkingWeekdays(workingWeekdays);
|
||||||
|
} finally {
|
||||||
|
setLoading(false); // Set loading to false after processing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setDayJSSettings();
|
setDayJSSettings();
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
return () => {
|
||||||
|
clearHolidays();
|
||||||
|
clearWorkingWeekdays();
|
||||||
|
};
|
||||||
}, [client, bodyshop]);
|
}, [client, bodyshop]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Row justify="center" align="middle" style={{ minHeight: "100vh" }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<ScoreboardTargetsTable scoreBoardlist={scoreBoardlist} />
|
<ScoreboardTargetsTable scoreBoardlist={scoreBoardlist} />
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<ScoreboardLastDays sbEntriesByDate={sbEntriesByDate} />
|
<ScoreboardLastDays sbEntriesByDate={sbEntriesByDate} />
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<ScoreboardChart sbEntriesByDate={sbEntriesByDate} />
|
<ScoreboardChart sbEntriesByDate={sbEntriesByDate} />
|
||||||
</Col>
|
</Col>
|
||||||
@@ -92,27 +107,12 @@ export function ScoreboardDisplayComponent({ bodyshop }) {
|
|||||||
|
|
||||||
function translateSettingsToWorkingDays(workingdays) {
|
function translateSettingsToWorkingDays(workingdays) {
|
||||||
const days = [];
|
const days = [];
|
||||||
|
if (workingdays.monday) days.push(1);
|
||||||
if (workingdays.monday) {
|
if (workingdays.tuesday) days.push(2);
|
||||||
days.push(1);
|
if (workingdays.wednesday) days.push(3);
|
||||||
}
|
if (workingdays.thursday) days.push(4);
|
||||||
if (workingdays.tuesday) {
|
if (workingdays.friday) days.push(5);
|
||||||
days.push(2);
|
if (workingdays.saturday) days.push(6);
|
||||||
}
|
if (workingdays.sunday) days.push(0);
|
||||||
if (workingdays.wednesday) {
|
|
||||||
days.push(3);
|
|
||||||
}
|
|
||||||
if (workingdays.thursday) {
|
|
||||||
days.push(4);
|
|
||||||
}
|
|
||||||
if (workingdays.friday) {
|
|
||||||
days.push(5);
|
|
||||||
}
|
|
||||||
if (workingdays.saturday) {
|
|
||||||
days.push(6);
|
|
||||||
}
|
|
||||||
if (workingdays.sunday) {
|
|
||||||
days.push(0);
|
|
||||||
}
|
|
||||||
return days;
|
return days;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
@@ -10,7 +9,7 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ export function ScoreboardLastDays({ bodyshop, sbEntriesByDate }) {
|
|||||||
<Row>
|
<Row>
|
||||||
{ArrayOfDate.map((a) => (
|
{ArrayOfDate.map((a) => (
|
||||||
<Col span={2} key={a}>
|
<Col span={2} key={a}>
|
||||||
{!!sbEntriesByDate ? <ScoreboardDayStat date={a} entries={sbEntriesByDate[a] || []} /> : <LoadingSkeleton />}
|
{sbEntriesByDate ? <ScoreboardDayStat date={a} entries={sbEntriesByDate[a] || []} /> : <LoadingSkeleton />}
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { CalendarOutlined } from "@ant-design/icons";
|
import { CalendarOutlined } from "@ant-design/icons";
|
||||||
import { Card, Col, Divider, Row, Statistic } from "antd";
|
import { Card, Col, Divider, Row, Statistic } from "antd";
|
||||||
import _ from "lodash";
|
import { groupBy } from "lodash";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import React, { useMemo } from "react";
|
import { useMemo } 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";
|
||||||
@@ -13,7 +13,7 @@ import * as Util from "./scoreboard-targets-table.util";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const values = useMemo(() => {
|
const values = useMemo(() => {
|
||||||
const dateHash = _.groupBy(scoreBoardlist, "date");
|
const dateHash = groupBy(scoreBoardlist, "date");
|
||||||
|
|
||||||
let ret = {
|
let ret = {
|
||||||
todayBody: 0,
|
todayBody: 0,
|
||||||
@@ -213,4 +213,5 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardTargetsTable);
|
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardTargetsTable);
|
||||||
|
|||||||
@@ -1,29 +1,172 @@
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
|
|
||||||
export const CalculateWorkingDaysThisMonth = () => dayjs().endOf("month").businessDaysInMonth().length;
|
const DEFAULT_WORKING_DAYS = [1, 2, 3, 4, 5]; // Default to Monday-Friday
|
||||||
|
|
||||||
export const CalculateWorkingDaysInPeriod = (start, end) => dayjs(end).businessDiff(dayjs(start));
|
// Module-level state for holidays and working weekdays
|
||||||
|
let holidays = [];
|
||||||
|
let workingWeekdays = DEFAULT_WORKING_DAYS;
|
||||||
|
|
||||||
export const CalculateWorkingDaysAsOfToday = () => dayjs().endOf("day").businessDiff(dayjs().startOf("month"));
|
/**
|
||||||
|
* Sets the holidays for the business logic.
|
||||||
|
* @param newHolidays
|
||||||
|
*/
|
||||||
|
export const setHolidays = (newHolidays = []) => {
|
||||||
|
holidays = newHolidays;
|
||||||
|
};
|
||||||
|
|
||||||
export const CalculateWorkingDaysLastMonth = () =>
|
/**
|
||||||
dayjs().subtract(1, "month").endOf("month").businessDaysInMonth().length;
|
* Clears the holidays.
|
||||||
|
*/
|
||||||
|
export const clearHolidays = () => {
|
||||||
|
holidays = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the working weekdays for the business logic.
|
||||||
|
* @param newWorkingWeekdays
|
||||||
|
*/
|
||||||
|
export const setWorkingWeekdays = (newWorkingWeekdays = DEFAULT_WORKING_DAYS) => {
|
||||||
|
workingWeekdays = newWorkingWeekdays;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the working weekdays, resetting to default (Monday-Friday).
|
||||||
|
*/
|
||||||
|
export const clearWorkingWeekdays = () => {
|
||||||
|
workingWeekdays = DEFAULT_WORKING_DAYS; // Reset to default
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates the bodyshop working days settings to an array of weekdays.
|
||||||
|
* @returns {*[]}
|
||||||
|
*/
|
||||||
|
export const getHolidays = () => {
|
||||||
|
return holidays;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates the working days settings from the bodyshop to an array of weekdays.
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
export const getWorkingWeekdays = () => {
|
||||||
|
return workingWeekdays;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the number of working days in the current month, excluding holidays.
|
||||||
|
* @returns {number}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export const CalculateWorkingDaysThisMonth = () => {
|
||||||
|
const businessDays = dayjs().businessDaysInMonth();
|
||||||
|
return businessDays.filter((day) => !holidays.includes(dayjs(day).format("MM-DD-YYYY"))).length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the number of working days in a given period, excluding holidays.
|
||||||
|
* @param start
|
||||||
|
* @param end
|
||||||
|
* @returns {number}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export const CalculateWorkingDaysInPeriod = (start, end) => {
|
||||||
|
let businessDays = dayjs(end).businessDiff(dayjs(start));
|
||||||
|
if (dayjs(end).isBusinessDay() && !holidays.includes(dayjs(end).format("MM-DD-YYYY"))) {
|
||||||
|
businessDays += 1;
|
||||||
|
}
|
||||||
|
return businessDays;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the number of working days as of today, excluding holidays.
|
||||||
|
* @returns {number}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export const CalculateWorkingDaysAsOfToday = () => {
|
||||||
|
const today = dayjs().startOf("day");
|
||||||
|
let businessDays = today.businessDiff(dayjs().startOf("month"));
|
||||||
|
if (today.isBusinessDay() && !holidays.includes(today.format("MM-DD-YYYY"))) {
|
||||||
|
businessDays += 1;
|
||||||
|
}
|
||||||
|
return businessDays;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the number of working days in the last month, excluding holidays.
|
||||||
|
* @returns {number}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export const CalculateWorkingDaysLastMonth = () => {
|
||||||
|
const businessDays = dayjs().subtract(1, "month").businessDaysInMonth();
|
||||||
|
return businessDays.filter((day) => !holidays.includes(dayjs(day).format("MM-DD-YYYY"))).length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the weekly target hours based on daily target hours and the number of working days in the current week.
|
||||||
|
* @param dailyTargetHrs
|
||||||
|
* @returns {number}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
export const WeeklyTargetHrs = (dailyTargetHrs) =>
|
export const WeeklyTargetHrs = (dailyTargetHrs) =>
|
||||||
dailyTargetHrs * CalculateWorkingDaysInPeriod(dayjs().startOf("week"), dayjs().endOf("week"));
|
dailyTargetHrs * CalculateWorkingDaysInPeriod(dayjs().startOf("week"), dayjs().endOf("week"));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the weekly target hours for a specific period.
|
||||||
|
* @param dailyTargetHrs
|
||||||
|
* @param start
|
||||||
|
* @param end
|
||||||
|
* @returns {number}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
export const WeeklyTargetHrsInPeriod = (dailyTargetHrs, start, end) =>
|
export const WeeklyTargetHrsInPeriod = (dailyTargetHrs, start, end) =>
|
||||||
dailyTargetHrs * CalculateWorkingDaysInPeriod(start, end);
|
dailyTargetHrs * CalculateWorkingDaysInPeriod(start, end);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the monthly target hours based on daily target hours and the number of working days in the current month.
|
||||||
|
* @param dailyTargetHrs
|
||||||
|
* @returns {number}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
export const MonthlyTargetHrs = (dailyTargetHrs) => dailyTargetHrs * CalculateWorkingDaysThisMonth();
|
export const MonthlyTargetHrs = (dailyTargetHrs) => dailyTargetHrs * CalculateWorkingDaysThisMonth();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the monthly target hours for the last month based on daily target hours and the number of working days
|
||||||
|
* in the last month.
|
||||||
|
* @param dailyTargetHrs
|
||||||
|
* @returns {number}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
export const LastMonthTargetHrs = (dailyTargetHrs) => dailyTargetHrs * CalculateWorkingDaysLastMonth();
|
export const LastMonthTargetHrs = (dailyTargetHrs) => dailyTargetHrs * CalculateWorkingDaysLastMonth();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the target hours as of today based on daily target hours and the number of working days as of today.
|
||||||
|
* @param dailyTargetHrs
|
||||||
|
* @returns {number}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
export const AsOfTodayTargetHrs = (dailyTargetHrs) => dailyTargetHrs * CalculateWorkingDaysAsOfToday();
|
export const AsOfTodayTargetHrs = (dailyTargetHrs) => dailyTargetHrs * CalculateWorkingDaysAsOfToday();
|
||||||
|
|
||||||
export const AsOfDateTargetHours = (dailyTargetHours, date) =>
|
/**
|
||||||
dailyTargetHours * dayjs(date).businessDiff(dayjs().startOf("month"));
|
* Calculates the target hours as of a specific date based on daily target hours and the number of business days up to
|
||||||
|
* that date.
|
||||||
|
* @param dailyTargetHours
|
||||||
|
* @param date
|
||||||
|
* @returns {number}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export const AsOfDateTargetHours = (dailyTargetHours, date) => {
|
||||||
|
let businessDays = dayjs(date).businessDiff(dayjs().startOf("month"));
|
||||||
|
if (dayjs(date).isBusinessDay() && !holidays.includes(dayjs(date).format("MM-DD-YYYY"))) {
|
||||||
|
businessDays += 1;
|
||||||
|
}
|
||||||
|
return dailyTargetHours * businessDays;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a list of all days in the current month.
|
||||||
|
* @returns {*[]}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
export const ListOfDaysInCurrentMonth = () => {
|
export const ListOfDaysInCurrentMonth = () => {
|
||||||
const days = [];
|
const days = [];
|
||||||
let dateStart = dayjs().startOf("month");
|
let dateStart = dayjs().startOf("month");
|
||||||
@@ -36,6 +179,13 @@ export const ListOfDaysInCurrentMonth = () => {
|
|||||||
return days;
|
return days;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a list of all days between two dates.
|
||||||
|
* @param start
|
||||||
|
* @param end
|
||||||
|
* @returns {*[]}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
export const ListDaysBetween = ({ start, end }) => {
|
export const ListDaysBetween = ({ start, end }) => {
|
||||||
const days = [];
|
const days = [];
|
||||||
let dateStart = dayjs(start);
|
let dateStart = dayjs(start);
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { useApolloClient, useMutation, useQuery } from "@apollo/client";
|
import { useApolloClient, useMutation, useQuery } from "@apollo/client";
|
||||||
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Card, Form, Input, InputNumber, Select, Switch, Table } from "antd";
|
import { Button, Card, Form, Input, InputNumber, Select, Switch, Table } from "antd";
|
||||||
import { useForm } from "antd/es/form/Form";
|
import { useForm } from "antd/es/form/Form";
|
||||||
import dayjs from "../../utils/day";
|
import queryString from "query-string";
|
||||||
import React, { 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";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import {
|
import {
|
||||||
CHECK_EMPLOYEE_NUMBER,
|
CHECK_EMPLOYEE_NUMBER,
|
||||||
@@ -20,19 +22,17 @@ import {
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CiecaSelect from "../../utils/Ciecaselect";
|
import CiecaSelect from "../../utils/Ciecaselect";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component";
|
import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component";
|
||||||
import queryString from "query-string";
|
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then(() => {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("employees.successes.save")
|
message: t("employees.successes.save")
|
||||||
});
|
});
|
||||||
@@ -120,13 +120,13 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
title: t("employees.fields.vacation.start"),
|
title: t("employees.fields.vacation.start"),
|
||||||
dataIndex: "start",
|
dataIndex: "start",
|
||||||
key: "start",
|
key: "start",
|
||||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
render: (text) => <DateFormatter>{text}</DateFormatter>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("employees.fields.vacation.end"),
|
title: t("employees.fields.vacation.end"),
|
||||||
dataIndex: "end",
|
dataIndex: "end",
|
||||||
key: "end",
|
key: "end",
|
||||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
render: (text) => <DateFormatter>{text}</DateFormatter>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("employees.fields.vacation.length"),
|
title: t("employees.fields.vacation.length"),
|
||||||
@@ -210,7 +210,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
required: true
|
required: true
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
({ getFieldValue }) => ({
|
() => ({
|
||||||
async validator(rule, value) {
|
async validator(rule, value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
const response = await client.query({
|
const response = await client.query({
|
||||||
@@ -369,8 +369,9 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
add();
|
add();
|
||||||
}}
|
}}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
|
id="add-employee-rate-button"
|
||||||
>
|
>
|
||||||
{t("employees.actions.newrate")}
|
<span id="new-employee-rate">{t("employees.actions.newrate")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
@@ -383,7 +384,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
title={() => <ShopEmployeeAddVacation employee={data && data.employees_by_pk} />}
|
title={() => <ShopEmployeeAddVacation employee={data && data.employees_by_pk} />}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey={"id"}
|
rowKey={"id"}
|
||||||
dataSource={data ? data.employees_by_pk.employee_vacations : []}
|
dataSource={data?.employees_by_pk?.employee_vacations ?? []}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Card, Tabs } from "antd";
|
import { Button, Card, Tabs } from "antd";
|
||||||
import React from "react";
|
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";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
|
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||||
import ShopInfoGeneral from "./shop-info.general.component";
|
import ShopInfoGeneral from "./shop-info.general.component";
|
||||||
import ShopInfoIntakeChecklistComponent from "./shop-info.intake.component";
|
import ShopInfoIntakeChecklistComponent from "./shop-info.intake.component";
|
||||||
import ShopInfoLaborRates from "./shop-info.laborrates.component";
|
import ShopInfoLaborRates from "./shop-info.laborrates.component";
|
||||||
|
import ShopInfoNotificationsAutoadd from "./shop-info.notifications-autoadd.component.jsx";
|
||||||
import ShopInfoOrderStatusComponent from "./shop-info.orderstatus.component";
|
import ShopInfoOrderStatusComponent from "./shop-info.orderstatus.component";
|
||||||
import ShopInfoPartsScan from "./shop-info.parts-scan";
|
import ShopInfoPartsScan from "./shop-info.parts-scan";
|
||||||
import ShopInfoRbacComponent from "./shop-info.rbac.component";
|
import ShopInfoRbacComponent from "./shop-info.rbac.component";
|
||||||
import ShopInfoResponsibilityCenterComponent from "./shop-info.responsibilitycenters.component";
|
import ShopInfoResponsibilityCenterComponent from "./shop-info.responsibilitycenters.component";
|
||||||
|
import ShopInfoRoGuard from "./shop-info.roguard.component";
|
||||||
import ShopInfoROStatusComponent from "./shop-info.rostatus.component";
|
import ShopInfoROStatusComponent from "./shop-info.rostatus.component";
|
||||||
import ShopInfoSchedulingComponent from "./shop-info.scheduling.component";
|
import ShopInfoSchedulingComponent from "./shop-info.scheduling.component";
|
||||||
import ShopInfoSpeedPrint from "./shop-info.speedprint.component";
|
import ShopInfoSpeedPrint from "./shop-info.speedprint.component";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
|
||||||
import ShopInfoTaskPresets from "./shop-info.task-presets.component";
|
import ShopInfoTaskPresets from "./shop-info.task-presets.component";
|
||||||
import queryString from "query-string";
|
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|
||||||
import ShopInfoRoGuard from "./shop-info.roguard.component";
|
|
||||||
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
|
||||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
|
||||||
import ShopInfoNotificationsAutoadd from "./shop-info.notifications-autoadd.component.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent);
|
||||||
@@ -158,7 +157,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" loading={saveLoading} onClick={() => form.submit()}>
|
<Button type="primary" loading={saveLoading} onClick={() => form.submit()} id="shop-info-save-button">
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Button, DatePicker, Form, Input, InputNumber, Radio, Select, Space, Switch } from "antd";
|
import { Button, DatePicker, Form, Input, InputNumber, Radio, Select, Space, Switch } from "antd";
|
||||||
import React 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";
|
||||||
@@ -15,11 +14,12 @@ import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-forma
|
|||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const timeZonesList = Intl.supportedValuesOf("timeZone");
|
const timeZonesList = Intl.supportedValuesOf("timeZone");
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
|
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
|
||||||
@@ -144,236 +144,246 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
|||||||
<InputNumber min={0} />
|
<InputNumber min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<FeatureWrapper featureName="export" noauth={() => null}>
|
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")} id="accountingsetup">
|
||||||
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")} id="accountingsetup">
|
{HasFeatureAccess({ featureName: "export", bodyshop }) && (
|
||||||
<Form.Item label={t("bodyshop.labels.qbo")} valuePropName="checked" name={["accountingconfig", "qbo"]}>
|
<>
|
||||||
<Switch />
|
<Form.Item label={t("bodyshop.labels.qbo")} valuePropName="checked" name={["accountingconfig", "qbo"]}>
|
||||||
</Form.Item>
|
<Switch />
|
||||||
{InstanceRenderManager({
|
</Form.Item>
|
||||||
imex: (
|
{InstanceRenderManager({
|
||||||
<Form.Item shouldUpdate noStyle>
|
imex: (
|
||||||
{() => (
|
<Form.Item shouldUpdate noStyle>
|
||||||
|
{() => (
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.labels.qbo_usa")}
|
||||||
|
shouldUpdate
|
||||||
|
valuePropName="checked"
|
||||||
|
name={["accountingconfig", "qbo_usa"]}
|
||||||
|
>
|
||||||
|
<Switch disabled={!form.getFieldValue(["accountingconfig", "qbo"])} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<Form.Item label={t("bodyshop.labels.qbo_departmentid")} name={["accountingconfig", "qbo_departmentid"]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.labels.accountingtiers")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={["accountingconfig", "tiers"]}
|
||||||
|
>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio value={2}>2</Radio>
|
||||||
|
<Radio value={3}>3</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.labels.qbo_usa")}
|
label={t("bodyshop.labels.2tiersetup")}
|
||||||
shouldUpdate
|
shouldUpdate
|
||||||
valuePropName="checked"
|
rules={[
|
||||||
name={["accountingconfig", "qbo_usa"]}
|
{
|
||||||
|
required: form.getFieldValue(["accountingconfig", "tiers"]) === 2
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={["accountingconfig", "twotierpref"]}
|
||||||
>
|
>
|
||||||
<Switch disabled={!form.getFieldValue(["accountingconfig", "qbo"])} />
|
<Radio.Group disabled={form.getFieldValue(["accountingconfig", "tiers"]) === 3}>
|
||||||
|
<Radio value="name">{t("bodyshop.labels.2tiername")}</Radio>
|
||||||
|
<Radio value="source">{t("bodyshop.labels.2tiersource")}</Radio>
|
||||||
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
);
|
||||||
</Form.Item>
|
}}
|
||||||
)
|
</Form.Item>
|
||||||
})}
|
<Form.Item
|
||||||
<Form.Item label={t("bodyshop.labels.qbo_departmentid")} name={["accountingconfig", "qbo_departmentid"]}>
|
label={t("bodyshop.labels.printlater")}
|
||||||
<Input />
|
valuePropName="checked"
|
||||||
</Form.Item>
|
name={["accountingconfig", "printlater"]}
|
||||||
<Form.Item
|
>
|
||||||
label={t("bodyshop.labels.accountingtiers")}
|
<Switch />
|
||||||
rules={[
|
</Form.Item>
|
||||||
{
|
<Form.Item
|
||||||
required: true
|
label={t("bodyshop.labels.emaillater")}
|
||||||
//message: t("general.validation.required"),
|
valuePropName="checked"
|
||||||
}
|
name={["accountingconfig", "emaillater"]}
|
||||||
]}
|
>
|
||||||
name={["accountingconfig", "tiers"]}
|
<Switch />
|
||||||
>
|
</Form.Item>
|
||||||
<Radio.Group>
|
</>
|
||||||
<Radio value={2}>2</Radio>
|
)}
|
||||||
<Radio value={3}>3</Radio>
|
<Form.Item
|
||||||
</Radio.Group>
|
label={t("bodyshop.fields.inhousevendorid")}
|
||||||
</Form.Item>
|
name={"inhousevendorid"}
|
||||||
<Form.Item shouldUpdate>
|
rules={[
|
||||||
{() => {
|
{
|
||||||
return (
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.default_adjustment_rate")}
|
||||||
|
name={"default_adjustment_rate"}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} precision={2} />
|
||||||
|
</Form.Item>
|
||||||
|
{InstanceRenderManager({
|
||||||
|
imex: (
|
||||||
|
<Form.Item label={t("bodyshop.fields.federal_tax_id")} name="federal_tax_id">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<Form.Item label={t("bodyshop.fields.state_tax_id")} name="state_tax_id">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
{HasFeatureAccess({ featureName: "bills", bodyshop }) && (
|
||||||
|
<>
|
||||||
|
{InstanceRenderManager({
|
||||||
|
imex: (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.labels.2tiersetup")}
|
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
||||||
shouldUpdate
|
name={["bill_tax_rates", "federal_tax_rate"]}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: form.getFieldValue(["accountingconfig", "tiers"]) === 2
|
required: true
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
name={["accountingconfig", "twotierpref"]}
|
|
||||||
>
|
>
|
||||||
<Radio.Group disabled={form.getFieldValue(["accountingconfig", "tiers"]) === 3}>
|
<InputNumber />
|
||||||
<Radio value="name">{t("bodyshop.labels.2tiername")}</Radio>
|
|
||||||
<Radio value="source">{t("bodyshop.labels.2tiersource")}</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
)
|
||||||
}}
|
})}
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
||||||
label={t("bodyshop.labels.printlater")}
|
name={["bill_tax_rates", "state_tax_rate"]}
|
||||||
valuePropName="checked"
|
rules={[
|
||||||
name={["accountingconfig", "printlater"]}
|
{
|
||||||
>
|
required: true
|
||||||
<Switch />
|
//message: t("general.validation.required"),
|
||||||
</Form.Item>
|
}
|
||||||
<Form.Item
|
]}
|
||||||
label={t("bodyshop.labels.emaillater")}
|
>
|
||||||
valuePropName="checked"
|
<InputNumber />
|
||||||
name={["accountingconfig", "emaillater"]}
|
</Form.Item>
|
||||||
>
|
<Form.Item
|
||||||
<Switch />
|
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
||||||
</Form.Item>
|
name={["bill_tax_rates", "local_tax_rate"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("bodyshop.fields.inhousevendorid")}
|
{
|
||||||
name={"inhousevendorid"}
|
required: true
|
||||||
rules={[
|
//message: t("general.validation.required"),
|
||||||
{
|
}
|
||||||
required: true
|
]}
|
||||||
//message: t("general.validation.required"),
|
>
|
||||||
}
|
<InputNumber />
|
||||||
]}
|
</Form.Item>
|
||||||
>
|
</>
|
||||||
<Input />
|
)}
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
name={["md_payment_types"]}
|
||||||
label={t("bodyshop.fields.default_adjustment_rate")}
|
label={t("bodyshop.fields.md_payment_types")}
|
||||||
name={"default_adjustment_rate"}
|
rules={[
|
||||||
rules={[
|
{
|
||||||
{
|
required: true,
|
||||||
required: true
|
//message: t("general.validation.required"),
|
||||||
//message: t("general.validation.required"),
|
type: "array"
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} precision={2} />
|
<Select mode="tags" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{InstanceRenderManager({
|
<Form.Item
|
||||||
imex: (
|
name={["md_categories"]}
|
||||||
<Form.Item label={t("bodyshop.fields.federal_tax_id")} name="federal_tax_id">
|
label={t("bodyshop.fields.md_categories")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
type: "array"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select mode="tags" />
|
||||||
|
</Form.Item>
|
||||||
|
{HasFeatureAccess({ featureName: "export", bodyshop }) && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
name={["accountingconfig", "ReceivableCustomField1"]}
|
||||||
|
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
|
||||||
|
>
|
||||||
|
{ReceivableCustomFieldSelect}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={["accountingconfig", "ReceivableCustomField2"]}
|
||||||
|
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
|
||||||
|
>
|
||||||
|
{ReceivableCustomFieldSelect}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={["accountingconfig", "ReceivableCustomField3"]}
|
||||||
|
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
|
||||||
|
>
|
||||||
|
{ReceivableCustomFieldSelect}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={["md_classes"]}
|
||||||
|
label={t("bodyshop.fields.md_classes")}
|
||||||
|
rules={[
|
||||||
|
({ getFieldValue }) => {
|
||||||
|
return {
|
||||||
|
required: getFieldValue("enforce_class"),
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
type: "array"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select mode="tags" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={["enforce_class"]} label={t("bodyshop.fields.enforce_class")} valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
{ClosingPeriod.treatment === "on" && (
|
||||||
|
<Form.Item
|
||||||
|
name={["accountingconfig", "ClosingPeriod"]}
|
||||||
|
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
|
||||||
|
>
|
||||||
|
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{ADPPayroll.treatment === "on" && (
|
||||||
|
<Form.Item name={["accountingconfig", "companyCode"]} label={t("bodyshop.fields.companycode")}>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)}
|
||||||
})}
|
{ADPPayroll.treatment === "on" && (
|
||||||
<Form.Item label={t("bodyshop.fields.state_tax_id")} name="state_tax_id">
|
<Form.Item name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
|
||||||
{InstanceRenderManager({
|
|
||||||
imex: (
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
|
||||||
name={["bill_tax_rates", "federal_tax_rate"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)}
|
||||||
})}
|
</>
|
||||||
<Form.Item
|
)}
|
||||||
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
</LayoutFormRow>
|
||||||
name={["bill_tax_rates", "state_tax_rate"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
|
||||||
name={["bill_tax_rates", "local_tax_rate"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={["md_payment_types"]}
|
|
||||||
label={t("bodyshop.fields.md_payment_types")}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
type: "array"
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Select mode="tags" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={["md_categories"]}
|
|
||||||
label={t("bodyshop.fields.md_categories")}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
type: "array"
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Select mode="tags" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name={["enforce_class"]} label={t("bodyshop.fields.enforce_class")} valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={["accountingconfig", "ReceivableCustomField1"]}
|
|
||||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
|
|
||||||
>
|
|
||||||
{ReceivableCustomFieldSelect}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={["accountingconfig", "ReceivableCustomField2"]}
|
|
||||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
|
|
||||||
>
|
|
||||||
{ReceivableCustomFieldSelect}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={["accountingconfig", "ReceivableCustomField3"]}
|
|
||||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
|
|
||||||
>
|
|
||||||
{ReceivableCustomFieldSelect}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={["md_classes"]}
|
|
||||||
label={t("bodyshop.fields.md_classes")}
|
|
||||||
rules={[
|
|
||||||
({ getFieldValue }) => {
|
|
||||||
return {
|
|
||||||
required: getFieldValue("enforce_class"),
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
type: "array"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Select mode="tags" />
|
|
||||||
</Form.Item>
|
|
||||||
{ClosingPeriod.treatment === "on" && (
|
|
||||||
<Form.Item
|
|
||||||
name={["accountingconfig", "ClosingPeriod"]}
|
|
||||||
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
|
|
||||||
>
|
|
||||||
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{ADPPayroll.treatment === "on" && (
|
|
||||||
<Form.Item name={["accountingconfig", "companyCode"]} label={t("bodyshop.fields.companycode")}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{ADPPayroll.treatment === "on" && (
|
|
||||||
<Form.Item name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</LayoutFormRow>
|
|
||||||
</FeatureWrapper>
|
|
||||||
<FeatureWrapper featureName="scoreboard" noauth={() => null}>
|
<FeatureWrapper featureName="scoreboard" noauth={() => null}>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.scoreboardsetup")} id="scoreboardsetup">
|
<LayoutFormRow header={t("bodyshop.labels.scoreboardsetup")} id="scoreboardsetup">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -823,7 +833,11 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
|||||||
}}
|
}}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow grow header={t("bodyshop.labels.insurancecos")} id="insurancecos">
|
<LayoutFormRow
|
||||||
|
grow
|
||||||
|
header=<span id="insurancecos-header">{t("bodyshop.labels.insurancecos")}</span>
|
||||||
|
id="insurancecos"
|
||||||
|
>
|
||||||
<Form.List name={["md_ins_cos"]}>
|
<Form.List name={["md_ins_cos"]}>
|
||||||
{(fields, { add, remove, move }) => {
|
{(fields, { add, remove, move }) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ import { ColorPicker } from "./shop-info.rostatus.component";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<LayoutFormRow>
|
<LayoutFormRow id="shopinfo-scheduling">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.appt_length")}
|
label={t("bodyshop.fields.appt_length")}
|
||||||
name={"appt_length"}
|
name={"appt_length"}
|
||||||
@@ -44,6 +44,7 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
|||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
id="schedule_start_time"
|
||||||
>
|
>
|
||||||
<TimePicker disableSeconds={true} format="HH:mm" />
|
<TimePicker disableSeconds={true} format="HH:mm" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -56,6 +57,7 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
|||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
id="schedule_end_time"
|
||||||
>
|
>
|
||||||
<TimePicker disableSeconds={true} format="HH:mm" />
|
<TimePicker disableSeconds={true} format="HH:mm" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ export default connect(mapStateToProps, mapDispatchToProps)(ShopSubStatus);
|
|||||||
export function ShopSubStatus({ bodyshop }) {
|
export function ShopSubStatus({ bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { sub_status } = bodyshop;
|
const { sub_status } = bodyshop;
|
||||||
return <Result status="403" title={t(`general.labels.sub_status.${sub_status}`)} />;
|
// ‘expired’ ‘trail-expired' are the valid sub_status values
|
||||||
|
return <Result status="403" title={t(`general.errors.sub_status.${sub_status}`)} />;
|
||||||
}
|
}
|
||||||
|
|||||||
156
client/src/components/task-center/task-center.component.jsx
Normal file
156
client/src/components/task-center/task-center.component.jsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { Virtuoso } from "react-virtuoso";
|
||||||
|
import { Badge, Button, Spin } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { forwardRef, useMemo, useRef } from "react";
|
||||||
|
import day from "../../utils/day.js";
|
||||||
|
import "./task-center.styles.scss";
|
||||||
|
import {
|
||||||
|
ArrowRightOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
PlusCircleOutlined,
|
||||||
|
QuestionCircleOutlined
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
|
||||||
|
const TaskCenterComponent = forwardRef(
|
||||||
|
({ visible, tasks, loading, error, onTaskClick, onLoadMore, hasMore, createNewTask, incompleteTaskCount }, ref) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const virtuosoRef = useRef(null);
|
||||||
|
|
||||||
|
const sectionIcons = {
|
||||||
|
[t("tasks.labels.overdue")]: <ClockCircleOutlined style={{ marginRight: 8 }} />,
|
||||||
|
[t("tasks.labels.due_today")]: <CalendarOutlined style={{ marginRight: 8 }} />,
|
||||||
|
[t("tasks.labels.upcoming")]: <ArrowRightOutlined style={{ marginRight: 8 }} />,
|
||||||
|
[t("tasks.labels.no_due_date")]: <QuestionCircleOutlined style={{ marginRight: 8 }} />
|
||||||
|
};
|
||||||
|
|
||||||
|
const groups = useMemo(() => {
|
||||||
|
const now = day();
|
||||||
|
const today = now.startOf("day");
|
||||||
|
|
||||||
|
const overdue = tasks.filter((t) => t.due_date && day(t.due_date).isBefore(today));
|
||||||
|
const dueToday = tasks.filter((t) => t.due_date && day(t.due_date).isSame(today, "day"));
|
||||||
|
const upcoming = tasks.filter(
|
||||||
|
(t) => t.due_date && day(t.due_date).isAfter(today) && !day(t.due_date).isSame(today, "day")
|
||||||
|
);
|
||||||
|
const noDueDate = tasks.filter((t) => !t.due_date);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ label: t("tasks.labels.overdue"), tasks: overdue },
|
||||||
|
{ label: t("tasks.labels.due_today"), tasks: dueToday },
|
||||||
|
{ label: t("tasks.labels.upcoming"), tasks: upcoming },
|
||||||
|
{ label: t("tasks.labels.no_due_date"), tasks: noDueDate }
|
||||||
|
].filter((group) => group.tasks.length > 0);
|
||||||
|
}, [tasks, t]);
|
||||||
|
|
||||||
|
const groupCounts = useMemo(() => groups.map((group) => group.tasks.length), [groups]);
|
||||||
|
|
||||||
|
const flatTasks = useMemo(() => groups.flatMap((group) => group.tasks), [groups]);
|
||||||
|
|
||||||
|
const priorityColors = {
|
||||||
|
1: "red",
|
||||||
|
2: "orange",
|
||||||
|
3: "green"
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPriorityColor = (priority) => priorityColors[priority] || null;
|
||||||
|
|
||||||
|
const groupContent = (groupIndex) => {
|
||||||
|
const { label, tasks } = groups[groupIndex];
|
||||||
|
let displayCount = tasks.length;
|
||||||
|
if (label === t("tasks.labels.no_due_date")) {
|
||||||
|
displayCount =
|
||||||
|
incompleteTaskCount -
|
||||||
|
groups.reduce((sum, group, idx) => (idx !== groupIndex ? sum + group.tasks.length : sum), 0);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="section-title">
|
||||||
|
{sectionIcons[label]}
|
||||||
|
{label} ({displayCount})
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemContent = (index) => {
|
||||||
|
const task = flatTasks[index];
|
||||||
|
const priorityColor = getPriorityColor(task.priority);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="task-row"
|
||||||
|
onClick={() => onTaskClick(task.id)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
onTaskClick(task.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="task-title-cell">
|
||||||
|
<div className="task-row-container">
|
||||||
|
<div className="task-title">{task.title}</div>
|
||||||
|
<div className="task-ro-number">
|
||||||
|
{t("tasks.labels.ro-number", {
|
||||||
|
ro_number: task.job?.ro_number || t("general.labels.na")
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="task-due-cell">
|
||||||
|
{task.due_date && <span>{day(task.due_date).fromNow()}</span>}
|
||||||
|
{!!priorityColor && <Badge color={priorityColor} dot style={{ marginLeft: 6 }} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className={`task-center ${visible ? "visible" : ""}`} ref={ref}>
|
||||||
|
<div className="task-header">
|
||||||
|
<h3>{t("tasks.labels.my_tasks_center")}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="error-message">{t("tasks.errors.load_failed")}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`task-center ${visible ? "visible" : ""}`} ref={ref}>
|
||||||
|
<div className="task-header">
|
||||||
|
<Badge count={incompleteTaskCount} size="medium" offset={[13, -5]}>
|
||||||
|
<h3>{t("tasks.labels.my_tasks_center")}</h3>
|
||||||
|
</Badge>
|
||||||
|
<div className="task-header-actions">
|
||||||
|
<Button className="create-task-button" type="link" icon={<PlusCircleOutlined />} onClick={createNewTask} />
|
||||||
|
{loading && <Spin spinning={loading} size="small" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tasks.length === 0 && !loading ? (
|
||||||
|
<div className="no-tasks-message">{t("tasks.labels.no_tasks")}</div>
|
||||||
|
) : (
|
||||||
|
<Virtuoso
|
||||||
|
ref={virtuosoRef}
|
||||||
|
style={{ height: "550px", width: "100%" }}
|
||||||
|
groupCounts={groupCounts}
|
||||||
|
groupContent={groupContent}
|
||||||
|
itemContent={itemContent}
|
||||||
|
endReached={hasMore && !loading ? onLoadMore : undefined}
|
||||||
|
components={{
|
||||||
|
Footer: () =>
|
||||||
|
loading ? (
|
||||||
|
<div className="loading-footer">
|
||||||
|
<Spin />
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
TaskCenterComponent.displayName = "TaskCenterComponent";
|
||||||
|
export default TaskCenterComponent;
|
||||||
135
client/src/components/task-center/task-center.container.jsx
Normal file
135
client/src/components/task-center/task-center.container.jsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
|
import { INITIAL_TASKS, TASKS_CENTER_POLL_INTERVAL, useSocket } from "../../contexts/SocketIO/useSocket";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee";
|
||||||
|
import TaskCenterComponent from "./task-center.component";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
import { QUERY_TASKS_NO_DUE_DATE_PAGINATED, QUERY_TASKS_WITH_DUE_DATES } from "../../graphql/tasks.queries";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||||
|
});
|
||||||
|
|
||||||
|
const TaskCenterContainer = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
bodyshop,
|
||||||
|
currentUser,
|
||||||
|
setTaskUpsertContext,
|
||||||
|
incompleteTaskCount
|
||||||
|
}) => {
|
||||||
|
const [tasks, setTasks] = useState([]);
|
||||||
|
const { isConnected } = useSocket();
|
||||||
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
|
||||||
|
const assignedToId = useMemo(() => {
|
||||||
|
const employee = bodyshop?.employees?.find((e) => e.user_email === currentUser?.email);
|
||||||
|
return employee?.id || null;
|
||||||
|
}, [bodyshop, currentUser]);
|
||||||
|
|
||||||
|
// Query 1: Tasks with due dates
|
||||||
|
const {
|
||||||
|
data: dueDateData,
|
||||||
|
loading: dueLoading,
|
||||||
|
error: dueError
|
||||||
|
} = useQuery(QUERY_TASKS_WITH_DUE_DATES, {
|
||||||
|
variables: {
|
||||||
|
bodyshop: bodyshop?.id,
|
||||||
|
assigned_to: assignedToId,
|
||||||
|
order: [{ due_date: "asc" }, { created_at: "desc" }]
|
||||||
|
},
|
||||||
|
skip: !bodyshop?.id || !assignedToId || !isEmployee || !currentUser?.email,
|
||||||
|
fetchPolicy: "cache-and-network",
|
||||||
|
pollInterval: isConnected ? 0 : TASKS_CENTER_POLL_INTERVAL
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query 2: Tasks with no due date (paginated)
|
||||||
|
const {
|
||||||
|
data: noDueDateData,
|
||||||
|
loading: noDueLoading,
|
||||||
|
error: noDueError,
|
||||||
|
fetchMore
|
||||||
|
} = useQuery(QUERY_TASKS_NO_DUE_DATE_PAGINATED, {
|
||||||
|
variables: {
|
||||||
|
bodyshop: bodyshop?.id,
|
||||||
|
assigned_to: assignedToId,
|
||||||
|
order: [{ priority: "asc" }, { created_at: "desc" }],
|
||||||
|
limit: INITIAL_TASKS, // Adjust this constant as needed
|
||||||
|
offset: 0
|
||||||
|
},
|
||||||
|
skip: !bodyshop?.id || !assignedToId || !isEmployee || !currentUser?.email,
|
||||||
|
fetchPolicy: "cache-and-network",
|
||||||
|
pollInterval: isConnected ? 0 : TASKS_CENTER_POLL_INTERVAL
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combine tasks from both queries
|
||||||
|
useEffect(() => {
|
||||||
|
const dueDateTasks = dueDateData?.tasks || [];
|
||||||
|
const noDueDateTasks = noDueDateData?.tasks || [];
|
||||||
|
setTasks([...dueDateTasks, ...noDueDateTasks]);
|
||||||
|
}, [dueDateData, noDueDateData]);
|
||||||
|
|
||||||
|
const noDueDateLength = noDueDateData?.tasks?.length || 0;
|
||||||
|
const totalNoDueDate = noDueDateData?.tasks_aggregate?.aggregate?.count || 0;
|
||||||
|
const hasMore = noDueDateLength < totalNoDueDate;
|
||||||
|
|
||||||
|
// Handle pagination for no-due-date tasks
|
||||||
|
const handleLoadMore = () => {
|
||||||
|
fetchMore({
|
||||||
|
variables: {
|
||||||
|
offset: noDueDateData?.tasks?.length || 0
|
||||||
|
},
|
||||||
|
updateQuery: (prev, { fetchMoreResult }) => {
|
||||||
|
if (!fetchMoreResult) return prev;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
tasks: [...prev.tasks, ...fetchMoreResult.tasks],
|
||||||
|
tasks_aggregate: fetchMoreResult.tasks_aggregate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTaskClick = useCallback(
|
||||||
|
(id) => {
|
||||||
|
const task = tasks.find((t) => t.id === id);
|
||||||
|
if (task) {
|
||||||
|
setTaskUpsertContext({
|
||||||
|
context: {
|
||||||
|
existingTask: task
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[tasks, setTaskUpsertContext]
|
||||||
|
);
|
||||||
|
|
||||||
|
const createNewTask = () => {
|
||||||
|
setTaskUpsertContext({ actions: {}, context: {} });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TaskCenterComponent
|
||||||
|
visible={visible}
|
||||||
|
onClose={onClose}
|
||||||
|
tasks={tasks}
|
||||||
|
loading={dueLoading || noDueLoading}
|
||||||
|
error={dueError || noDueError}
|
||||||
|
onTaskClick={handleTaskClick}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
|
createNewTask={createNewTask}
|
||||||
|
incompleteTaskCount={incompleteTaskCount}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(TaskCenterContainer);
|
||||||
147
client/src/components/task-center/task-center.styles.scss
Normal file
147
client/src/components/task-center/task-center.styles.scss
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
.task-center {
|
||||||
|
position: absolute;
|
||||||
|
top: 64px;
|
||||||
|
right: 0;
|
||||||
|
width: 500px;
|
||||||
|
max-width: 500px;
|
||||||
|
background: #fff;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), 0 3px 6px rgba(0, 0, 0, 0.06);
|
||||||
|
z-index: 1000;
|
||||||
|
display: none;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-header {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-task-button {
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #40a9ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-section {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
padding: 0px 10px;
|
||||||
|
margin: 0px;
|
||||||
|
//font-size: 12px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
font-weight: 650;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row-container {
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row {
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-title-cell {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 8px;
|
||||||
|
vertical-align: top;
|
||||||
|
//font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
max-width: 350px; // or whatever fits your layout
|
||||||
|
|
||||||
|
.task-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 550;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%; // Or a specific width if you want more control
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-ro-number {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-due-cell {
|
||||||
|
padding: 6px 8px;
|
||||||
|
vertical-align: top;
|
||||||
|
//font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 8px auto;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background-color: #1677ff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
//font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #4096ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: #d9d9d9;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tasks-message,
|
||||||
|
.error-message {
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-footer {
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,12 @@ import {
|
|||||||
DeleteFilled,
|
DeleteFilled,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditFilled,
|
EditFilled,
|
||||||
ExclamationCircleFilled,
|
|
||||||
PlusCircleFilled,
|
PlusCircleFilled,
|
||||||
SyncOutlined
|
SyncOutlined
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Button, Card, Space, Switch, Table } from "antd";
|
import { Button, Card, Space, Switch, Table } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -19,6 +18,7 @@ import { pageLimit } from "../../utils/config";
|
|||||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter.jsx";
|
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter.jsx";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
||||||
|
import PriorityLabel from "../../utils/tasksPriorityLabel.jsx";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Task List Component
|
* Task List Component
|
||||||
@@ -54,47 +54,12 @@ const RemindAtRecord = ({ remindAt }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Priority Label Component
|
|
||||||
* @param priority
|
|
||||||
* @returns {Element}
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
const PriorityLabel = ({ priority }) => {
|
|
||||||
switch (priority) {
|
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
High <ExclamationCircleFilled style={{ marginLeft: "5px", color: "red" }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case 2:
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
Medium <ExclamationCircleFilled style={{ marginLeft: "5px", color: "yellow" }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case 3:
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
Low <ExclamationCircleFilled style={{ marginLeft: "5px", color: "green" }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
None <ExclamationCircleFilled style={{ marginLeft: "5px" }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
// Existing dispatch props...
|
// Existing dispatch props...
|
||||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({});
|
const mapStateToProps = () => ({});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(TaskListComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(TaskListComponent);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useMutation, useQuery } from "@apollo/client";
|
|||||||
import { MUTATION_TOGGLE_TASK_COMPLETED, MUTATION_TOGGLE_TASK_DELETED } from "../../graphql/tasks.queries.js";
|
import { MUTATION_TOGGLE_TASK_COMPLETED, MUTATION_TOGGLE_TASK_DELETED } from "../../graphql/tasks.queries.js";
|
||||||
import { pageLimit } from "../../utils/config.js";
|
import { pageLimit } from "../../utils/config.js";
|
||||||
import AlertComponent from "../alert/alert.component.jsx";
|
import AlertComponent from "../alert/alert.component.jsx";
|
||||||
import React from "react";
|
|
||||||
import TaskListComponent from "./task-list.component.jsx";
|
import TaskListComponent from "./task-list.component.jsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect, useDispatch } from "react-redux";
|
import { connect, useDispatch } from "react-redux";
|
||||||
@@ -20,7 +19,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({});
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(TaskListContainer);
|
export default connect(mapStateToProps, mapDispatchToProps)(TaskListContainer);
|
||||||
|
|
||||||
@@ -55,8 +54,8 @@ export function TaskListContainer({
|
|||||||
bodyshop: bodyshop.id,
|
bodyshop: bodyshop.id,
|
||||||
[relationshipType]: relationshipId,
|
[relationshipType]: relationshipId,
|
||||||
deleted: deleted === "true",
|
deleted: deleted === "true",
|
||||||
completed: completed === "true", //TODO: Find where mine is set.
|
completed: completed === "true",
|
||||||
assigned_to: onlyMine ? bodyshop?.employees?.find((e) => e.user_email === currentUser.email)?.id : undefined, // replace currentUserID with the actual ID of the current user
|
assigned_to: onlyMine ? bodyshop?.employees?.find((e) => e.user_email === currentUser.email)?.id : undefined,
|
||||||
offset: page ? (page - 1) * pageLimit : 0,
|
offset: page ? (page - 1) * pageLimit : 0,
|
||||||
limit: pageLimit,
|
limit: pageLimit,
|
||||||
order: [
|
order: [
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Col, Form, Input, Row, Select, Switch } from "antd";
|
import { Col, Form, Input, Row, Select, Switch } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||||
@@ -8,6 +7,7 @@ import { connect } from "react-redux";
|
|||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
|
||||||
import JobSearchSelectComponent from "../job-search-select/job-search-select.component.jsx";
|
import JobSearchSelectComponent from "../job-search-select/job-search-select.component.jsx";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -42,7 +42,7 @@ export function TaskUpsertModalComponent({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const generatePresets = (job) => {
|
const generatePresets = (job) => {
|
||||||
if (!job || !selectedJobDetails) return datePickerPresets; // return default presets if no job selected
|
if (!job || !selectedJobDetails) return datePickerPresets;
|
||||||
const relativePresets = [];
|
const relativePresets = [];
|
||||||
|
|
||||||
if (selectedJobDetails?.scheduled_completion) {
|
if (selectedJobDetails?.scheduled_completion) {
|
||||||
@@ -97,13 +97,8 @@ export function TaskUpsertModalComponent({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the selected job id
|
|
||||||
* @param jobId
|
|
||||||
*/
|
|
||||||
const changeJobId = (jobId) => {
|
const changeJobId = (jobId) => {
|
||||||
setSelectedJobId(jobId || null);
|
setSelectedJobId(jobId || null);
|
||||||
// Reset the form fields when selectedJobId changes
|
|
||||||
clearRelations();
|
clearRelations();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -163,6 +158,13 @@ export function TaskUpsertModalComponent({
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
extra={
|
||||||
|
existingTask && selectedJobId ? (
|
||||||
|
<div style={{ textAlign: "right" }}>
|
||||||
|
<Link to={`/manage/jobs/${selectedJobId}`}>{t("tasks.labels.go_to_job")}</Link>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<JobSearchSelectComponent
|
<JobSearchSelectComponent
|
||||||
placeholder={t("tasks.placeholders.jobid")}
|
placeholder={t("tasks.placeholders.jobid")}
|
||||||
@@ -203,7 +205,18 @@ export function TaskUpsertModalComponent({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Form.Item label={t("tasks.fields.billid")} name="billid">
|
<Form.Item
|
||||||
|
label={t("tasks.fields.billid")}
|
||||||
|
name="billid"
|
||||||
|
extra={
|
||||||
|
form.getFieldValue("billid") ? (
|
||||||
|
<Link to={`/manage/bills?billid=${form.getFieldValue("billid")}`}>
|
||||||
|
{t("tasks.links.go_to_bill")} (
|
||||||
|
{selectedJobDetails?.bills?.find((bill) => bill.id === form.getFieldValue("billid"))?.invoice_number})
|
||||||
|
</Link>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
allowClear
|
allowClear
|
||||||
placeholder={t("tasks.placeholders.billid")}
|
placeholder={t("tasks.placeholders.billid")}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { Form, Modal } from "antd";
|
import { Form, Modal } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default function TimeTicketCalculatorComponent({
|
|||||||
open={visible}
|
open={visible}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
placement="right"
|
placement="right"
|
||||||
destroyTooltipOnHide
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Button onClick={(e) => e.preventDefault()}>
|
<Button onClick={(e) => e.preventDefault()}>
|
||||||
<Space>
|
<Space>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// SocketProvider.js
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import SocketIO from "socket.io-client";
|
import SocketIO from "socket.io-client";
|
||||||
import { auth } from "../../firebase/firebase.utils";
|
import { auth } from "../../firebase/firebase.utils";
|
||||||
@@ -16,7 +15,9 @@ import {
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { SocketContext, INITIAL_NOTIFICATIONS } from "./useSocket.js";
|
import { INITIAL_NOTIFICATIONS, SocketContext } from "./useSocket.js";
|
||||||
|
|
||||||
|
const LIMIT = INITIAL_NOTIFICATIONS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Socket Provider - Scenario Notifications / Web Socket related items
|
* Socket Provider - Scenario Notifications / Web Socket related items
|
||||||
@@ -157,7 +158,10 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
|||||||
auth: { token, bodyshopId: bodyshop.id },
|
auth: { token, bodyshopId: bodyshop.id },
|
||||||
reconnectionAttempts: Infinity,
|
reconnectionAttempts: Infinity,
|
||||||
reconnectionDelay: 2000,
|
reconnectionDelay: 2000,
|
||||||
reconnectionDelayMax: 10000
|
reconnectionDelayMax: 60000
|
||||||
|
// randomizationFactor: 0.5,
|
||||||
|
// transports: ["websocket", "polling"], // Add this to prefer WebSocket with polling fallback
|
||||||
|
// rememberUpgrade: true
|
||||||
});
|
});
|
||||||
|
|
||||||
socketRef.current = socketInstance;
|
socketRef.current = socketInstance;
|
||||||
@@ -167,6 +171,82 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
|||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "alert-update":
|
case "alert-update":
|
||||||
store.dispatch(addAlerts(message.payload));
|
store.dispatch(addAlerts(message.payload));
|
||||||
|
break;
|
||||||
|
case "task-created":
|
||||||
|
case "task-updated":
|
||||||
|
case "task-deleted":
|
||||||
|
const payload = message.payload;
|
||||||
|
const assignedToId = bodyshop?.employees?.find((e) => e.user_email === currentUser?.email)?.id;
|
||||||
|
if (!assignedToId || payload.assigned_to !== assignedToId) return;
|
||||||
|
|
||||||
|
const dueVars = {
|
||||||
|
bodyshop: bodyshop?.id,
|
||||||
|
assigned_to: assignedToId,
|
||||||
|
order: [{ due_date: "asc" }, { created_at: "desc" }]
|
||||||
|
};
|
||||||
|
const noDueVars = {
|
||||||
|
bodyshop: bodyshop?.id,
|
||||||
|
assigned_to: assignedToId,
|
||||||
|
order: [{ created_at: "desc" }],
|
||||||
|
limit: LIMIT,
|
||||||
|
offset: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const whereBase = {
|
||||||
|
bodyshopid: { _eq: bodyshop?.id },
|
||||||
|
assigned_to: { _eq: assignedToId },
|
||||||
|
deleted: { _eq: false },
|
||||||
|
completed: { _eq: false }
|
||||||
|
};
|
||||||
|
const whereDue = { ...whereBase, due_date: { _is_null: false } };
|
||||||
|
const whereNoDue = { ...whereBase, due_date: { _is_null: true } };
|
||||||
|
|
||||||
|
// Helper to invalidate a cache entry
|
||||||
|
const invalidateCache = (fieldName, args) => {
|
||||||
|
try {
|
||||||
|
client.cache.evict({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fieldName,
|
||||||
|
args
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error invalidating cache:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Invalidate lists and aggregates based on event type
|
||||||
|
if (message.type === "task-deleted" || message.type === "task-updated") {
|
||||||
|
// Invalidate both lists and no due aggregate for deletes and updates
|
||||||
|
invalidateCache("tasks", { where: whereDue, order_by: dueVars.order });
|
||||||
|
invalidateCache("tasks", {
|
||||||
|
where: whereNoDue,
|
||||||
|
order_by: noDueVars.order,
|
||||||
|
limit: noDueVars.limit,
|
||||||
|
offset: noDueVars.offset
|
||||||
|
});
|
||||||
|
invalidateCache("tasks_aggregate", { where: whereNoDue });
|
||||||
|
} else if (message.type === "task-created") {
|
||||||
|
// For creates, invalidate the target list and no due aggregate if applicable
|
||||||
|
const hasDue = !!payload.due_date;
|
||||||
|
if (hasDue) {
|
||||||
|
invalidateCache("tasks", { where: whereDue, order_by: dueVars.order });
|
||||||
|
} else {
|
||||||
|
invalidateCache("tasks", {
|
||||||
|
where: whereNoDue,
|
||||||
|
order_by: noDueVars.order,
|
||||||
|
limit: noDueVars.limit,
|
||||||
|
offset: noDueVars.offset
|
||||||
|
});
|
||||||
|
invalidateCache("tasks_aggregate", { where: whereNoDue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always invalidate the total count for all events (handles creates, deletes, updates including completions)
|
||||||
|
invalidateCache("tasks_aggregate", { where: whereBase });
|
||||||
|
|
||||||
|
// Garbage collect after evictions
|
||||||
|
client.cache.gc();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { createContext, useContext } from "react";
|
|||||||
const SocketContext = createContext(null);
|
const SocketContext = createContext(null);
|
||||||
|
|
||||||
const INITIAL_NOTIFICATIONS = 10;
|
const INITIAL_NOTIFICATIONS = 10;
|
||||||
|
const INITIAL_TASKS = 5;
|
||||||
|
const TASKS_CENTER_POLL_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds
|
||||||
|
|
||||||
const useSocket = () => {
|
const useSocket = () => {
|
||||||
const context = useContext(SocketContext);
|
const context = useContext(SocketContext);
|
||||||
@@ -10,4 +12,4 @@ const useSocket = () => {
|
|||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { SocketContext, INITIAL_NOTIFICATIONS, useSocket };
|
export { SocketContext, INITIAL_NOTIFICATIONS, INITIAL_TASKS, TASKS_CENTER_POLL_INTERVAL, useSocket };
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
|
|||||||
color
|
color
|
||||||
note
|
note
|
||||||
job {
|
job {
|
||||||
|
scheduled_in
|
||||||
|
scheduled_completion
|
||||||
alt_transport
|
alt_transport
|
||||||
ro_number
|
ro_number
|
||||||
ownr_ln
|
ownr_ln
|
||||||
|
|||||||
@@ -312,7 +312,6 @@ export const QUERY_INTAKE_CHECKLIST = gql`
|
|||||||
intakechecklist
|
intakechecklist
|
||||||
status
|
status
|
||||||
owner {
|
owner {
|
||||||
allow_text_message
|
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) {
|
labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const CONVERSATION_SUBSCRIPTION_BY_PK = gql`
|
|||||||
id
|
id
|
||||||
status
|
status
|
||||||
text
|
text
|
||||||
|
is_system
|
||||||
isoutbound
|
isoutbound
|
||||||
image
|
image
|
||||||
image_path
|
image_path
|
||||||
@@ -77,6 +78,7 @@ export const GET_CONVERSATION_DETAILS = gql`
|
|||||||
id
|
id
|
||||||
status
|
status
|
||||||
text
|
text
|
||||||
|
is_system
|
||||||
isoutbound
|
isoutbound
|
||||||
image
|
image
|
||||||
image_path
|
image_path
|
||||||
|
|||||||
@@ -874,7 +874,6 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
|||||||
}
|
}
|
||||||
owner {
|
owner {
|
||||||
id
|
id
|
||||||
allow_text_message
|
|
||||||
preferred_contact
|
preferred_contact
|
||||||
tax_number
|
tax_number
|
||||||
}
|
}
|
||||||
@@ -2071,7 +2070,6 @@ export const QUERY_JOB_CHECKLISTS = gql`
|
|||||||
production_vars
|
production_vars
|
||||||
owner {
|
owner {
|
||||||
id
|
id
|
||||||
allow_text_message
|
|
||||||
}
|
}
|
||||||
bodyshop {
|
bodyshop {
|
||||||
id
|
id
|
||||||
@@ -2428,7 +2426,6 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
|||||||
ownr_ph2
|
ownr_ph2
|
||||||
owner {
|
owner {
|
||||||
id
|
id
|
||||||
allow_text_message
|
|
||||||
preferred_contact
|
preferred_contact
|
||||||
tax_number
|
tax_number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export const QUERY_OWNER_BY_ID = gql`
|
|||||||
owners_by_pk(id: $id) {
|
owners_by_pk(id: $id) {
|
||||||
id
|
id
|
||||||
accountingid
|
accountingid
|
||||||
allow_text_message
|
|
||||||
ownr_addr1
|
ownr_addr1
|
||||||
ownr_addr2
|
ownr_addr2
|
||||||
ownr_co_nm
|
ownr_co_nm
|
||||||
@@ -104,7 +103,6 @@ export const QUERY_ALL_OWNERS = gql`
|
|||||||
query QUERY_ALL_OWNERS {
|
query QUERY_ALL_OWNERS {
|
||||||
owners {
|
owners {
|
||||||
id
|
id
|
||||||
allow_text_message
|
|
||||||
created_at
|
created_at
|
||||||
ownr_addr1
|
ownr_addr1
|
||||||
ownr_addr2
|
ownr_addr2
|
||||||
@@ -129,7 +127,6 @@ export const QUERY_ALL_OWNERS_PAGINATED = gql`
|
|||||||
query QUERY_ALL_OWNERS_PAGINATED($search: String, $offset: Int, $limit: Int, $order: [owners_order_by!]!) {
|
query QUERY_ALL_OWNERS_PAGINATED($search: String, $offset: Int, $limit: Int, $order: [owners_order_by!]!) {
|
||||||
search_owners(args: { search: $search }, offset: $offset, limit: $limit, order_by: $order) {
|
search_owners(args: { search: $search }, offset: $offset, limit: $limit, order_by: $order) {
|
||||||
id
|
id
|
||||||
allow_text_message
|
|
||||||
created_at
|
created_at
|
||||||
ownr_addr1
|
ownr_addr1
|
||||||
ownr_addr2
|
ownr_addr2
|
||||||
|
|||||||
@@ -26,3 +26,39 @@ export const GET_PHONE_NUMBER_OPT_OUTS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const GET_PHONE_NUMBER_OPT_OUTS_BY_NUMBERS = gql`
|
||||||
|
query GET_PHONE_NUMBER_OPT_OUTS_BY_NUMBERS($bodyshopid: uuid!, $phone_numbers: [String!]) {
|
||||||
|
phone_number_opt_out(
|
||||||
|
where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _in: $phone_numbers } }
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
bodyshopid
|
||||||
|
phone_number
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SEARCH_OWNERS_BY_PHONE_NUMBERS = gql`
|
||||||
|
query SEARCH_OWNERS_BY_PHONE_NUMBERS($bodyshopid: uuid!, $phone_numbers: [String!]) {
|
||||||
|
owners(
|
||||||
|
where: {
|
||||||
|
shopid: { _eq: $bodyshopid },
|
||||||
|
_or: [
|
||||||
|
{ ownr_ph1: { _in: $phone_numbers } },
|
||||||
|
{ ownr_ph2: { _in: $phone_numbers } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_co_nm
|
||||||
|
ownr_ph1
|
||||||
|
ownr_ph2
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -67,6 +67,105 @@ export const PARTIAL_TASK_FIELDS = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const PARTIAL_TASK_CENTER_FIELDS = gql`
|
||||||
|
fragment PartialTaskFields on tasks {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
due_date
|
||||||
|
priority
|
||||||
|
jobid
|
||||||
|
job {
|
||||||
|
ro_number
|
||||||
|
}
|
||||||
|
joblineid
|
||||||
|
partsorderid
|
||||||
|
billid
|
||||||
|
remind_at
|
||||||
|
created_at
|
||||||
|
assigned_to
|
||||||
|
bodyshopid
|
||||||
|
deleted
|
||||||
|
completed
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const QUERY_TASKS_WITH_DUE_DATES = gql`
|
||||||
|
${PARTIAL_TASK_CENTER_FIELDS}
|
||||||
|
query QUERY_TASKS_WITH_DUE_DATES($bodyshop: uuid!, $assigned_to: uuid!, $order: [tasks_order_by!]!) {
|
||||||
|
tasks(
|
||||||
|
where: {
|
||||||
|
bodyshopid: { _eq: $bodyshop }
|
||||||
|
assigned_to: { _eq: $assigned_to }
|
||||||
|
deleted: { _eq: false }
|
||||||
|
completed: { _eq: false }
|
||||||
|
due_date: { _is_null: false }
|
||||||
|
}
|
||||||
|
order_by: $order
|
||||||
|
) {
|
||||||
|
...PartialTaskFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export const QUERY_TASKS_NO_DUE_DATE_PAGINATED = gql`
|
||||||
|
${PARTIAL_TASK_CENTER_FIELDS}
|
||||||
|
query QUERY_TASKS_NO_DUE_DATE_PAGINATED(
|
||||||
|
$bodyshop: uuid!
|
||||||
|
$assigned_to: uuid!
|
||||||
|
$order: [tasks_order_by!]!
|
||||||
|
$limit: Int!
|
||||||
|
$offset: Int!
|
||||||
|
) {
|
||||||
|
tasks(
|
||||||
|
where: {
|
||||||
|
bodyshopid: { _eq: $bodyshop }
|
||||||
|
assigned_to: { _eq: $assigned_to }
|
||||||
|
deleted: { _eq: false }
|
||||||
|
completed: { _eq: false }
|
||||||
|
due_date: { _is_null: true }
|
||||||
|
}
|
||||||
|
order_by: $order
|
||||||
|
limit: $limit
|
||||||
|
offset: $offset
|
||||||
|
) {
|
||||||
|
...PartialTaskFields
|
||||||
|
}
|
||||||
|
tasks_aggregate(
|
||||||
|
where: {
|
||||||
|
bodyshopid: { _eq: $bodyshop }
|
||||||
|
assigned_to: { _eq: $assigned_to }
|
||||||
|
deleted: { _eq: false }
|
||||||
|
completed: { _eq: false }
|
||||||
|
due_date: { _is_null: true }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
aggregate {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
/**
|
||||||
|
* Query to get the count of my tasks
|
||||||
|
* @type {DocumentNode}
|
||||||
|
*/
|
||||||
|
export const QUERY_MY_TASKS_COUNT = gql`
|
||||||
|
query QUERY_MY_TASKS_COUNT($assigned_to: uuid!, $bodyshopid: uuid!) {
|
||||||
|
tasks_aggregate(
|
||||||
|
where: {
|
||||||
|
assigned_to: { _eq: $assigned_to }
|
||||||
|
bodyshopid: { _eq: $bodyshopid }
|
||||||
|
completed: { _eq: false }
|
||||||
|
deleted: { _eq: false }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
aggregate {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const QUERY_GET_TASK_BY_ID = gql`
|
export const QUERY_GET_TASK_BY_ID = gql`
|
||||||
${PARTIAL_TASK_FIELDS}
|
${PARTIAL_TASK_FIELDS}
|
||||||
query QUERY_GET_TASK_BY_ID($id: uuid!) {
|
query QUERY_GET_TASK_BY_ID($id: uuid!) {
|
||||||
@@ -287,6 +386,43 @@ export const QUERY_JOB_TASKS_PAGINATED = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const QUERY_MY_ACTIVE_TASKS_PAGINATED = gql`
|
||||||
|
${PARTIAL_TASK_FIELDS}
|
||||||
|
query QUERY_MY_ACTIVE_TASKS_PAGINATED(
|
||||||
|
$assigned_to: uuid!
|
||||||
|
$bodyshop: uuid!
|
||||||
|
$offset: Int
|
||||||
|
$limit: Int
|
||||||
|
$order: [tasks_order_by!]!
|
||||||
|
) {
|
||||||
|
tasks(
|
||||||
|
offset: $offset
|
||||||
|
limit: $limit
|
||||||
|
order_by: $order
|
||||||
|
where: {
|
||||||
|
assigned_to: { _eq: $assigned_to }
|
||||||
|
bodyshopid: { _eq: $bodyshop }
|
||||||
|
deleted: { _eq: false }
|
||||||
|
completed: { _eq: false }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
...TaskFields
|
||||||
|
}
|
||||||
|
tasks_aggregate(
|
||||||
|
where: {
|
||||||
|
assigned_to: { _eq: $assigned_to }
|
||||||
|
bodyshopid: { _eq: $bodyshop }
|
||||||
|
deleted: { _eq: false }
|
||||||
|
completed: { _eq: false }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
aggregate {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const QUERY_MY_TASKS_PAGINATED = gql`
|
export const QUERY_MY_TASKS_PAGINATED = gql`
|
||||||
${PARTIAL_TASK_FIELDS}
|
${PARTIAL_TASK_FIELDS}
|
||||||
query QUERY_MY_TASKS_PAGINATED(
|
query QUERY_MY_TASKS_PAGINATED(
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
|||||||
if (!!!job.ownerid) {
|
if (!!!job.ownerid) {
|
||||||
ownerData = job.owner.data;
|
ownerData = job.owner.data;
|
||||||
ownerData.shopid = bodyshop.id;
|
ownerData.shopid = bodyshop.id;
|
||||||
delete ownerData.allow_text_message;
|
|
||||||
delete ownerData.preferred_contact;
|
delete ownerData.preferred_contact;
|
||||||
delete job.ownerid;
|
delete job.ownerid;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { Badge, Button, Divider, Form, Space, Tabs } from "antd";
|
|||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FaHardHat, FaRegStickyNote, FaShieldAlt, FaTasks } from "react-icons/fa";
|
import { FaHardHat, FaRegStickyNote, FaShieldAlt, FaTasks } from "react-icons/fa";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -28,6 +28,7 @@ import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal
|
|||||||
import JobProfileDataWarning from "../../components/job-profile-data-warning/job-profile-data-warning.component";
|
import JobProfileDataWarning from "../../components/job-profile-data-warning/job-profile-data-warning.component";
|
||||||
import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
|
import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
|
||||||
import JobSyncButton from "../../components/job-sync-button/job-sync-button.component";
|
import JobSyncButton from "../../components/job-sync-button/job-sync-button.component";
|
||||||
|
import JobWatcherToggleContainer from "../../components/job-watcher-toggle/job-watcher-toggle.container.jsx";
|
||||||
import JobsChangeStatus from "../../components/jobs-change-status/jobs-change-status.component";
|
import JobsChangeStatus from "../../components/jobs-change-status/jobs-change-status.component";
|
||||||
import JobsConvertButton from "../../components/jobs-convert-button/jobs-convert-button.component";
|
import JobsConvertButton from "../../components/jobs-convert-button/jobs-convert-button.component";
|
||||||
import JobsDetailDatesComponent from "../../components/jobs-detail-dates/jobs-detail-dates.component";
|
import JobsDetailDatesComponent from "../../components/jobs-detail-dates/jobs-detail-dates.component";
|
||||||
@@ -45,6 +46,8 @@ import LockWrapperComponent from "../../components/lock-wrapper/lock-wrapper.com
|
|||||||
import NoteUpsertModalComponent from "../../components/note-upsert-modal/note-upsert-modal.container";
|
import NoteUpsertModalComponent from "../../components/note-upsert-modal/note-upsert-modal.container";
|
||||||
import ScheduleJobModalContainer from "../../components/schedule-job-modal/schedule-job-modal.container";
|
import ScheduleJobModalContainer from "../../components/schedule-job-modal/schedule-job-modal.container";
|
||||||
import TaskListContainer from "../../components/task-list/task-list.container.jsx";
|
import TaskListContainer from "../../components/task-list/task-list.container.jsx";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { QUERY_PARTS_BILLS_BY_JOBID } from "../../graphql/bills.queries.js";
|
import { QUERY_PARTS_BILLS_BY_JOBID } from "../../graphql/bills.queries.js";
|
||||||
import { QUERY_JOB_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
import { QUERY_JOB_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
@@ -55,9 +58,6 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
|||||||
import { DateTimeFormat } from "../../utils/DateFormatter";
|
import { DateTimeFormat } from "../../utils/DateFormatter";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
|
||||||
import JobWatcherToggleContainer from "../../components/job-watcher-toggle/job-watcher-toggle.container.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -322,11 +322,11 @@ export function JobsDetailPage({
|
|||||||
>
|
>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
// onBack={() => window.history.back()}
|
// onBack={() => window.history.back()}
|
||||||
|
id="job-detail-header"
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
{scenarioNotificationsOn && <JobWatcherToggleContainer job={job} />}
|
{scenarioNotificationsOn && <JobWatcherToggleContainer job={job} />}
|
||||||
{job.ro_number || t("general.labels.na")}
|
<span id="job-ro_number">{job.ro_number || t("general.labels.na")}</span>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
extra={menuExtra}
|
extra={menuExtra}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Icon, { FieldTimeOutlined } from "@ant-design/icons";
|
import Icon, { FieldTimeOutlined } from "@ant-design/icons";
|
||||||
import { Card, Tabs } from "antd";
|
import { Card, Tabs } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FaShieldAlt } from "react-icons/fa";
|
import { FaShieldAlt } from "react-icons/fa";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -78,7 +78,7 @@ export function ScoreboardContainer({ setBreadcrumbs, setSelectedHeader }) {
|
|||||||
<RbacWrapper action="scoreboard:view">
|
<RbacWrapper action="scoreboard:view">
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={tab || "sb"}
|
activeKey={tab || "sb"}
|
||||||
destroyInactiveTabPane
|
destroyOnHidden
|
||||||
onChange={(key) => {
|
onChange={(key) => {
|
||||||
searchParams.tab = key;
|
searchParams.tab = key;
|
||||||
history({
|
history({
|
||||||
|
|||||||
@@ -92,13 +92,15 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Consent Settings tab
|
if (bodyshop.messagingservicesid) {
|
||||||
items.push({
|
// Add Consent Settings tab
|
||||||
key: "consent",
|
items.push({
|
||||||
label: t("bodyshop.labels.consent_settings"),
|
key: "consent",
|
||||||
children: <ShopInfoConsentComponent bodyshop={bodyshop} />
|
label: t("bodyshop.labels.consent_settings"),
|
||||||
});
|
children: <ShopInfoConsentComponent bodyshop={bodyshop} />
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RbacWrapper action="shop:config">
|
<RbacWrapper action="shop:config">
|
||||||
<Tabs activeKey={search.tab} onChange={(key) => history({ search: `?tab=${key}` })} items={items} />
|
<Tabs activeKey={search.tab} onChange={(key) => history({ search: `?tab=${key}` })} items={items} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import TasksPageComponent from "./tasks.page.component";
|
import TasksPageComponent from "./tasks.page.component";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import TasksPageComponent from "./tasks.page.component";
|
import TasksPageComponent from "./tasks.page.component";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import TaskListContainer from "../../components/task-list/task-list.container.jsx";
|
import TaskListContainer from "../../components/task-list/task-list.container.jsx";
|
||||||
import { QUERY_ALL_TASKS_PAGINATED, QUERY_MY_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
import { QUERY_ALL_TASKS_PAGINATED, QUERY_MY_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
||||||
import taskPageTypes from "./taskPageTypes.jsx";
|
import taskPageTypes from "./taskPageTypes.jsx";
|
||||||
@@ -10,7 +9,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({});
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(TasksPageComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(TasksPageComponent);
|
||||||
|
|
||||||
|
|||||||
@@ -335,20 +335,12 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
InstanceRenderManager({
|
window.$crisp.push(["set", "user:company", [payload.shopname]]);
|
||||||
executeFunction: true,
|
window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]);
|
||||||
args: [],
|
if (authRecord[0] && authRecord[0].user.validemail) {
|
||||||
imex: () => {
|
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
||||||
window.$crisp.push(["set", "user:company", [payload.shopname]]);
|
}
|
||||||
window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]);
|
|
||||||
if (authRecord[0] && authRecord[0].user.validemail) {
|
|
||||||
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rome: () => {
|
|
||||||
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
payload.features?.allAccess === true
|
payload.features?.allAccess === true
|
||||||
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
||||||
: (() => {
|
: (() => {
|
||||||
@@ -359,6 +351,14 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
|||||||
);
|
);
|
||||||
window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]);
|
window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
InstanceRenderManager({
|
||||||
|
executeFunction: true,
|
||||||
|
args: [],
|
||||||
|
rome: () => {
|
||||||
|
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Couldnt find $crisp.", error.message);
|
console.warn("Couldnt find $crisp.", error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -775,7 +775,6 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addtoproduction": "Add Job to Production?",
|
"addtoproduction": "Add Job to Production?",
|
||||||
"allow_text_message": "Permission to Text?",
|
|
||||||
"checklist": "Checklist",
|
"checklist": "Checklist",
|
||||||
"printpack": "Job Intake Print Pack",
|
"printpack": "Job Intake Print Pack",
|
||||||
"removefromproduction": "Remove Job from Production?"
|
"removefromproduction": "Remove Job from Production?"
|
||||||
@@ -976,7 +975,10 @@
|
|||||||
"addcomponent": "Add Component"
|
"addcomponent": "Add Component"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
|
"atp": "No Alt. Transport*",
|
||||||
|
"insco": "No Ins. Co.*",
|
||||||
"refreshrequired": "You must refresh the dashboard data to see this component.",
|
"refreshrequired": "You must refresh the dashboard data to see this component.",
|
||||||
|
"status": "No Status*",
|
||||||
"updatinglayout": "Error saving updated layout {{message}}"
|
"updatinglayout": "Error saving updated layout {{message}}"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
@@ -999,6 +1001,8 @@
|
|||||||
"productiondollars": "Total Dollars in Production",
|
"productiondollars": "Total Dollars in Production",
|
||||||
"productionhours": "Total Hours in Production",
|
"productionhours": "Total Hours in Production",
|
||||||
"projectedmonthlysales": "Projected Monthly Sales",
|
"projectedmonthlysales": "Projected Monthly Sales",
|
||||||
|
"scheduleddeliverydate": "Scheduled Delivery Date: {{date}}",
|
||||||
|
"scheduleddeliverytoday": "Scheduled Delivery Today",
|
||||||
"scheduledindate": "Scheduled In Today: {{date}}",
|
"scheduledindate": "Scheduled In Today: {{date}}",
|
||||||
"scheduledintoday": "Scheduled In Today",
|
"scheduledintoday": "Scheduled In Today",
|
||||||
"scheduledoutdate": "Scheduled Out Today: {{date}}",
|
"scheduledoutdate": "Scheduled Out Today: {{date}}",
|
||||||
@@ -1231,7 +1235,11 @@
|
|||||||
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
|
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
|
||||||
"notfound": "No record was found.",
|
"notfound": "No record was found.",
|
||||||
"sizelimit": "The selected items exceed the size limit.",
|
"sizelimit": "The selected items exceed the size limit.",
|
||||||
"submit-for-testing": "Error submitting Job for testing."
|
"submit-for-testing": "Error submitting Job for testing.",
|
||||||
|
"sub_status": {
|
||||||
|
"expired": "The subscription for this shop has expired. Please contact Sales to reactivate.",
|
||||||
|
"trial-expired": "The trial for this shop has expired. Please contact Sales to reactivate."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"itemtypes": {
|
"itemtypes": {
|
||||||
"contract": "CC Contract",
|
"contract": "CC Contract",
|
||||||
@@ -2032,9 +2040,10 @@
|
|||||||
"stands": "Stands",
|
"stands": "Stands",
|
||||||
"waived": "Waived"
|
"waived": "Waived"
|
||||||
},
|
},
|
||||||
"deleteconfirm": "Are you sure you want to delete this Job? This cannot be undone. ",
|
"deleteconfirm": "Are you sure you want to delete this Job? This cannot be undone.",
|
||||||
"deletedelivery": "Delete Delivery Checklist",
|
"deletedelivery": "Delete Delivery Checklist",
|
||||||
"deleteintake": "Delete Intake Checklist",
|
"deleteintake": "Delete Intake Checklist",
|
||||||
|
"deletewatchers": "Remove Watchers before deleting this Job.",
|
||||||
"deliverchecklist": "Deliver Checklist",
|
"deliverchecklist": "Deliver Checklist",
|
||||||
"difference": "Difference",
|
"difference": "Difference",
|
||||||
"diskscan": "Scan Disk for Estimates",
|
"diskscan": "Scan Disk for Estimates",
|
||||||
@@ -2302,8 +2311,10 @@
|
|||||||
"productionlist": "Production Board - List",
|
"productionlist": "Production Board - List",
|
||||||
"readyjobs": "Ready Jobs",
|
"readyjobs": "Ready Jobs",
|
||||||
"recent": "Recent Items",
|
"recent": "Recent Items",
|
||||||
|
"remoteassist": "Remote Assist",
|
||||||
"reportcenter": "Report Center",
|
"reportcenter": "Report Center",
|
||||||
"rescueme": "Rescue me!",
|
"rescueme": "Rescue Me!",
|
||||||
|
"rescuemezoho": "Remote Me In!",
|
||||||
"schedule": "Schedule",
|
"schedule": "Schedule",
|
||||||
"scoreboard": "Scoreboard",
|
"scoreboard": "Scoreboard",
|
||||||
"search": {
|
"search": {
|
||||||
@@ -2379,7 +2390,7 @@
|
|||||||
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
|
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
|
||||||
"noattachedjobs": "No Jobs have been associated to this conversation. ",
|
"noattachedjobs": "No Jobs have been associated to this conversation. ",
|
||||||
"updatinglabel": "Error updating label. {{error}}",
|
"updatinglabel": "Error updating label. {{error}}",
|
||||||
"no_consent": "This phone number has not consented to receive messages."
|
"no_consent": "This phone number has opted-out of Messaging."
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addlabel": "Add a label to this conversation.",
|
"addlabel": "Add a label to this conversation.",
|
||||||
@@ -2396,7 +2407,7 @@
|
|||||||
"sentby": "Sent by {{by}} at {{time}}",
|
"sentby": "Sent by {{by}} at {{time}}",
|
||||||
"typeamessage": "Send a message...",
|
"typeamessage": "Send a message...",
|
||||||
"unarchive": "Unarchive",
|
"unarchive": "Unarchive",
|
||||||
"no_consent": "No Consent"
|
"no_consent": "Opted-out"
|
||||||
},
|
},
|
||||||
"render": {
|
"render": {
|
||||||
"conversation_list": "Conversation List"
|
"conversation_list": "Conversation List"
|
||||||
@@ -2521,7 +2532,6 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"accountingid": "Accounting ID",
|
"accountingid": "Accounting ID",
|
||||||
"address": "Address",
|
"address": "Address",
|
||||||
"allow_text_message": "Permission to Text?",
|
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"note": "Owner Note",
|
"note": "Owner Note",
|
||||||
"ownr_addr1": "Address",
|
"ownr_addr1": "Address",
|
||||||
@@ -3290,6 +3300,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
|
"labels": {
|
||||||
|
"my_tasks_center": "Task Center",
|
||||||
|
"go_to_job": "Go to Job",
|
||||||
|
"overdue": "Overdue",
|
||||||
|
"due_today": "Today",
|
||||||
|
"upcoming": "Upcoming",
|
||||||
|
"no_due_date": "Incomplete",
|
||||||
|
"ro-number": "RO #{{ro_number}}",
|
||||||
|
"no_tasks": "No Tasks Found"
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"edit": "Edit Task",
|
"edit": "Edit Task",
|
||||||
"new": "New Task",
|
"new": "New Task",
|
||||||
@@ -3304,6 +3324,9 @@
|
|||||||
"myTasks": "Mine",
|
"myTasks": "Mine",
|
||||||
"refresh": "Refresh"
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"load_failure": "Failed to load Tasks."
|
||||||
|
},
|
||||||
"date_presets": {
|
"date_presets": {
|
||||||
"completion": "Completion",
|
"completion": "Completion",
|
||||||
"day": "Day",
|
"day": "Day",
|
||||||
@@ -3500,7 +3523,7 @@
|
|||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"dms": "DMS Export",
|
"dms": "DMS Export",
|
||||||
"export-logs": "Export Logs",
|
"export-logs": "Export Logs",
|
||||||
"feature-request": "Feature Requet",
|
"feature-request": "Feature Request",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
"jobs": "Jobs",
|
"jobs": "Jobs",
|
||||||
"jobs-active": "Active Jobs",
|
"jobs-active": "Active Jobs",
|
||||||
@@ -3869,8 +3892,12 @@
|
|||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
"phone_number": "Phone Number",
|
"phone_number": "Phone Number",
|
||||||
"status": "Consent Status",
|
"associated_owners": "Associated Owners",
|
||||||
"created_at": "Created At"
|
"created_at": "Opt-Out Date",
|
||||||
|
"no_owners": "No Associated Owners",
|
||||||
|
"phone_1": "Phone 1",
|
||||||
|
"phone_2": "Phone 2",
|
||||||
|
"text_body": "Users can opt out of receiving SMS messages by replying with keywords such as STOP, UNSUBSCRIBE, CANCEL, END, QUIT, STOPALL, REVOKE and OPTOUT. To opt back in, users can reply with START, YES, or UNSTOP. Even after opting out, users can still send messages to us, which will be received and processed as needed. Ensure customers are informed to reply with these keywords to manage their messaging preferences. After opting out, users receive a confirmation message and will not receive further messages until they opt back in."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Phone Number Opt-Out List"
|
"title": "Phone Number Opt-Out List"
|
||||||
|
|||||||
@@ -656,6 +656,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"consent_settings": "",
|
||||||
"2tiername": "",
|
"2tiername": "",
|
||||||
"2tiersetup": "",
|
"2tiersetup": "",
|
||||||
"2tiersource": "",
|
"2tiersource": "",
|
||||||
@@ -774,7 +775,6 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addtoproduction": "",
|
"addtoproduction": "",
|
||||||
"allow_text_message": "",
|
|
||||||
"checklist": "",
|
"checklist": "",
|
||||||
"printpack": "",
|
"printpack": "",
|
||||||
"removefromproduction": ""
|
"removefromproduction": ""
|
||||||
@@ -975,7 +975,10 @@
|
|||||||
"addcomponent": ""
|
"addcomponent": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
|
"atp": "",
|
||||||
|
"insco": "",
|
||||||
"refreshrequired": "",
|
"refreshrequired": "",
|
||||||
|
"status": "",
|
||||||
"updatinglayout": ""
|
"updatinglayout": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
@@ -998,6 +1001,8 @@
|
|||||||
"productiondollars": "",
|
"productiondollars": "",
|
||||||
"productionhours": "",
|
"productionhours": "",
|
||||||
"projectedmonthlysales": "",
|
"projectedmonthlysales": "",
|
||||||
|
"scheduleddeliverydate": "",
|
||||||
|
"scheduleddeliverytoday": "",
|
||||||
"scheduledindate": "",
|
"scheduledindate": "",
|
||||||
"scheduledintoday": "",
|
"scheduledintoday": "",
|
||||||
"scheduledoutdate": "",
|
"scheduledoutdate": "",
|
||||||
@@ -1230,7 +1235,11 @@
|
|||||||
"fcm": "",
|
"fcm": "",
|
||||||
"notfound": "",
|
"notfound": "",
|
||||||
"sizelimit": "",
|
"sizelimit": "",
|
||||||
"submit-for-testing": ""
|
"submit-for-testing": "",
|
||||||
|
"sub_status": {
|
||||||
|
"expired": "",
|
||||||
|
"trial-expired": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"itemtypes": {
|
"itemtypes": {
|
||||||
"contract": "",
|
"contract": "",
|
||||||
@@ -2034,6 +2043,7 @@
|
|||||||
"deleteconfirm": "",
|
"deleteconfirm": "",
|
||||||
"deletedelivery": "",
|
"deletedelivery": "",
|
||||||
"deleteintake": "",
|
"deleteintake": "",
|
||||||
|
"deletewatchers": "",
|
||||||
"deliverchecklist": "",
|
"deliverchecklist": "",
|
||||||
"difference": "",
|
"difference": "",
|
||||||
"diskscan": "",
|
"diskscan": "",
|
||||||
@@ -2301,8 +2311,10 @@
|
|||||||
"productionlist": "",
|
"productionlist": "",
|
||||||
"readyjobs": "",
|
"readyjobs": "",
|
||||||
"recent": "",
|
"recent": "",
|
||||||
|
"remoteassist": "",
|
||||||
"reportcenter": "",
|
"reportcenter": "",
|
||||||
"rescueme": "",
|
"rescueme": "",
|
||||||
|
"rescuemezoho": "",
|
||||||
"schedule": "Programar",
|
"schedule": "Programar",
|
||||||
"scoreboard": "",
|
"scoreboard": "",
|
||||||
"search": {
|
"search": {
|
||||||
@@ -2377,7 +2389,8 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"invalidphone": "",
|
"invalidphone": "",
|
||||||
"noattachedjobs": "",
|
"noattachedjobs": "",
|
||||||
"updatinglabel": ""
|
"updatinglabel": "",
|
||||||
|
"no_consent": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addlabel": "",
|
"addlabel": "",
|
||||||
@@ -2393,7 +2406,8 @@
|
|||||||
"selectmedia": "",
|
"selectmedia": "",
|
||||||
"sentby": "",
|
"sentby": "",
|
||||||
"typeamessage": "Enviar un mensaje...",
|
"typeamessage": "Enviar un mensaje...",
|
||||||
"unarchive": ""
|
"unarchive": "",
|
||||||
|
"no_consent": ""
|
||||||
},
|
},
|
||||||
"render": {
|
"render": {
|
||||||
"conversation_list": ""
|
"conversation_list": ""
|
||||||
@@ -2498,7 +2512,8 @@
|
|||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"job-watchers": "",
|
"job-watchers": "",
|
||||||
"not-employee": ""
|
"not-employee": "",
|
||||||
|
"not-employee-notifications": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
@@ -2519,7 +2534,6 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"accountingid": "",
|
"accountingid": "",
|
||||||
"address": "Dirección",
|
"address": "Dirección",
|
||||||
"allow_text_message": "Permiso de texto?",
|
|
||||||
"name": "Nombre",
|
"name": "Nombre",
|
||||||
"note": "",
|
"note": "",
|
||||||
"ownr_addr1": "Dirección",
|
"ownr_addr1": "Dirección",
|
||||||
@@ -3288,6 +3302,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
|
"labels": {
|
||||||
|
"my_tasks_center": "",
|
||||||
|
"go_to_job": "",
|
||||||
|
"overdue": "",
|
||||||
|
"due_today": "",
|
||||||
|
"upcoming": "",
|
||||||
|
"no_due_date": "",
|
||||||
|
"ro-number": "",
|
||||||
|
"no_tasks": ""
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"edit": "",
|
"edit": "",
|
||||||
"new": "",
|
"new": "",
|
||||||
@@ -3302,6 +3326,9 @@
|
|||||||
"myTasks": "",
|
"myTasks": "",
|
||||||
"refresh": ""
|
"refresh": ""
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"load_failure": ""
|
||||||
|
},
|
||||||
"date_presets": {
|
"date_presets": {
|
||||||
"completion": "",
|
"completion": "",
|
||||||
"day": "",
|
"day": "",
|
||||||
@@ -3864,6 +3891,18 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": ""
|
"unique_vendor_name": ""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"consent": {
|
||||||
|
"phone_number": "",
|
||||||
|
"associated_owners": "",
|
||||||
|
"created_at": "",
|
||||||
|
"no_owners": "",
|
||||||
|
"phone_1": "",
|
||||||
|
"phone_2": "",
|
||||||
|
"text_body": ""
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -656,6 +656,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"consent_settings": "",
|
||||||
"2tiername": "",
|
"2tiername": "",
|
||||||
"2tiersetup": "",
|
"2tiersetup": "",
|
||||||
"2tiersource": "",
|
"2tiersource": "",
|
||||||
@@ -774,7 +775,6 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addtoproduction": "",
|
"addtoproduction": "",
|
||||||
"allow_text_message": "",
|
|
||||||
"checklist": "",
|
"checklist": "",
|
||||||
"printpack": "",
|
"printpack": "",
|
||||||
"removefromproduction": ""
|
"removefromproduction": ""
|
||||||
@@ -975,7 +975,10 @@
|
|||||||
"addcomponent": ""
|
"addcomponent": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
|
"atp": "",
|
||||||
|
"insco": "",
|
||||||
"refreshrequired": "",
|
"refreshrequired": "",
|
||||||
|
"status": "",
|
||||||
"updatinglayout": ""
|
"updatinglayout": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
@@ -998,6 +1001,8 @@
|
|||||||
"productiondollars": "",
|
"productiondollars": "",
|
||||||
"productionhours": "",
|
"productionhours": "",
|
||||||
"projectedmonthlysales": "",
|
"projectedmonthlysales": "",
|
||||||
|
"scheduleddeliverydate": "",
|
||||||
|
"scheduleddeliverytoday": "",
|
||||||
"scheduledindate": "",
|
"scheduledindate": "",
|
||||||
"scheduledintoday": "",
|
"scheduledintoday": "",
|
||||||
"scheduledoutdate": "",
|
"scheduledoutdate": "",
|
||||||
@@ -1230,7 +1235,11 @@
|
|||||||
"fcm": "",
|
"fcm": "",
|
||||||
"notfound": "",
|
"notfound": "",
|
||||||
"sizelimit": "",
|
"sizelimit": "",
|
||||||
"submit-for-testing": ""
|
"submit-for-testing": "",
|
||||||
|
"sub_status": {
|
||||||
|
"expired": "",
|
||||||
|
"trial-expired": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"itemtypes": {
|
"itemtypes": {
|
||||||
"contract": "",
|
"contract": "",
|
||||||
@@ -2034,6 +2043,7 @@
|
|||||||
"deleteconfirm": "",
|
"deleteconfirm": "",
|
||||||
"deletedelivery": "",
|
"deletedelivery": "",
|
||||||
"deleteintake": "",
|
"deleteintake": "",
|
||||||
|
"deletewatchers": "",
|
||||||
"deliverchecklist": "",
|
"deliverchecklist": "",
|
||||||
"difference": "",
|
"difference": "",
|
||||||
"diskscan": "",
|
"diskscan": "",
|
||||||
@@ -2301,8 +2311,10 @@
|
|||||||
"productionlist": "",
|
"productionlist": "",
|
||||||
"readyjobs": "",
|
"readyjobs": "",
|
||||||
"recent": "",
|
"recent": "",
|
||||||
|
"remoteassist": "",
|
||||||
"reportcenter": "",
|
"reportcenter": "",
|
||||||
"rescueme": "",
|
"rescueme": "",
|
||||||
|
"rescuemezoho": "",
|
||||||
"schedule": "Programme",
|
"schedule": "Programme",
|
||||||
"scoreboard": "",
|
"scoreboard": "",
|
||||||
"search": {
|
"search": {
|
||||||
@@ -2377,7 +2389,8 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"invalidphone": "",
|
"invalidphone": "",
|
||||||
"noattachedjobs": "",
|
"noattachedjobs": "",
|
||||||
"updatinglabel": ""
|
"updatinglabel": "",
|
||||||
|
"no_consent": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addlabel": "",
|
"addlabel": "",
|
||||||
@@ -2393,7 +2406,8 @@
|
|||||||
"selectmedia": "",
|
"selectmedia": "",
|
||||||
"sentby": "",
|
"sentby": "",
|
||||||
"typeamessage": "Envoyer un message...",
|
"typeamessage": "Envoyer un message...",
|
||||||
"unarchive": ""
|
"unarchive": "",
|
||||||
|
"no_consent": ""
|
||||||
},
|
},
|
||||||
"render": {
|
"render": {
|
||||||
"conversation_list": ""
|
"conversation_list": ""
|
||||||
@@ -2498,7 +2512,8 @@
|
|||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"job-watchers": "",
|
"job-watchers": "",
|
||||||
"not-employee": ""
|
"not-employee": "",
|
||||||
|
"not-employee-notifications": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
@@ -2519,7 +2534,6 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"accountingid": "",
|
"accountingid": "",
|
||||||
"address": "Adresse",
|
"address": "Adresse",
|
||||||
"allow_text_message": "Autorisation de texte?",
|
|
||||||
"name": "Prénom",
|
"name": "Prénom",
|
||||||
"note": "",
|
"note": "",
|
||||||
"ownr_addr1": "Adresse",
|
"ownr_addr1": "Adresse",
|
||||||
@@ -3288,6 +3302,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
|
"labels": {
|
||||||
|
"my_tasks_center": "",
|
||||||
|
"go_to_job": "",
|
||||||
|
"overdue": "",
|
||||||
|
"due_today": "",
|
||||||
|
"upcoming": "",
|
||||||
|
"no_due_date": "",
|
||||||
|
"ro-number": "",
|
||||||
|
"no_tasks": ""
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"edit": "",
|
"edit": "",
|
||||||
"new": "",
|
"new": "",
|
||||||
@@ -3302,6 +3326,9 @@
|
|||||||
"myTasks": "",
|
"myTasks": "",
|
||||||
"refresh": ""
|
"refresh": ""
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"load_failure": ""
|
||||||
|
},
|
||||||
"date_presets": {
|
"date_presets": {
|
||||||
"completion": "",
|
"completion": "",
|
||||||
"day": "",
|
"day": "",
|
||||||
@@ -3864,6 +3891,18 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": ""
|
"unique_vendor_name": ""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"consent": {
|
||||||
|
"phone_number": "Phone Number",
|
||||||
|
"associated_owners": "Associated Owners",
|
||||||
|
"created_at": "Opt-Out Date",
|
||||||
|
"no_owners": "No Associated Owners",
|
||||||
|
"phone_1": "Phone 1",
|
||||||
|
"phone_2": "Phone 2",
|
||||||
|
"text_body": ""
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Tooltip } from "antd";
|
import { Tooltip } from "antd";
|
||||||
import dayjs from "../utils/day";
|
import dayjs from "../utils/day";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export function DateFormatter(props) {
|
export function DateFormatter(props) {
|
||||||
return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : null;
|
return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : null;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user