Compare commits
205 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 | ||
|
|
d52426f5f5 | ||
|
|
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 | ||
|
|
2c508cf1a1 | ||
|
|
16a91c772a | ||
|
|
5c47088b11 | ||
|
|
8e5dc4fa71 | ||
|
|
39c3729f6d | ||
|
|
e3d854e02b | ||
|
|
618acf2acf | ||
|
|
2cf2b70293 | ||
|
|
0541afceb8 | ||
|
|
28ed3f9936 | ||
|
|
6afa50332b | ||
|
|
8c8c68867d | ||
|
|
8ee52598e8 | ||
|
|
c822028174 | ||
|
|
36b82c6195 | ||
|
|
079dffce4d | ||
|
|
831802f5af | ||
|
|
7bd5190bf2 | ||
|
|
83860152a9 | ||
|
|
1e10493615 | ||
|
|
9d81c68a4d | ||
|
|
985d066978 | ||
|
|
6ad9e27d1d | ||
|
|
19ebdda5b3 | ||
|
|
4602dd1183 | ||
|
|
6005eaee6a | ||
|
|
6d59e3994f | ||
|
|
f770b2f1b1 | ||
|
|
b014744940 | ||
|
|
714c90c25e | ||
|
|
9a3a971da6 | ||
|
|
96cba0aaab | ||
|
|
c069600cfd | ||
|
|
186cbf2c97 | ||
|
|
392988ae11 | ||
|
|
2e33b79eb9 | ||
|
|
d4f718c44c | ||
|
|
fa99ef7b37 | ||
|
|
c4aff1b516 | ||
|
|
61276bb2d1 | ||
|
|
8b89e2eb9d | ||
|
|
9ab41308e7 | ||
|
|
f76052ec9b | ||
|
|
b8841e3ded | ||
|
|
a49b3f6496 | ||
|
|
3e17ec3cf8 | ||
|
|
76c0c7c41e | ||
|
|
025b986f60 | ||
|
|
6e6addd62f | ||
|
|
266c3acf34 | ||
|
|
c4631f50e5 | ||
|
|
ca18291425 | ||
|
|
110fad2abc | ||
|
|
b7456cecd4 | ||
|
|
84db1fe81b | ||
|
|
a9814c1eb1 | ||
|
|
3fe0e3a33c | ||
|
|
12c87ed689 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -130,3 +130,5 @@ test-output.txt
|
||||
server/job/test/fixtures
|
||||
|
||||
.github
|
||||
_reference/ragmate/.ragmate.env
|
||||
docker_data
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
client_max_body_size 50M;
|
||||
client_body_buffer_size 5M;
|
||||
client_body_buffer_size 5M;
|
||||
@@ -12791,27 +12791,6 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</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>
|
||||
<name>checklist</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -42614,27 +42593,6 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</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>
|
||||
<name>name</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
713
client/package-lock.json
generated
713
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,21 +12,21 @@
|
||||
"@apollo/client": "^3.13.6",
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||
"@firebase/analytics": "^0.10.12",
|
||||
"@firebase/app": "^0.11.4",
|
||||
"@firebase/auth": "^1.10.0",
|
||||
"@firebase/firestore": "^4.7.10",
|
||||
"@firebase/messaging": "^0.12.17",
|
||||
"@firebase/analytics": "^0.10.16",
|
||||
"@firebase/app": "^0.13.1",
|
||||
"@firebase/auth": "^1.10.6",
|
||||
"@firebase/firestore": "^4.7.17",
|
||||
"@firebase/messaging": "^0.12.21",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.6.1",
|
||||
"@sentry/cli": "^2.44.0",
|
||||
"@sentry/react": "^9.15.0",
|
||||
"@sentry/vite-plugin": "^3.4.0",
|
||||
"@splitsoftware/splitio-react": "^2.1.1",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@sentry/cli": "^2.47.1",
|
||||
"@sentry/react": "^9.38.0",
|
||||
"@sentry/vite-plugin": "^3.5.0",
|
||||
"@splitsoftware/splitio-react": "^2.3.1",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"antd": "^5.24.9",
|
||||
"antd": "^5.25.4",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^4.2.0",
|
||||
"apollo-link-sentry": "^4.3.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.8.4",
|
||||
"classnames": "^2.5.1",
|
||||
@@ -41,24 +41,25 @@
|
||||
"i18next": "^24.2.3",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.12.6",
|
||||
"libphonenumber-js": "^1.12.10",
|
||||
"logrocket": "^9.0.2",
|
||||
"markerjs2": "^2.32.4",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.1",
|
||||
"normalize-url": "^8.0.2",
|
||||
"object-hash": "^3.0.0",
|
||||
"phone": "^3.1.59",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.1.2",
|
||||
"query-string": "^9.2.0",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.18.0",
|
||||
"react-big-calendar": "^1.19.2",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^8.0.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drag-listview": "^2.0.0",
|
||||
"react-grid-gallery": "^1.0.1",
|
||||
"react-grid-layout": "1.3.4",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-i18next": "^15.5.2",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -69,7 +70,7 @@
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.30.0",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-virtuoso": "^4.12.7",
|
||||
"react-virtuoso": "^4.12.8",
|
||||
"recharts": "^2.15.2",
|
||||
"redux": "^5.0.1",
|
||||
"redux-actions": "^3.0.3",
|
||||
@@ -77,9 +78,9 @@
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.86.3",
|
||||
"sass": "^1.89.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"styled-components": "^6.1.17",
|
||||
"styled-components": "^6.1.18",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"use-memo-one": "^1.1.3",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
@@ -130,17 +131,17 @@
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@dotenvx/dotenvx": "^1.43.0",
|
||||
"@dotenvx/dotenvx": "^1.47.5",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@sentry/webpack-plugin": "^3.4.0",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@playwright/test": "^1.54.1",
|
||||
"@sentry/webpack-plugin": "^3.5.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"browserslist": "^4.24.5",
|
||||
"@vitejs/plugin-react": "^4.5.1",
|
||||
"browserslist": "^4.25.0",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"chalk": "^5.4.1",
|
||||
"eslint": "^8.57.1",
|
||||
@@ -148,9 +149,9 @@
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"memfs": "^4.17.1",
|
||||
"memfs": "^4.17.2",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.51.1",
|
||||
"playwright": "^1.54.1",
|
||||
"react-error-overlay": "^6.1.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
@@ -160,7 +161,7 @@
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
"vitest": "^3.1.3",
|
||||
"vitest": "^3.2.3",
|
||||
"workbox-window": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,21 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "partsOrder"
|
||||
})
|
||||
),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid,
|
||||
operation,
|
||||
type
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
|
||||
@@ -69,7 +82,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, b
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={() => setOpen(false)}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
title={t("bills.actions.return")}
|
||||
onOk={() => form.submit()}
|
||||
>
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function BillDetailEditcontainer() {
|
||||
delete search.billid;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
open={search.billid}
|
||||
>
|
||||
<BillDetailEditComponent />
|
||||
|
||||
@@ -2,10 +2,11 @@ import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Checkbox, Form, Modal, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
||||
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.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 { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-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({
|
||||
billEnterModal: selectBillEnterModal,
|
||||
@@ -53,10 +54,10 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
||||
const notification = useNotification();
|
||||
|
||||
const {
|
||||
treatments: { Enhanced_Payroll }
|
||||
treatments: { Enhanced_Payroll, Imgproxy }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Enhanced_Payroll"],
|
||||
names: ["Enhanced_Payroll", "Imgproxy"],
|
||||
splitKey: bodyshop.imexshopid
|
||||
});
|
||||
|
||||
@@ -196,7 +197,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
||||
job: { lbr_adjustments: newAdjustments }
|
||||
}
|
||||
});
|
||||
if (!!jobUpdate.errors) {
|
||||
if (jobUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.saving", {
|
||||
message: JSON.stringify(jobUpdate.errors)
|
||||
@@ -213,7 +214,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
||||
variables: { partsLineIds: markPolReceived.map((p) => p.id) },
|
||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"]
|
||||
});
|
||||
if (!!r2.errors) {
|
||||
if (r2.errors) {
|
||||
setLoading(false);
|
||||
setEnterAgain(false);
|
||||
notification["error"]({
|
||||
@@ -224,7 +225,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
||||
}
|
||||
}
|
||||
|
||||
if (!!r1.errors) {
|
||||
if (r1.errors) {
|
||||
setLoading(false);
|
||||
setEnterAgain(false);
|
||||
notification["error"]({
|
||||
@@ -244,7 +245,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
||||
consumedbybillid: billId
|
||||
}
|
||||
});
|
||||
if (!!r2.errors) {
|
||||
if (r2.errors) {
|
||||
setLoading(false);
|
||||
setEnterAgain(false);
|
||||
notification["error"]({
|
||||
@@ -298,20 +299,39 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
upload.forEach((u) => {
|
||||
handleUpload(
|
||||
{ file: u.originFileObj },
|
||||
{
|
||||
bodyshop: bodyshop,
|
||||
uploaded_by: currentUser.email,
|
||||
jobId: values.jobid,
|
||||
billId: billId,
|
||||
tagsArray: null,
|
||||
callback: null
|
||||
},
|
||||
notification
|
||||
);
|
||||
});
|
||||
//Check if using Imgproxy or cloudinary
|
||||
|
||||
if (Imgproxy.treatment === "on") {
|
||||
upload.forEach((u) => {
|
||||
handleUploadToImageProxy(
|
||||
{ file: u.originFileObj },
|
||||
{
|
||||
bodyshop: bodyshop,
|
||||
uploaded_by: currentUser.email,
|
||||
jobId: values.jobid,
|
||||
billId: billId,
|
||||
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")}
|
||||
</Checkbox>
|
||||
<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")}
|
||||
</Button>
|
||||
{billEnterModal.context && billEnterModal.context.id ? null : (
|
||||
@@ -406,13 +426,14 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
||||
onClick={() => {
|
||||
setEnterAgain(true);
|
||||
}}
|
||||
id="save-and-new-bill-enter-modal"
|
||||
>
|
||||
{t("general.actions.saveandnew")}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Select } from "antd";
|
||||
import React, { forwardRef } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
||||
|
||||
@@ -43,7 +43,7 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
|
||||
label: (
|
||||
<div style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
|
||||
<div style={{ whiteSpace: "normal", wordBreak: "break-word" }}>
|
||||
<span>
|
||||
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import { connect } from "react-redux";
|
||||
@@ -209,6 +209,7 @@ export function BillsListTableComponent({
|
||||
}
|
||||
});
|
||||
}}
|
||||
id="reconcile-bills-button"
|
||||
>
|
||||
<LockerWrapperComponent featureName="bills"> {t("jobs.actions.reconcile")}</LockerWrapperComponent>
|
||||
</Button>
|
||||
|
||||
@@ -75,7 +75,7 @@ export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisi
|
||||
title={t("payments.labels.findermodal")}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
onOk={() => toggleModalVisible()}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
forceRender
|
||||
>
|
||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button, Form, InputNumber, Popover, Space } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export default function CABCpvrtCalculator({ disabled, form }) {
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
|
||||
@@ -39,7 +40,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
|
||||
<Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
|
||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||
<CalculatorFilled />
|
||||
</Button>
|
||||
|
||||
@@ -40,7 +40,7 @@ function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodys
|
||||
</Button>
|
||||
]}
|
||||
width="80%"
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
>
|
||||
<CardPaymentModalComponent />
|
||||
</Modal>
|
||||
|
||||
@@ -34,16 +34,14 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
|
||||
SubscribeToTopicForFCMNotification();
|
||||
|
||||
//Register WS handlers
|
||||
// Register WebSocket handlers
|
||||
if (socket && socket.connected) {
|
||||
registerMessagingHandlers({ socket, client });
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (socket && socket.connected) {
|
||||
return () => {
|
||||
unregisterMessagingHandlers({ socket });
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}, [bodyshop, socket, t, client]);
|
||||
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||
|
||||
@@ -202,8 +202,6 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
||||
text: message.text
|
||||
};
|
||||
|
||||
// Add cases for other known message types as needed
|
||||
|
||||
default:
|
||||
// Log a warning for unhandled message types
|
||||
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
|
||||
? [
|
||||
newConversation,
|
||||
...existingList.conversations.filter((conv) => conv.id !== newConversation.id) // Prevent duplicates
|
||||
]
|
||||
: [newConversation];
|
||||
? [newConversation, ...existingList.conversations.filter((conv) => conv.id !== newConversation.id)]
|
||||
: [newConversation]; // Prevent duplicates
|
||||
|
||||
client.cache.writeQuery({
|
||||
query: CONVERSATION_LIST_QUERY,
|
||||
@@ -403,6 +398,7 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
logLocal("handleConversationChanged - Unhandled type", { type });
|
||||
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-detailed", handleNewMessageDetailed);
|
||||
socket.on("message-changed", handleMessageChanged);
|
||||
socket.on("conversation-changed", handleConversationChanged);
|
||||
socket.on("phone-number-opted-out", handlePhoneNumberOptedOut);
|
||||
socket.on("phone-number-opted-in", handlePhoneNumberOptedIn);
|
||||
};
|
||||
|
||||
export const unregisterMessagingHandlers = ({ socket }) => {
|
||||
@@ -431,4 +512,6 @@ export const unregisterMessagingHandlers = ({ socket }) => {
|
||||
socket.off("new-message-detailed");
|
||||
socket.off("message-changed");
|
||||
socket.off("conversation-changed");
|
||||
socket.off("phone-number-opted-out");
|
||||
socket.off("phone-number-opted-in");
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Badge, Card, List, Space, Tag } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Badge, Card, List, Space, Tag, Tooltip } from "antd";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -9,36 +9,62 @@ import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import _ from "lodash";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import "./chat-conversation-list.styles.scss";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries.js";
|
||||
import { phone } from "phone";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation
|
||||
selectedConversation: selectSelectedConversation,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
||||
});
|
||||
|
||||
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) {
|
||||
// That comma is there for a reason, do not remove it
|
||||
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [, forceUpdate] = useState(false);
|
||||
|
||||
// Re-render every minute
|
||||
const phoneNumbers = conversationList.map((item) => phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""));
|
||||
|
||||
const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
|
||||
variables: {
|
||||
bodyshopid: bodyshop.id,
|
||||
phone_numbers: phoneNumbers
|
||||
},
|
||||
skip: !conversationList.length,
|
||||
fetchPolicy: "cache-and-network"
|
||||
});
|
||||
|
||||
const optOutMap = useMemo(() => {
|
||||
const map = new Map();
|
||||
optOutData?.phone_number_opt_out?.forEach((optOut) => {
|
||||
map.set(optOut.phone_number, true);
|
||||
});
|
||||
return map;
|
||||
}, [optOutData?.phone_number_opt_out]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
forceUpdate((prev) => !prev); // Toggle state to trigger re-render
|
||||
}, 60000); // 1 minute in milliseconds
|
||||
|
||||
return () => clearInterval(interval); // Cleanup on unmount
|
||||
forceUpdate((prev) => !prev);
|
||||
}, 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Memoize the sorted conversation list
|
||||
const sortedConversationList = React.useMemo(() => {
|
||||
const sortedConversationList = useMemo(() => {
|
||||
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
|
||||
}, [conversationList]);
|
||||
|
||||
const renderConversation = (index) => {
|
||||
const renderConversation = (index, t) => {
|
||||
const item = sortedConversationList[index];
|
||||
const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
|
||||
const hasOptOutEntry = optOutMap.has(normalizedPhone);
|
||||
|
||||
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||
const cardContentLeft =
|
||||
item.job_conversations.length > 0
|
||||
@@ -60,7 +86,18 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
||||
</>
|
||||
);
|
||||
|
||||
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count} />;
|
||||
const cardExtra = (
|
||||
<>
|
||||
<Badge count={item.messages_aggregate.aggregate.count} />
|
||||
{hasOptOutEntry && (
|
||||
<Tooltip title={t("consent.text_body")}>
|
||||
<Tag color="red" icon={<ExclamationCircleOutlined />}>
|
||||
{t("messaging.labels.no_consent")}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const getCardStyle = () =>
|
||||
item.id === selectedConversation
|
||||
@@ -73,9 +110,25 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
||||
>
|
||||
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
||||
<Card style={getCardStyle()} variant={true} size="small" extra={cardExtra} title={cardTitle}>
|
||||
<div
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "70%",
|
||||
textAlign: "left"
|
||||
}}
|
||||
>
|
||||
{cardContentLeft}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "30%",
|
||||
textAlign: "right"
|
||||
}}
|
||||
>
|
||||
{cardContentRight}
|
||||
</div>
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
@@ -85,7 +138,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
||||
<div className="chat-list-container">
|
||||
<Virtuoso
|
||||
data={sortedConversationList}
|
||||
itemContent={(index) => renderConversation(index)}
|
||||
itemContent={(index) => renderConversation(index, t)}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
/* Add spacing and better alignment for items */
|
||||
.chat-list-item {
|
||||
padding: 0.5rem 0; /* Add spacing between list items */
|
||||
padding: 0.2rem 0; /* Add spacing between list items */
|
||||
|
||||
.ant-card {
|
||||
border-radius: 8px; /* Slight rounding for card edges */
|
||||
|
||||
@@ -58,6 +58,7 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
||||
userid
|
||||
created_at
|
||||
read
|
||||
is_system
|
||||
}
|
||||
`,
|
||||
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 JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import "./chat-media-selector.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
||||
|
||||
export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
|
||||
@@ -37,9 +38,8 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
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
|
||||
});
|
||||
|
||||
@@ -56,25 +56,25 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
||||
//If Imageproxy is on, rely only on the LMS selector
|
||||
//If not on, use the old methods.
|
||||
const content = (
|
||||
<div>
|
||||
<div className="media-selector-content">
|
||||
{loading && <LoadingSpinner />}
|
||||
{error && <AlertComponent message={error.message} type="error" />}
|
||||
{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}
|
||||
|
||||
{Imgproxy.treatment === "on" ? (
|
||||
<>
|
||||
{!bodyshop.uselocalmediaserver && (
|
||||
<JobsDocumentImgproxyGalleryExternal
|
||||
jobId={conversation.job_conversations[0].jobid}
|
||||
jobId={conversation.job_conversations[0]?.jobid}
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
/>
|
||||
)}
|
||||
{bodyshop.uselocalmediaserver && open && (
|
||||
<JobDocumentsLocalGalleryExternal
|
||||
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 && (
|
||||
<JobDocumentsLocalGalleryExternal
|
||||
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 (
|
||||
<Popover
|
||||
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")}
|
||||
trigger="click"
|
||||
open={open}
|
||||
onOpenChange={handleVisibleChange}
|
||||
classNames={{ root: "media-selector-popover" }}
|
||||
>
|
||||
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
||||
<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%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.archive-button {
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -37,11 +40,13 @@
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
.chat-send-message-button{
|
||||
|
||||
.chat-send-message-button {
|
||||
margin: 0.3rem;
|
||||
padding-left: 0.5rem;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.message-icon {
|
||||
position: absolute;
|
||||
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 {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
@@ -2,17 +2,29 @@ import Icon from "@ant-design/icons";
|
||||
import { Tooltip } from "antd";
|
||||
import i18n from "i18next";
|
||||
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";
|
||||
|
||||
export const renderMessage = (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 (
|
||||
<div key={index} className={`${message.isoutbound ? "mine messages" : "yours messages"}`}>
|
||||
<div key={index} className={messageClass}>
|
||||
<div className="message msgmargin">
|
||||
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<div>
|
||||
{isSystem && <span className="system-label">System</span>}
|
||||
{/* Render images if available */}
|
||||
{message.image && message.image_path?.length > 0 && (
|
||||
<div className="message-images">
|
||||
@@ -26,20 +38,31 @@ export const renderMessage = (messages, index) => {
|
||||
</div>
|
||||
)}
|
||||
{/* 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>
|
||||
</Tooltip>
|
||||
|
||||
{/* Message status icons */}
|
||||
{message.status && (message.status === "sent" || message.status === "delivered") && (
|
||||
<div className="message-status">
|
||||
<Icon component={message.status === "sent" ? MdDone : MdDoneAll} className="message-icon" />
|
||||
</div>
|
||||
)}
|
||||
{/* Message status icons for non-system messages */}
|
||||
{!isSystem &&
|
||||
message.status &&
|
||||
(message.status === "sent" || message.status === "delivered" || message.status === "failed") && (
|
||||
<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>
|
||||
|
||||
{/* Outbound message metadata */}
|
||||
{message.isoutbound && (
|
||||
{/* Outbound message metadata for non-system messages */}
|
||||
{!isSystem && message.isoutbound && (
|
||||
<div style={{ fontSize: 10 }}>
|
||||
{i18n.t("messaging.labels.sentby", {
|
||||
by: message.userid,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
||||
import { Input, Spin } from "antd";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ExclamationCircleOutlined, LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
||||
import { Alert, Input, Space, Spin, Tooltip } from "antd";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -10,6 +10,9 @@ import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
|
||||
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { phone } from "phone";
|
||||
import { GET_PHONE_NUMBER_OPT_OUT } from "../../graphql/phone-number-opt-out.queries";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -25,16 +28,24 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
||||
const inputArea = useRef(null);
|
||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const normalizedPhone = phone(conversation.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
|
||||
const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUT, {
|
||||
variables: { bodyshopid: bodyshop.id, phone_number: normalizedPhone },
|
||||
fetchPolicy: "cache-and-network"
|
||||
});
|
||||
|
||||
const isOptedOut = !!optOutData?.phone_number_opt_out?.[0];
|
||||
|
||||
useEffect(() => {
|
||||
inputArea.current.focus();
|
||||
}, [isSending, setMessage]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleEnter = () => {
|
||||
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
||||
if ((message === "" || !message) && selectedImages.length === 0) return;
|
||||
if (isOptedOut) return; // Prevent sending if phone number is opted out
|
||||
logImEXEvent("messaging_send_message");
|
||||
|
||||
if (selectedImages.length < 11) {
|
||||
@@ -44,7 +55,8 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
||||
messagingServiceSid: bodyshop.messagingservicesid,
|
||||
conversationid: conversation.id,
|
||||
selectedMedia: selectedImages,
|
||||
imexshopid: bodyshop.imexshopid
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
bodyshopid: bodyshop.id
|
||||
};
|
||||
sendMessage(newMessage);
|
||||
setSelectedMedia(
|
||||
@@ -56,47 +68,67 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="imex-flex-row" style={{ width: "100%" }}>
|
||||
<ChatPresetsComponent className="imex-flex-row__margin" />
|
||||
<ChatMediaSelector
|
||||
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}
|
||||
placeholder={t("messaging.labels.typeamessage")}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onPressEnter={(event) => {
|
||||
event.preventDefault();
|
||||
if (!!!event.shiftKey) handleEnter();
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<SendOutlined
|
||||
className="chat-send-message-button"
|
||||
// disabled={message === "" || !message}
|
||||
onClick={handleEnter}
|
||||
/>
|
||||
<Spin
|
||||
style={{ display: `${isSending ? "" : "none"}` }}
|
||||
indicator={
|
||||
<LoadingOutlined
|
||||
style={{
|
||||
fontSize: 24
|
||||
}}
|
||||
spin
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
||||
{isOptedOut && (
|
||||
<Tooltip title={t("consent.text_body")}>
|
||||
<Alert
|
||||
showIcon={true}
|
||||
icon={<ExclamationCircleOutlined />}
|
||||
message={t("messaging.errors.no_consent")}
|
||||
type="error"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<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 { useTranslation } from "react-i18next";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const ContractStatusComponent = ({ value, onChange }, ref) => {
|
||||
const ContractStatusComponent = ({ value, onChange }) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export function ContractsFindModalContainer({
|
||||
title={t("contracts.labels.findermodal")}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
onOk={() => toggleModalVisible()}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
forceRender
|
||||
>
|
||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Slider } from "antd";
|
||||
import React, { forwardRef } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
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 { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||
import dayjs from "../../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
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";
|
||||
@@ -169,7 +169,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport",
|
||||
text: s || t("dashboard.errors.atp"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
@@ -313,7 +313,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Ins. Co.*",
|
||||
text: s || t("dashboard.errors.insco"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
@@ -335,7 +335,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport",
|
||||
text: s || t("dashboard.errors.atp"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
||||
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||
import dayjs from "../../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import { 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";
|
||||
@@ -138,7 +138,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport*",
|
||||
text: s || t("dashboard.errors.atp"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
@@ -154,7 +154,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
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,
|
||||
filters:
|
||||
(scheduledOutToday &&
|
||||
@@ -163,7 +163,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Status*",
|
||||
text: s || t("dashboard.errors.status"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
@@ -306,7 +306,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Ins. Co.*",
|
||||
text: s || t("dashboard.errors.insco"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
@@ -328,7 +328,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport*",
|
||||
text: s || t("dashboard.errors.atp"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import i18next from "i18next";
|
||||
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component.jsx";
|
||||
import {
|
||||
DashboardTotalProductionHours,
|
||||
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 JobLifecycleDashboardComponent, {
|
||||
JobLifecycleDashboardGQL
|
||||
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx";
|
||||
import DashboardMonthlyEmployeeEfficiency, {
|
||||
DashboardMonthlyEmployeeEfficiencyGql
|
||||
} 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, {
|
||||
DashboardScheduledInTodayGql
|
||||
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx";
|
||||
import DashboardScheduledOutToday, {
|
||||
DashboardScheduledOutTodayGql
|
||||
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx";
|
||||
import JobLifecycleDashboardComponent, {
|
||||
JobLifecycleDashboardGQL
|
||||
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx";
|
||||
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component.jsx";
|
||||
import {
|
||||
DashboardTotalProductionHours,
|
||||
DashboardTotalProductionHoursGql
|
||||
} from "../dashboard-components/total-production-hours/total-production-hours.component.jsx";
|
||||
|
||||
const componentList = {
|
||||
ProductionDollars: {
|
||||
@@ -118,6 +121,15 @@ const componentList = {
|
||||
w: 10,
|
||||
h: 3
|
||||
},
|
||||
ScheduleDeliveryToday: {
|
||||
label: i18next.t("dashboard.titles.scheduleddeliverytoday"),
|
||||
component: DashboardScheduledDeliveryToday,
|
||||
gqlFragment: DashboardScheduledDeliveryTodayGql,
|
||||
minW: 6,
|
||||
minH: 2,
|
||||
w: 10,
|
||||
h: 3
|
||||
},
|
||||
JobLifecycle: {
|
||||
label: i18next.t("dashboard.titles.joblifecycle"),
|
||||
component: JobLifecycleDashboardComponent,
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Typography
|
||||
} from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -24,14 +23,14 @@ import i18n from "../../translations/i18n";
|
||||
import dayjs from "../../utils/day";
|
||||
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.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 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({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
|
||||
@@ -93,7 +92,9 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
||||
})
|
||||
: ""
|
||||
}`.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>
|
||||
|
||||
@@ -152,7 +152,7 @@ export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOv
|
||||
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
return (
|
||||
<Modal
|
||||
destroyOnClose={true}
|
||||
destroyOnHidden
|
||||
open={modalVisible}
|
||||
maskClosable={false}
|
||||
width={"80%"}
|
||||
|
||||
@@ -81,8 +81,9 @@ export function HasFeatureAccess({ featureName, bodyshop, bypass, debug = false
|
||||
}
|
||||
return (
|
||||
bodyshop?.features?.allAccess ||
|
||||
bodyshop?.features?.[featureName] ||
|
||||
dayjs(bodyshop?.features[featureName]).isAfter(dayjs())
|
||||
(typeof bodyshop?.features?.[featureName] === "boolean"
|
||||
? bodyshop?.features?.[featureName]
|
||||
: dayjs(bodyshop?.features?.[featureName]).isAfter(dayjs()))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DatePicker, Space, TimePicker } from "antd";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -94,7 +94,24 @@ const DateTimePicker = ({
|
||||
showTime={false}
|
||||
format="MM/DD/YYYY"
|
||||
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")}
|
||||
onBlur={handleBlur}
|
||||
disabledDate={handleDisabledDate}
|
||||
@@ -105,13 +122,25 @@ const DateTimePicker = ({
|
||||
<TimePicker
|
||||
format="hh:mm a"
|
||||
minuteStep={15}
|
||||
value={value && dayjs(value).hour() === 0 && dayjs(value).minute() === 0 ? null : dayjs(value)}
|
||||
defaultOpenValue={dayjs(value)
|
||||
.hour(dayjs().hour())
|
||||
.minute(Math.floor(dayjs().minute() / 15) * 15)
|
||||
.second(0)}
|
||||
onChange={(value) => {
|
||||
handleChange(value);
|
||||
onBlur();
|
||||
onChange={(timeValue) => {
|
||||
if (timeValue) {
|
||||
// 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")}
|
||||
{...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,58 +1,29 @@
|
||||
import {
|
||||
BankFilled,
|
||||
BarChartOutlined,
|
||||
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";
|
||||
// noinspection RegExpAnonymousGroup
|
||||
|
||||
import { BellFilled } from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Badge, Layout, Menu, Spin } from "antd";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Badge, Layout, Menu, Spin, Tooltip } from "antd";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsKanban } from "react-icons/bs";
|
||||
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 { FaTasks } from "react-icons/fa";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
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 { QUERY_MY_TASKS_COUNT } from "../../graphql/tasks.queries.js";
|
||||
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { signOutStart } from "../../redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
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 { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
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({
|
||||
currentUser: selectCurrentUser,
|
||||
recentItems: selectRecentItems,
|
||||
@@ -70,35 +41,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
|
||||
function Header({
|
||||
handleMenuClick,
|
||||
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;
|
||||
|
||||
// --- Utility Hooks ---
|
||||
function useUnreadNotifications(userAssociationId, isConnected, scenarioNotificationsOn) {
|
||||
const {
|
||||
data: unreadData,
|
||||
refetch: refetchUnread,
|
||||
@@ -124,618 +68,286 @@ function Header({
|
||||
}
|
||||
}, [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(() => {
|
||||
const updateTitle = () => {
|
||||
const currentTitle = document.title;
|
||||
// Check if the current title differs from what we last set
|
||||
if (currentTitle !== lastSetTitleRef.current) {
|
||||
// Extract base title by removing any unread count prefix
|
||||
const baseTitleMatch = currentTitle.match(/^\(\d+\)\s*(.*)$/);
|
||||
baseTitleRef.current = baseTitleMatch ? baseTitleMatch[1] : currentTitle;
|
||||
}
|
||||
|
||||
// Apply unread count to the base title
|
||||
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) {
|
||||
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
|
||||
updateTitle();
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [taskCenterVisible, notificationVisible, scenarioNotificationsOn]);
|
||||
|
||||
// Poll every 100ms to catch child component changes
|
||||
const interval = setInterval(updateTitle, 100);
|
||||
// --- Event Handlers ---
|
||||
const handleTaskCenterClick = useCallback(
|
||||
(e) => {
|
||||
setTaskCenterVisible((prev) => {
|
||||
if (prev) return false;
|
||||
return true;
|
||||
});
|
||||
if (handleMenuClick) handleMenuClick(e);
|
||||
},
|
||||
[handleMenuClick]
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
document.title = baseTitleRef.current; // Reset to base title on unmount
|
||||
};
|
||||
}, [unreadCount]); // Re-run when unreadCount changes
|
||||
const handleNotificationClick = useCallback(
|
||||
(e) => {
|
||||
setNotificationVisible((prev) => {
|
||||
if (prev) return false;
|
||||
return true;
|
||||
});
|
||||
if (handleMenuClick) handleMenuClick(e);
|
||||
},
|
||||
[handleMenuClick]
|
||||
);
|
||||
|
||||
const handleNotificationClick = (e) => {
|
||||
setNotificationVisible(!notificationVisible);
|
||||
if (handleMenuClick) handleMenuClick(e);
|
||||
};
|
||||
// --- Menu Items ---
|
||||
|
||||
const accountingChildren = [
|
||||
{
|
||||
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>
|
||||
)
|
||||
}
|
||||
]
|
||||
// built externally to keep the component clean, but on this level to prevent unnecessary re-renders
|
||||
const accountingChildren = useMemo(
|
||||
() =>
|
||||
buildAccountingChildren({
|
||||
t,
|
||||
bodyshop,
|
||||
currentUser,
|
||||
setBillEnterContext,
|
||||
setPaymentContext,
|
||||
setCardPaymentContext,
|
||||
setTimeTicketContext,
|
||||
ImEXPay,
|
||||
DmsAp,
|
||||
Simple_Inventory
|
||||
}),
|
||||
[
|
||||
t,
|
||||
bodyshop,
|
||||
currentUser,
|
||||
setBillEnterContext,
|
||||
setPaymentContext,
|
||||
setCardPaymentContext,
|
||||
setTimeTicketContext,
|
||||
ImEXPay,
|
||||
DmsAp,
|
||||
Simple_Inventory
|
||||
]
|
||||
);
|
||||
|
||||
// Built externally to keep the component clean
|
||||
const leftMenuItems = useMemo(
|
||||
() =>
|
||||
buildLeftMenuItems({
|
||||
t,
|
||||
bodyshop,
|
||||
recentItems,
|
||||
setTaskUpsertContext,
|
||||
setReportCenterContext,
|
||||
signOutStart,
|
||||
accountingChildren
|
||||
}),
|
||||
[t, bodyshop, recentItems, setTaskUpsertContext, setReportCenterContext, signOutStart, accountingChildren]
|
||||
);
|
||||
|
||||
const rightMenuItems = useMemo(() => {
|
||||
const items = [];
|
||||
if (scenarioNotificationsOn) {
|
||||
items.push({
|
||||
key: "notifications",
|
||||
id: "header-notifications",
|
||||
icon: unreadLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<Badge offset={[8, 0]} size="small" count={isEmployee ? unreadCount : 0}>
|
||||
<BellFilled />
|
||||
</Badge>
|
||||
),
|
||||
onClick: handleNotificationClick
|
||||
});
|
||||
}
|
||||
];
|
||||
|
||||
// Left menu items (includes original navigation items)
|
||||
const leftMenuItems = [
|
||||
{
|
||||
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>
|
||||
items.push({
|
||||
key: "taskcenter",
|
||||
id: "header-taskcenter",
|
||||
icon: taskCountLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<Badge offset={[8, 0]} size="small" count={incompleteTaskCount > 0 ? incompleteTaskCount : 0} showZero={false}>
|
||||
<Tooltip title={t("menus.header.tasks")}>
|
||||
<FaTasks />
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
),
|
||||
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")
|
||||
},
|
||||
...(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={unreadCount}>
|
||||
<BellFilled />
|
||||
</Badge>
|
||||
),
|
||||
onClick: handleNotificationClick
|
||||
}
|
||||
]
|
||||
: [];
|
||||
onClick: handleTaskCenterClick
|
||||
});
|
||||
return items;
|
||||
}, [
|
||||
scenarioNotificationsOn,
|
||||
unreadLoading,
|
||||
unreadCount,
|
||||
taskCountLoading,
|
||||
incompleteTaskCount,
|
||||
isEmployee,
|
||||
handleNotificationClick,
|
||||
handleTaskCenterClick,
|
||||
t
|
||||
]);
|
||||
|
||||
// --- Render ---
|
||||
return (
|
||||
<Layout.Header style={{ padding: 0, background: "#001529" }}>
|
||||
<div
|
||||
style={{
|
||||
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 && (
|
||||
<div style={{ display: "flex", alignItems: "center", height: "100%", overflow: "hidden" }}>
|
||||
<div style={{ flexGrow: 1, overflowX: "auto", whiteSpace: "nowrap" }}>
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
theme="dark"
|
||||
selectedKeys={[selectedHeader]}
|
||||
onClick={handleMenuClick}
|
||||
subMenuCloseDelay={0.3}
|
||||
items={notificationItem}
|
||||
style={{ flex: "0 0 auto", minWidth: 0, borderBottom: "none", background: "transparent" }}
|
||||
items={leftMenuItems}
|
||||
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>
|
||||
{scenarioNotificationsOn && (
|
||||
<NotificationCenterContainer
|
||||
visible={notificationVisible}
|
||||
onClose={() => setNotificationVisible(false)}
|
||||
unreadCount={unreadCount}
|
||||
/>
|
||||
<div ref={notificationRef}>
|
||||
<NotificationCenterContainer
|
||||
visible={notificationVisible}
|
||||
onClose={() => setNotificationVisible(false)}
|
||||
unreadCount={unreadCount}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div ref={taskCenterRef}>
|
||||
<TaskCenterContainer
|
||||
incompleteTaskCount={incompleteTaskCount}
|
||||
visible={taskCenterVisible}
|
||||
onClose={() => setTaskCenterVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
</Layout.Header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export function InventoryUpsertModalContainer({ currentUser, bodyshop, inventory
|
||||
onCancel={() => {
|
||||
toggleModalVisible();
|
||||
}}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||
<InventoryUpsertModal form={form} />
|
||||
|
||||
@@ -66,7 +66,7 @@ export function ScheduleEventComponent({
|
||||
const [popOverVisible, setPopOverVisible] = useState(false);
|
||||
|
||||
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
|
||||
variables: { id: event.job.id },
|
||||
variables: { id: event.job?.id },
|
||||
onCompleted: (data) => {
|
||||
if (data?.jobs_by_pk) {
|
||||
const totalHours =
|
||||
@@ -83,6 +83,7 @@ export function ScheduleEventComponent({
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
@@ -384,7 +385,9 @@ export function ScheduleEventComponent({
|
||||
previousEvent: event.id,
|
||||
color: event.color,
|
||||
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
|
||||
}
|
||||
});
|
||||
}}
|
||||
@@ -394,30 +397,33 @@ export function ScheduleEventComponent({
|
||||
) : (
|
||||
<ScheduleManualEvent event={event} />
|
||||
)}
|
||||
{event.isintake && HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
||||
search: `?appointmentId=${event.id}`
|
||||
}}
|
||||
>
|
||||
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Popover //open={open}
|
||||
content={popMenu}
|
||||
open={popOverVisible}
|
||||
onOpenChange={setPopOverVisible}
|
||||
onClick={(e) => {
|
||||
getJobDetails();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
trigger="click"
|
||||
>
|
||||
<Button disabled={event.arrived}>{t("jobs.actions.intake_quick")}</Button>
|
||||
</Popover>
|
||||
)}
|
||||
{event.job &&
|
||||
(HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
||||
search: `?appointmentId=${event.id}`
|
||||
}}
|
||||
>
|
||||
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Popover //open={open}
|
||||
content={popMenu}
|
||||
open={popOverVisible}
|
||||
onOpenChange={setPopOverVisible}
|
||||
onClick={(e) => {
|
||||
if (event.job?.id) {
|
||||
e.stopPropagation();
|
||||
getJobDetails();
|
||||
}
|
||||
}}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
trigger="click"
|
||||
>
|
||||
<Button disabled={event.arrived}>{t("jobs.actions.intake_quick")}</Button>
|
||||
</Popover>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Form, Input, Switch } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
@@ -9,7 +9,6 @@ import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../../../firebase/firebase.utils";
|
||||
import { MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED } from "../../../../graphql/appointments.queries";
|
||||
import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
|
||||
import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
|
||||
import { insertAuditTrail } from "../../../../redux/application/application.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
|
||||
@@ -32,7 +31,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED);
|
||||
const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED);
|
||||
const [updateOwner] = useMutation(UPDATE_OWNER);
|
||||
const notification = useNotification();
|
||||
|
||||
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);
|
||||
|
||||
if (!!!result.errors) {
|
||||
@@ -189,7 +169,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
||||
initialValues={{
|
||||
...(type === "intake" && {
|
||||
addToProduction: true,
|
||||
allow_text_message: job.owner && job.owner.allow_text_message,
|
||||
scheduled_completion:
|
||||
(job && job.scheduled_completion && dayjs(job.scheduled_completion)) ||
|
||||
(job &&
|
||||
@@ -228,14 +207,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
||||
>
|
||||
<Switch disabled={readOnly} />
|
||||
</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
|
||||
name="scheduled_completion"
|
||||
label={t("jobs.fields.scheduled_completion")}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function JobCostingModalContainer({ jobCostingModal, toggleModalVisible }
|
||||
}}
|
||||
cancelButtonProps={{ style: { display: "none" } }}
|
||||
width="90%"
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
>
|
||||
{!costingData ? (
|
||||
<LoadingSpinner loading={true} />
|
||||
|
||||
@@ -32,7 +32,13 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||
setPrintCenterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "printCenter"
|
||||
})
|
||||
),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
@@ -87,7 +93,7 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTra
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||
{loading ? <LoadingSpinner /> : null}
|
||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||
{data ? (
|
||||
|
||||
@@ -80,7 +80,7 @@ export function JobEmployeeAssignments({
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover destroyTooltipOnHide content={popContent} open={visibility}>
|
||||
<Popover destroyOnHidden content={popContent} open={visibility}>
|
||||
<Spin spinning={loading}>
|
||||
<DataLabel label={t("jobs.fields.employee_body")}>
|
||||
{body ? (
|
||||
|
||||
@@ -44,7 +44,7 @@ function JobReconciliationModalContainer({ reconciliationModal, toggleModalVisib
|
||||
onOk={handleCancel}
|
||||
onCancel={handleCancel}
|
||||
cancelButtonProps={{ display: "none" }}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
className="imex-reconciliation-modal"
|
||||
>
|
||||
{loading && <LoadingSpinner loading={loading} />}
|
||||
|
||||
@@ -24,7 +24,8 @@ export default function JobWatcherToggleComponent({
|
||||
handleToggleSelf,
|
||||
handleRemoveWatcher,
|
||||
handleWatcherSelect,
|
||||
handleTeamSelect
|
||||
handleTeamSelect,
|
||||
isEmployee
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -66,22 +67,32 @@ export default function JobWatcherToggleComponent({
|
||||
<List>
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button
|
||||
type={isWatching ? "primary" : "default"}
|
||||
danger={!isWatching}
|
||||
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
|
||||
size="medium"
|
||||
onClick={handleToggleSelf}
|
||||
loading={adding || removing}
|
||||
>
|
||||
{isWatching ? t("notifications.labels.unwatch") : t("notifications.labels.watch")}
|
||||
</Button>
|
||||
<Tooltip title={!isEmployee ? t("notifications.tooltips.not-employee") : ""} placement="top">
|
||||
<span>
|
||||
<Button
|
||||
type={isWatching ? "primary" : "default"}
|
||||
danger={!isWatching}
|
||||
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
|
||||
size="medium"
|
||||
onClick={handleToggleSelf}
|
||||
loading={adding || removing}
|
||||
disabled={!isEmployee || adding || removing}
|
||||
>
|
||||
{isWatching ? t("notifications.labels.unwatch") : t("notifications.labels.watch")}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta>
|
||||
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
|
||||
{t("notifications.labels.watching-issue")}
|
||||
</Text>
|
||||
{!isEmployee && (
|
||||
<Text type="danger" style={{ marginBottom: 8, display: "block" }}>
|
||||
{t("notifications.tooltips.not-employee")}
|
||||
</Text>
|
||||
)}
|
||||
</List.Item.Meta>
|
||||
</List.Item>
|
||||
</List>
|
||||
@@ -98,8 +109,11 @@ export default function JobWatcherToggleComponent({
|
||||
<EmployeeSearchSelectComponent
|
||||
style={{ minWidth: "100%" }}
|
||||
options={
|
||||
bodyshop?.employees?.filter((e) =>
|
||||
jobWatchers.every((w) => w.user_email !== e.user_email && e.active && e.user_email)
|
||||
bodyshop?.employees?.filter(
|
||||
(e) =>
|
||||
e.user_email && // Ensure user_email is not null or undefined
|
||||
e.active && // Ensure employee is active
|
||||
jobWatchers.every((w) => w.user_email !== e.user_email) // Ensure not already a watcher
|
||||
) || []
|
||||
}
|
||||
placeholder={t("notifications.labels.employee-search")}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
||||
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -21,13 +22,14 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const userEmail = currentUser.email;
|
||||
const jobid = job.id;
|
||||
|
||||
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedWatcher, setSelectedWatcher] = useState(null);
|
||||
const [selectedTeam, setSelectedTeam] = useState(null);
|
||||
|
||||
const userEmail = currentUser.email;
|
||||
const jobid = job.id;
|
||||
|
||||
// Fetch current watchers with refetch capability
|
||||
const {
|
||||
data: watcherData,
|
||||
@@ -139,13 +141,13 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
||||
});
|
||||
|
||||
const handleToggleSelf = useCallback(async () => {
|
||||
if (adding || removing) return;
|
||||
if (adding || removing || !isEmployee) return;
|
||||
if (isWatching) {
|
||||
await removeWatcher({ variables: { jobid, userEmail } });
|
||||
} else {
|
||||
await addWatcher({ variables: { jobid, userEmail } });
|
||||
}
|
||||
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing]);
|
||||
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing, isEmployee]);
|
||||
|
||||
const handleRemoveWatcher = useCallback(
|
||||
async (email) => {
|
||||
@@ -187,7 +189,16 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
||||
setSelectedTeam(null);
|
||||
return;
|
||||
}
|
||||
await Promise.all(newWatchers.map((email) => addWatcher({ variables: { jobid, userEmail: email } })));
|
||||
await Promise.all(
|
||||
newWatchers.map((email) =>
|
||||
addWatcher({
|
||||
variables: {
|
||||
jobid,
|
||||
userEmail: email
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
[jobWatchers, addWatcher, jobid, adding]
|
||||
);
|
||||
@@ -212,6 +223,7 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
||||
handleWatcherSelect={handleWatcherSelect}
|
||||
handleTeamSelect={handleTeamSelect}
|
||||
currentUser={currentUser}
|
||||
isEmployee={isEmployee} // Pass isEmployee to the component
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,12 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
|
||||
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.estimate_approved")} name="estimate_approved">
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.date_scheduled")} name="date_scheduled">
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { DownloadOutlined, SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Input, Space, Table } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { selectPartnerVersion } from "../../redux/application/application.selectors";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
partnerVersion: selectPartnerVersion
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAvailableScan);
|
||||
@@ -126,6 +126,7 @@ export function JobsAvailableScan({ partnerVersion, refetch }) {
|
||||
onClick={() => {
|
||||
scanEstimates();
|
||||
}}
|
||||
id="scan-estimates-button"
|
||||
>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
|
||||
@@ -4,11 +4,12 @@ import { Col, Row } from "antd";
|
||||
import Axios from "axios";
|
||||
import _ from "lodash";
|
||||
import queryString from "query-string";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import {
|
||||
DELETE_AVAILABLE_JOB,
|
||||
@@ -33,7 +34,6 @@ import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.contai
|
||||
import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
|
||||
import HeaderFields from "./jobs-available-supplement.headerfields";
|
||||
import JobsAvailableTableComponent from "./jobs-available-table.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -195,7 +195,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
||||
|
||||
await deleteJob({
|
||||
variables: { id: estData.id }
|
||||
}).then((r) => {
|
||||
}).then(() => {
|
||||
refetch();
|
||||
setInsertLoading(false);
|
||||
});
|
||||
@@ -315,7 +315,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
||||
|
||||
deleteJob({
|
||||
variables: { id: estData.id }
|
||||
}).then((r) => {
|
||||
}).then(() => {
|
||||
refetch();
|
||||
setInsertLoading(false);
|
||||
});
|
||||
@@ -372,7 +372,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
||||
loadEstData({ variables: { id: record.id } });
|
||||
modalSearchState[1](record.clm_no);
|
||||
setJobModalVisible(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -456,7 +456,7 @@ function replaceEmpty(someObj, replaceValue = null) {
|
||||
return JSON.parse(temp);
|
||||
}
|
||||
|
||||
async function CheckTaxRatesUSA(estData, bodyshop) {
|
||||
async function CheckTaxRatesUSA(estData) {
|
||||
if (!estData.parts_tax_rates?.PAM) {
|
||||
estData.parts_tax_rates.PAM = estData.parts_tax_rates.PAC;
|
||||
}
|
||||
@@ -568,7 +568,7 @@ async function CheckTaxRates(estData, bodyshop) {
|
||||
});
|
||||
//}
|
||||
}
|
||||
function ResolveCCCLineIssues(estData, bodyshop) {
|
||||
function ResolveCCCLineIssues(estData) {
|
||||
//Find all misc amounts, populate them to the act price.
|
||||
//This needs to be done before cleansing unq_seq since some misc prices could move over.
|
||||
estData.joblines.data.forEach((line) => {
|
||||
@@ -585,6 +585,9 @@ function ResolveCCCLineIssues(estData, bodyshop) {
|
||||
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
|
||||
line.mod_lbr_ty = "LAR";
|
||||
}
|
||||
if (line.mod_lbr_ty === "OTSL") {
|
||||
line.mod_lbr_ty = line.mod_lbr_hrs === 0 ? null : "LAB";
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Form, Input, Switch } from "antd";
|
||||
import { Form, Input } from "antd";
|
||||
import React, { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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"]}>
|
||||
<Input disabled={!state.owner.new} />
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Card, Input, Table } from "antd";
|
||||
import React, { useContext, useState } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
@@ -91,6 +91,7 @@ export default function JobsCreateOwnerInfoSearchComponent({ loading, owners })
|
||||
});
|
||||
}}
|
||||
enterButton
|
||||
id="search-owner"
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -112,9 +113,9 @@ export default function JobsCreateOwnerInfoSearchComponent({ loading, owners })
|
||||
type: "radio",
|
||||
selectedRowKeys: [state.owner.selectedid]
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
onRow={(record) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
onClick: () => {
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
setState({
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
|
||||
open={open}
|
||||
placement="left"
|
||||
onOpenChange={handleOpenChange}
|
||||
destroyTooltipOnHide
|
||||
destroyOnHidden
|
||||
>
|
||||
<SearchOutlined style={{ cursor: "pointer" }} />
|
||||
</Popover>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Card, Input, Space, Table } from "antd";
|
||||
import { useContext, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
|
||||
export default function JobsCreateVehicleInfoSearchComponent({ loading, vehicles }) {
|
||||
@@ -63,6 +63,7 @@ export default function JobsCreateVehicleInfoSearchComponent({ loading, vehicles
|
||||
});
|
||||
}}
|
||||
enterButton
|
||||
id="search-vehicle"
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
@@ -91,9 +92,9 @@ export default function JobsCreateVehicleInfoSearchComponent({ loading, vehicles
|
||||
type: "radio",
|
||||
selectedRowKeys: [state.vehicle.selectedid]
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
onRow={(record) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
onClick: () => {
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
setState({
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Form, Statistic, Tooltip } from "antd";
|
||||
import React, { useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import dayjs from "../../utils/day";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import FormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
@@ -40,6 +41,20 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
||||
<Form.Item label={t("jobs.fields.date_rentalresp")} name="date_rentalresp">
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
|
||||
<DateTimePicker
|
||||
disabled={jobRO}
|
||||
value={job.estimate_sent_approval ? dayjs(job.estimate_sent_approval) : null}
|
||||
placeholder={t("general.labels.na")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.estimate_approved")} name="estimate_approved">
|
||||
<DateTimePicker
|
||||
disabled={jobRO}
|
||||
value={job.estimate_approved ? dayjs(job.estimate_approved) : null}
|
||||
placeholder={t("general.labels.na")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</FormRow>
|
||||
|
||||
<FormRow header={t("jobs.forms.scheddates")}>
|
||||
@@ -48,7 +63,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
||||
</Form.Item>
|
||||
<Tooltip title={t("jobs.labels.scheduledinchange")}>
|
||||
<Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
<DateTimePicker disabled={true} />
|
||||
</Form.Item>
|
||||
</Tooltip>
|
||||
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
||||
@@ -76,21 +91,15 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.actual_completion")}
|
||||
name="actual_completion"
|
||||
rules={[
|
||||
{
|
||||
required: jobInPostProduction
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
{() => (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.actual_completion")}
|
||||
name="actual_completion"
|
||||
rules={[{ required: jobInPostProduction }]}
|
||||
>
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.scheduled_delivery")} name="scheduled_delivery">
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
@@ -101,19 +110,16 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
||||
</FormRow>
|
||||
<FormRow header={t("jobs.forms.admindates")}>
|
||||
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
<DateTimePicker disabled={true} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
<DateTimePicker disabled={true} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
<DateTimePicker disabled={true} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
<DateTimePicker disabled={true} />
|
||||
</Form.Item>
|
||||
</FormRow>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
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 DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -673,7 +673,9 @@ export function JobsDetailHeaderActions({
|
||||
context: {
|
||||
jobId: job.id,
|
||||
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({
|
||||
key: "deletejob",
|
||||
id: "job-actions-deletejob",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.deleteconfirm")}
|
||||
okText={t("general.labels.yes")}
|
||||
cancelText={t("general.labels.no")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={handleDeleteJob}
|
||||
>
|
||||
{t("menus.jobsactions.deletejob")}
|
||||
</Popconfirm>
|
||||
)
|
||||
label:
|
||||
job.job_watchers.length === 0 ? (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.deleteconfirm")}
|
||||
okText={t("general.labels.yes")}
|
||||
cancelText={t("general.labels.no")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={handleDeleteJob}
|
||||
>
|
||||
{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>
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.voidjob")}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
okText={t("general.labels.yes")}
|
||||
cancelText={t("general.labels.no")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={handleVoidJob}
|
||||
>
|
||||
|
||||
@@ -167,7 +167,18 @@ export function JobsDetailHeaderActionsToggleProduction({
|
||||
<FormDateTimePickerComponent disabled={jobRO} />
|
||||
</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} />
|
||||
</Form.Item>
|
||||
</>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled } from "@ant-design/icons";
|
||||
import { Card, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Card, Checkbox, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
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 { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { DateTimeFormatter, DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
@@ -29,41 +34,73 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" }))
|
||||
setPrintCenterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "printCenter"
|
||||
})
|
||||
),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid,
|
||||
operation,
|
||||
type
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
const colSpan = {
|
||||
xs: {
|
||||
span: 24
|
||||
},
|
||||
sm: {
|
||||
span: 24
|
||||
},
|
||||
md: {
|
||||
span: 12
|
||||
},
|
||||
lg: {
|
||||
span: 6
|
||||
},
|
||||
xl: {
|
||||
span: 6
|
||||
}
|
||||
xs: { span: 24 },
|
||||
sm: { span: 24 },
|
||||
md: { span: 12 },
|
||||
lg: { span: 6 },
|
||||
xl: { span: 6 }
|
||||
};
|
||||
|
||||
export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail }) {
|
||||
const { t } = useTranslation();
|
||||
const { notification } = useNotification();
|
||||
const [notesClamped, setNotesClamped] = useState(true);
|
||||
const vehicleTitle = `${job.v_model_yr || ""} ${job.v_color || ""}
|
||||
${job.v_make_desc || ""}
|
||||
${job.v_model_desc || ""}`.trim();
|
||||
|
||||
const [updateJob] = useMutation(UPDATE_JOB);
|
||||
const vehicleTitle =
|
||||
`${job.v_model_yr || ""} ${job.v_color || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim();
|
||||
const bodyHrs = job.joblines.filter((j) => j.mod_lbr_ty !== "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
||||
const refinishHrs = job.joblines
|
||||
.filter((line) => line.mod_lbr_ty === "LAR")
|
||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
||||
|
||||
const ownerTitle = OwnerNameDisplayFunction(job).trim();
|
||||
|
||||
// Handle checkbox changes
|
||||
const handleCheckboxChange = async (field, checked) => {
|
||||
const value = checked ? dayjs().toISOString() : null;
|
||||
try {
|
||||
const ret = await updateJob({
|
||||
variables: {
|
||||
jobId: job.id,
|
||||
job: { [field]: value }
|
||||
},
|
||||
refetchQueries: ["GET_JOB_BY_PK"],
|
||||
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) {
|
||||
notification.error({
|
||||
message: t("jobs.errors.saving", { error: error.message })
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
|
||||
<Col {...colSpan}>
|
||||
@@ -72,11 +109,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
<DataLabel label={t("jobs.fields.status")}>
|
||||
<Space wrap>
|
||||
{job.status}
|
||||
{job.inproduction && (
|
||||
<Tag color="#f50" key="production">
|
||||
{t("jobs.labels.inproduction")}
|
||||
</Tag>
|
||||
)}
|
||||
{job.inproduction && <Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>}
|
||||
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||
{job.iouparent && (
|
||||
<Link to={`/manage/jobs/${job.iouparent}`}>
|
||||
@@ -110,7 +143,6 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
<span style={{ margin: "0rem .5rem" }}>/</span>
|
||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||
</DataLabel>
|
||||
|
||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||
{job.alt_transport}
|
||||
<JobAltTransportChange job={job} />
|
||||
@@ -127,11 +159,39 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
))}
|
||||
</DataLabel>
|
||||
)}
|
||||
|
||||
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
||||
<ProductionListColumnProductionNote record={job} />
|
||||
</DataLabel>
|
||||
|
||||
<DataLabel label={t("jobs.fields.estimate_sent_approval")}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={!!job.estimate_sent_approval}
|
||||
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{job.estimate_sent_approval && (
|
||||
<span style={{ color: "#888" }}>
|
||||
<DateTimeFormatter>{job.estimate_sent_approval}</DateTimeFormatter>
|
||||
</span>
|
||||
)}
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.estimate_approved")}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={!!job.estimate_approved}
|
||||
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{job.estimate_approved && (
|
||||
<span style={{ color: "#888" }}>
|
||||
<DateTimeFormatter>{job.estimate_approved}</DateTimeFormatter>
|
||||
</span>
|
||||
)}
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</DataLabel>
|
||||
<Space wrap>
|
||||
{job.special_coverage_policy && (
|
||||
<Tag color="tomato">
|
||||
@@ -275,7 +335,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
</Card>
|
||||
</Col>
|
||||
<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>
|
||||
<JobEmployeeAssignments job={job} />
|
||||
<Divider style={{ margin: ".5rem" }} />
|
||||
|
||||
@@ -42,7 +42,7 @@ export function JobsDocumentsContainer({
|
||||
variables: { jobId: jobId },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
skip: Imgproxy.treatment === "on" || !!billId
|
||||
skip: !!billId
|
||||
});
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
|
||||
@@ -3,7 +3,6 @@ import axios from "axios";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import cleanAxios from "../../utils/CleanAxios";
|
||||
import formatBytes from "../../utils/formatbytes";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -12,7 +11,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -26,7 +25,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
|
||||
|
||||
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier, jobId }) {
|
||||
export function JobsDocumentsImgproxyDownloadButton({ galleryImages, identifier, jobId }) {
|
||||
const { t } = useTranslation();
|
||||
const [download, setDownload] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -46,32 +45,40 @@ export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, i
|
||||
}
|
||||
|
||||
function standardMediaDownload(bufferData) {
|
||||
const a = document.createElement("a");
|
||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||
a.href = url;
|
||||
a.download = `${identifier || "documents"}.zip`;
|
||||
a.click();
|
||||
try {
|
||||
const a = document.createElement("a");
|
||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||
a.href = url;
|
||||
a.download = `${identifier || "documents"}.zip`;
|
||||
a.click();
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setDownload(null);
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownload = async () => {
|
||||
logImEXEvent("jobs_documents_download");
|
||||
setLoading(true);
|
||||
const zipUrl = await axios({
|
||||
url: "/media/imgproxy/download",
|
||||
method: "POST",
|
||||
data: { jobId, documentids: imagesToDownload.map((_) => _.id) }
|
||||
});
|
||||
try {
|
||||
const response = await axios({
|
||||
url: "/media/imgproxy/download",
|
||||
method: "POST",
|
||||
responseType: "blob",
|
||||
data: { jobId, documentids: imagesToDownload.map((_) => _.id) },
|
||||
onDownloadProgress: downloadProgress
|
||||
});
|
||||
|
||||
const theDownloadedZip = await cleanAxios({
|
||||
url: zipUrl.data.url,
|
||||
method: "GET",
|
||||
responseType: "arraybuffer",
|
||||
onDownloadProgress: downloadProgress
|
||||
});
|
||||
setLoading(false);
|
||||
setDownload(null);
|
||||
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 (
|
||||
|
||||
@@ -98,7 +98,13 @@ function JobsDocumentsImgproxyComponent({
|
||||
jobId={jobId}
|
||||
totalSize={totalSize}
|
||||
billId={billId}
|
||||
callbackAfterUpload={billsCallback || fetchThumbnails || refetch}
|
||||
callbackAfterUpload={
|
||||
billsCallback ||
|
||||
function () {
|
||||
isFunction(refetch) && refetch();
|
||||
isFunction(fetchThumbnails) && fetchThumbnails();
|
||||
}
|
||||
}
|
||||
ignoreSizeLimit={ignoreSizeLimit}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Checkbox, Divider, Input, Space, Table } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import dayjs from "../../utils/day";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
@@ -223,9 +223,9 @@ export default function JobsFindModalComponent({
|
||||
type: "radio",
|
||||
selectedRowKeys: [selectedJob]
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
onRow={(record) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
onClick: () => {
|
||||
handleOnRowClick(record);
|
||||
}
|
||||
};
|
||||
@@ -241,15 +241,17 @@ export default function JobsFindModalComponent({
|
||||
overrideHeaders: e.target.checked
|
||||
})
|
||||
}
|
||||
id="override_header"
|
||||
>
|
||||
{t("jobs.labels.override_header")}
|
||||
</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")}
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={updateSchComp.checked}
|
||||
onChange={(e) => setSchComp({ ...updateSchComp, checked: e.target.checked })}
|
||||
id="update_scheduled_completion"
|
||||
>
|
||||
{t("jobs.labels.update_scheduled_completion")}
|
||||
</Checkbox>
|
||||
@@ -261,6 +263,7 @@ export default function JobsFindModalComponent({
|
||||
onChange={(e) => {
|
||||
setSchComp({ ...updateSchComp, scheduled_completion: e });
|
||||
}}
|
||||
id="scheduled_completion_date_time_picker"
|
||||
/>
|
||||
) : null}
|
||||
<Checkbox
|
||||
@@ -273,6 +276,7 @@ export default function JobsFindModalComponent({
|
||||
automatic: true
|
||||
});
|
||||
}}
|
||||
id="calculate_scheduled_completion"
|
||||
>
|
||||
{t("jobs.labels.calc_scheuled_completion")}
|
||||
</Checkbox>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Modal } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -65,8 +64,8 @@ export default connect(
|
||||
<Modal
|
||||
title={t("jobs.labels.existing_jobs")}
|
||||
width={"80%"}
|
||||
destroyOnClose
|
||||
okButtonProps={{ disabled: selectedJob ? false : true }}
|
||||
destroyOnHidden
|
||||
okButtonProps={{ disabled: selectedJob ? false : true, id: "jobs-find-modal-container-ok" }}
|
||||
{...modalProps}
|
||||
>
|
||||
{loading ? <LoadingSpinner /> : null}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button, Card, Input, Space, Table, Typography } from "antd";
|
||||
import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
@@ -20,7 +20,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -203,6 +203,8 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
|
||||
return (
|
||||
<Card
|
||||
id="all-jobs-list"
|
||||
title={t("titles.bc.jobs-all")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
{search.search && (
|
||||
@@ -256,6 +258,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
rowKey="id"
|
||||
dataSource={search?.search ? openSearchResults : jobs}
|
||||
onChange={handleTableChange}
|
||||
id="all-jobs-list-table"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, SyncOut
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
@@ -22,7 +22,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export function JobsList({ bodyshop }) {
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
@@ -342,13 +342,14 @@ export function JobsList({ bodyshop }) {
|
||||
type: "radio"
|
||||
}}
|
||||
onChange={handleTableChange}
|
||||
onRow={(record, rowIndex) => {
|
||||
onRow={(record) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
onClick: () => {
|
||||
handleOnRowClick(record);
|
||||
}
|
||||
};
|
||||
}}
|
||||
id="active-jobs-list-table"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,14 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid,
|
||||
operation,
|
||||
type
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleModalVisible, insertAuditTrail }) {
|
||||
@@ -123,7 +130,7 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
|
||||
onCancel={() => {
|
||||
toggleModalVisible();
|
||||
}}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||
<NoteUpsertModalComponent form={form} />
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
|
||||
import { Alert, Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
|
||||
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import "./notification-center.styles.scss";
|
||||
import day from "../../utils/day.js";
|
||||
import { forwardRef, useRef, useEffect } from "react";
|
||||
import { forwardRef, useEffect, useRef } from "react";
|
||||
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
@@ -26,7 +26,8 @@ const NotificationCenterComponent = forwardRef(
|
||||
markAllRead,
|
||||
loadMore,
|
||||
onNotificationClick,
|
||||
unreadCount
|
||||
unreadCount,
|
||||
isEmployee
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -93,7 +94,12 @@ const NotificationCenterComponent = forwardRef(
|
||||
) : (
|
||||
<EyeOutlined className="notification-toggle-icon" />
|
||||
)}
|
||||
<Switch checked={showUnreadOnly} onChange={(checked) => toggleUnreadOnly(checked)} size="small" />
|
||||
<Switch
|
||||
checked={showUnreadOnly}
|
||||
onChange={(checked) => toggleUnreadOnly(checked)}
|
||||
size="small"
|
||||
disabled={!isEmployee}
|
||||
/>
|
||||
</Space>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
||||
@@ -106,17 +112,25 @@ const NotificationCenterComponent = forwardRef(
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
style={{ height: "400px", width: "100%" }}
|
||||
data={notifications}
|
||||
totalCount={notifications.length}
|
||||
endReached={loadMore}
|
||||
itemContent={renderNotification}
|
||||
/>
|
||||
{!isEmployee ? (
|
||||
<div style={{ padding: 10 }}>
|
||||
<Alert message={t("notifications.labels.employee-notification")} type="warning" />
|
||||
</div>
|
||||
) : (
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
style={{ height: "400px", width: "100%" }}
|
||||
data={notifications}
|
||||
totalCount={notifications.length}
|
||||
endReached={loadMore}
|
||||
itemContent={renderNotification}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
NotificationCenterComponent.displayName = "NotificationCenterComponent";
|
||||
|
||||
export default NotificationCenterComponent;
|
||||
|
||||
@@ -4,9 +4,10 @@ import { connect } from "react-redux";
|
||||
import NotificationCenterComponent from "./notification-center.component";
|
||||
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||
import day from "../../utils/day.js";
|
||||
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||
|
||||
// This will be used to poll for notifications when the socket is disconnected
|
||||
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
||||
@@ -17,17 +18,18 @@ const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
||||
* @param onClose
|
||||
* @param bodyshop
|
||||
* @param unreadCount
|
||||
* @param currentUser
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
|
||||
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount, currentUser }) => {
|
||||
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
||||
const notificationRef = useRef(null);
|
||||
|
||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||
|
||||
const baseWhereClause = useMemo(() => {
|
||||
return { associationid: { _eq: userAssociationId } };
|
||||
@@ -51,7 +53,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
||||
fetchPolicy: "cache-and-network",
|
||||
notifyOnNetworkStatusChange: true,
|
||||
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
||||
skip: !userAssociationId,
|
||||
skip: !userAssociationId || !isEmployee,
|
||||
onError: (err) => {
|
||||
console.error(`Error polling Notifications: ${err?.message || ""}`);
|
||||
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
||||
@@ -71,7 +73,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
||||
}, [visible, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.notifications) {
|
||||
if (data?.notifications && isEmployee) {
|
||||
const processedNotifications = data.notifications
|
||||
.map((notif) => {
|
||||
let scenarioText;
|
||||
@@ -101,11 +103,13 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
||||
})
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
setNotifications(processedNotifications);
|
||||
} else if (!isEmployee) {
|
||||
setNotifications([]); // Clear notifications if not an employee
|
||||
}
|
||||
}, [data]);
|
||||
}, [data, isEmployee]);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (!queryLoading && data?.notifications.length) {
|
||||
if (!queryLoading && data?.notifications.length && isEmployee) {
|
||||
setIsLoading(true); // Show spinner during fetchMore
|
||||
fetchMore({
|
||||
variables: { offset: data.notifications.length, where: whereClause },
|
||||
@@ -121,13 +125,14 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
||||
})
|
||||
.finally(() => setIsLoading(false)); // Hide spinner when done
|
||||
}
|
||||
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause]);
|
||||
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause, isEmployee]);
|
||||
|
||||
const handleToggleUnreadOnly = (value) => {
|
||||
setShowUnreadOnly(value);
|
||||
};
|
||||
|
||||
const handleMarkAllRead = useCallback(() => {
|
||||
if (!isEmployee) return; // Do nothing if not an employee
|
||||
setIsLoading(true);
|
||||
markAllNotificationsRead()
|
||||
.then(() => {
|
||||
@@ -147,7 +152,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
||||
})
|
||||
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly]);
|
||||
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly, isEmployee]);
|
||||
|
||||
const handleNotificationClick = useCallback(
|
||||
(notificationId) => {
|
||||
@@ -170,17 +175,18 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && !isConnected) {
|
||||
if (visible && !isConnected && isEmployee) {
|
||||
setIsLoading(true);
|
||||
refetch()
|
||||
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
|
||||
.finally(() => setIsLoading(false));
|
||||
}
|
||||
}, [visible, isConnected, refetch]);
|
||||
}, [visible, isConnected, refetch, isEmployee]);
|
||||
|
||||
return (
|
||||
<NotificationCenterComponent
|
||||
ref={notificationRef}
|
||||
isEmployee={isEmployee}
|
||||
visible={visible}
|
||||
onClose={onClose}
|
||||
notifications={notifications}
|
||||
@@ -196,7 +202,8 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
||||
};
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(NotificationCenterContainer);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Card, Checkbox, Divider, Form, Space, Switch, Table, Typography } from "antd";
|
||||
import { Alert, Button, Card, Checkbox, Divider, Form, Space, Switch, Table, Typography } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import {
|
||||
QUERY_NOTIFICATION_SETTINGS,
|
||||
@@ -16,14 +16,16 @@ import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
|
||||
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||
|
||||
/**
|
||||
* Notifications Settings Form
|
||||
* @param currentUser
|
||||
* @param bodyshop
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const NotificationSettingsForm = ({ currentUser }) => {
|
||||
const NotificationSettingsForm = ({ currentUser, bodyshop }) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [initialValues, setInitialValues] = useState({});
|
||||
@@ -31,6 +33,7 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
||||
const [autoAddEnabled, setAutoAddEnabled] = useState(false);
|
||||
const [initialAutoAdd, setInitialAutoAdd] = useState(false);
|
||||
const notification = useNotification();
|
||||
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||
|
||||
// Fetch notification settings and notifications_autoadd
|
||||
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
||||
@@ -199,6 +202,11 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{!isEmployee && (
|
||||
<div style={{ width: "100%", marginBottom: "10px" }}>
|
||||
<Alert message={t("notifications.labels.employee-notification")} type="warning" />
|
||||
</div>
|
||||
)}
|
||||
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
|
||||
<Divider />
|
||||
</Card>
|
||||
@@ -209,11 +217,13 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
||||
NotificationSettingsForm.propTypes = {
|
||||
currentUser: PropTypes.shape({
|
||||
email: PropTypes.string.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
bodyshop: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(NotificationSettingsForm);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Form, Input, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { Form, Input, Tooltip } from "antd";
|
||||
import { CloseCircleFilled } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.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 { 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 { getFieldValue } = form;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormFieldsChanged form={form} />
|
||||
@@ -26,7 +27,7 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("owners.fields.accountingid")} name="accountingid">
|
||||
<Input disabled/>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("owners.forms.address")}>
|
||||
@@ -50,9 +51,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<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
|
||||
label={t("owners.fields.ownr_ea")}
|
||||
name="ownr_ea"
|
||||
@@ -65,19 +63,55 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
||||
>
|
||||
<FormItemEmail email={getFieldValue("ownr_ea")} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("owners.fields.ownr_ph1")}
|
||||
name="ownr_ph1"
|
||||
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]}
|
||||
>
|
||||
<FormItemPhone />
|
||||
<Form.Item label={t("owners.fields.ownr_ph1")} style={{ marginBottom: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Form.Item
|
||||
name="ownr_ph1"
|
||||
noStyle
|
||||
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
|
||||
label={t("owners.fields.ownr_ph2")}
|
||||
name="ownr_ph2"
|
||||
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]}
|
||||
>
|
||||
<FormItemPhone />
|
||||
<Form.Item label={t("owners.fields.ownr_ph2")} style={{ marginBottom: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Form.Item
|
||||
name="ownr_ph2"
|
||||
noStyle
|
||||
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 label={t("owners.fields.preferred_contact")} name="preferred_contact">
|
||||
<Input />
|
||||
|
||||
@@ -1,69 +1,115 @@
|
||||
import { Button, Form, Popconfirm } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
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 { 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 [form] = Form.useForm();
|
||||
const history = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [optedOutPhones, setOptedOutPhones] = useState(new Set());
|
||||
const [updateOwner] = useMutation(UPDATE_OWNER);
|
||||
const [deleteOwner] = useMutation(DELETE_OWNER);
|
||||
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 () => {
|
||||
setLoading(true);
|
||||
const result = await deleteOwner({
|
||||
variables: { id: owner.id }
|
||||
});
|
||||
console.log(result);
|
||||
if (result.errors) {
|
||||
notification["error"]({
|
||||
try {
|
||||
const result = await deleteOwner({
|
||||
variables: { id: owner.id }
|
||||
});
|
||||
if (result.errors) {
|
||||
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", {
|
||||
error: JSON.stringify(result.errors)
|
||||
error: error.message
|
||||
})
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
} else {
|
||||
notification["success"]({
|
||||
message: t("owners.successes.delete")
|
||||
});
|
||||
setLoading(false);
|
||||
history(`/manage/owners`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
const result = await updateOwner({
|
||||
variables: { ownerId: owner.id, owner: values }
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
try {
|
||||
const result = await updateOwner({
|
||||
variables: { ownerId: owner.id, owner: values }
|
||||
});
|
||||
if (result.errors) {
|
||||
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", {
|
||||
error: JSON.stringify(result.errors)
|
||||
error: error.message
|
||||
})
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
notification["success"]({
|
||||
message: t("owners.successes.save")
|
||||
});
|
||||
|
||||
if (refetch) await refetch();
|
||||
form.resetFields();
|
||||
form.resetFields();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -72,6 +118,7 @@ function OwnerDetailFormContainer({ owner, refetch }) {
|
||||
title={t("menus.header.owners")}
|
||||
extra={[
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
trigger="click"
|
||||
onConfirm={handleDelete}
|
||||
disabled={owner.jobs.length !== 0}
|
||||
@@ -81,16 +128,29 @@ function OwnerDetailFormContainer({ owner, refetch }) {
|
||||
{t("general.actions.delete")}
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
<Button type="primary" loading={loading} onClick={() => form.submit()}>
|
||||
<Button key="save" type="primary" loading={loading} onClick={() => form.submit()}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
]}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default OwnerDetailFormContainer;
|
||||
export default connect(mapStateToProps)(OwnerDetailFormContainer);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { Input, Modal } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { QUERY_SEARCH_OWNER_BY_IDX } from "../../graphql/owners.queries";
|
||||
import AlertComponent from "../alert/alert.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 OwnerFindModalComponent from "./owner-find-modal.component";
|
||||
|
||||
export default function OwnerFindModalContainer({
|
||||
loading,
|
||||
@@ -41,6 +41,7 @@ export default function OwnerFindModalContainer({
|
||||
<Modal
|
||||
title={<span id="owner-find-modal-title">{t("owners.labels.existing_owners")}</span>}
|
||||
width={"80%"}
|
||||
okButtonProps={{ id: "owner-find-modal-ok-button" }}
|
||||
{...modalProps}
|
||||
>
|
||||
{loading ? <LoadingSpinner /> : null}
|
||||
|
||||
@@ -75,7 +75,7 @@ export function PartsOrderBackorderEta({
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
|
||||
<Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
|
||||
<DateFormatter>{backordered_eta}</DateFormatter>
|
||||
{isAlreadyBackordered && <CalendarFilled style={{ cursor: "pointer" }} onClick={handlePopover} />}
|
||||
{loading && <Spin />}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function PartsOrderLineBackorderButton({ partsOrderStatus, partsLineId, j
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
|
||||
<Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
|
||||
<Button loading={loading} onClick={handlePopover}>
|
||||
{isAlreadyBackordered ? t("parts_orders.actions.receive") : t("parts_orders.actions.backordered")}
|
||||
</Button>
|
||||
|
||||
@@ -333,7 +333,7 @@ export function PartsOrderModalContainer({
|
||||
onOk={() => form.submit()}
|
||||
okButtonProps={{ loading: saving }}
|
||||
cancelButtonProps={{ loading: saving }}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
width="75%"
|
||||
forceRender
|
||||
>
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function PartsQueueDetailCard() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||
{loading ? <LoadingSpinner /> : null}
|
||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||
{data ? (
|
||||
|
||||
@@ -90,7 +90,7 @@ export function PartsReceiveModalContainer({ partsReceiveModal, toggleModalVisib
|
||||
onCancel={() => toggleModalVisible()}
|
||||
onOk={() => form.submit()}
|
||||
okButtonProps={{ loading: loading }}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
forceRender
|
||||
width="50%"
|
||||
>
|
||||
|
||||
@@ -134,7 +134,7 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop }) {
|
||||
<Modal
|
||||
title={!context || (context && !context.id) ? t("payments.labels.new") : t("payments.labels.edit")}
|
||||
open={open}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
okText={t("general.actions.save")}
|
||||
onOk={() => form.submit()}
|
||||
width="50%"
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Input, Table, Typography } from "antd";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries";
|
||||
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({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
function PhoneNumberConsentList({ bodyshop, currentUser }) {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Fetch opt-out phone numbers
|
||||
const { loading: optOutLoading, data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
|
||||
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined },
|
||||
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 = [
|
||||
{
|
||||
title: t("consent.phone_number"),
|
||||
dataIndex: "phone_number",
|
||||
render: (text) => <ChatOpenButton phone={text} />,
|
||||
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"),
|
||||
dataIndex: "created_at",
|
||||
render: (text) => <TimeAgoFormatter>{text}</TimeAgoFormatter>,
|
||||
sorter: (a, b) => new Date(a.created_at) - new Date(b.created_at)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Paragraph>{t("consent.text_body")}</Paragraph>
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
onSearch={(value) => setSearch(value)}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={optOutData?.phone_number_opt_out}
|
||||
loading={optOutLoading /* || ownersLoading*/}
|
||||
rowKey="id"
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PhoneNumberConsentList);
|
||||
@@ -32,7 +32,7 @@ export function PrintCenterModalContainer({ printCenterModal, toggleModalVisible
|
||||
okText={t("general.actions.close")}
|
||||
width="90%"
|
||||
title={t("printcenter.labels.title")}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
>
|
||||
<PrintCenterModalComponent context={context} />
|
||||
</Modal>
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { Card, Col, Form, Radio, Row } from "antd";
|
||||
import PropTypes from "prop-types";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../../redux/user/user.selectors";
|
||||
import { HasFeatureAccess } from "../../feature-wrapper/feature-wrapper.component";
|
||||
|
||||
const LayoutSettings = ({ t }) => (
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const LayoutSettings = ({ t, bodyshop }) => (
|
||||
<Card title={t("production.settings.layout")} style={{ maxWidth: "100%", overflowX: "auto" }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{[
|
||||
@@ -30,14 +38,18 @@ const LayoutSettings = ({ t }) => (
|
||||
{ value: false, label: t("production.labels.wide") }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "cardcolor",
|
||||
label: t("production.labels.cardcolor"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.on") },
|
||||
{ value: false, label: t("production.labels.off") }
|
||||
]
|
||||
},
|
||||
...(HasFeatureAccess({ bodyshop, featureName: "smartscheduling" })
|
||||
? [
|
||||
{
|
||||
name: "cardcolor",
|
||||
label: t("production.labels.cardcolor"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.on") },
|
||||
{ value: false, label: t("production.labels.off") }
|
||||
]
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: "kiosk",
|
||||
label: t("production.labels.kiosk_mode"),
|
||||
@@ -67,4 +79,4 @@ LayoutSettings.propTypes = {
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LayoutSettings;
|
||||
export default connect(mapStateToProps)(LayoutSettings);
|
||||
|
||||
@@ -20,7 +20,7 @@ const Board = ({ id, className, orientation, cardSettings, ...additionalProps })
|
||||
default:
|
||||
return cardSizesVertical.small;
|
||||
}
|
||||
}, [cardSettings]);
|
||||
}, [cardSettings?.cardSize]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -101,11 +101,33 @@ const BoardContainer = ({
|
||||
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
|
||||
setIsDragging(false);
|
||||
|
||||
// Only update drag time if it's a valid drop with a different destination
|
||||
if (type === "lane" && source && destination && !isEqual(source, destination)) {
|
||||
setDragTime(source.droppableId);
|
||||
setIsProcessing(true);
|
||||
// Validate drag type and source
|
||||
if (type !== "lane" || !source) {
|
||||
// Invalid drag type or missing source, attempt to revert if possible
|
||||
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(
|
||||
actions.moveCardAcrossLanes({
|
||||
fromLaneId: source.droppableId,
|
||||
@@ -114,14 +136,33 @@ const BoardContainer = ({
|
||||
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 {
|
||||
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||
} catch (err) {
|
||||
console.error("Error in onLaneDrag", err);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
try {
|
||||
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||
} catch (err) {
|
||||
console.error("Error in onLaneDrag", err);
|
||||
// Ensure revert on error
|
||||
dispatch(
|
||||
actions.moveCardAcrossLanes({
|
||||
fromLaneId: source.droppableId,
|
||||
toLaneId: source.droppableId,
|
||||
cardId: draggableId,
|
||||
index: source.index
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
},
|
||||
[dispatch, onDragEnd, setDragTime]
|
||||
|
||||
@@ -133,7 +133,9 @@ const Lane = ({
|
||||
Item: ItemComponent
|
||||
},
|
||||
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 = {
|
||||
@@ -149,8 +151,6 @@ const Lane = ({
|
||||
|
||||
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
|
||||
? orientation === "horizontal"
|
||||
? {
|
||||
@@ -161,9 +161,8 @@ const Lane = ({
|
||||
: {}
|
||||
: 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
|
||||
// a card is dragged over it
|
||||
const shouldRenderPlaceholder = orientation !== "horizontal" && (collapsed || renderedCards.length === 0);
|
||||
// Always render placeholder for empty lanes in vertical mode to ensure droppable area
|
||||
const shouldRenderPlaceholder = orientation === "vertical" ? collapsed || renderedCards.length === 0 : collapsed;
|
||||
|
||||
return (
|
||||
<HeightMemoryWrapper
|
||||
@@ -178,8 +177,8 @@ const Lane = ({
|
||||
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
|
||||
>
|
||||
<div
|
||||
ref={laneRef} // Ensure laneRef is set here
|
||||
style={{ height: "100%", width: "100%" }} // Make it scrollable
|
||||
ref={laneRef}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
|
||||
>
|
||||
<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]);
|
||||
|
||||
return (
|
||||
<Popover destroyTooltipOnHide content={popContent} open={visibility}>
|
||||
<Popover destroyOnHidden content={popContent} open={visibility}>
|
||||
<Spin spinning={loading}>
|
||||
{record[type] ? (
|
||||
<div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { QUERY_ACTIVE_EMPLOYEES, QUERY_ACTIVE_EMPLOYEES_WITH_EMAIL } from "../../graphql/employees.queries";
|
||||
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
||||
import { selectReportCenter } from "../../redux/modals/modals.selectors";
|
||||
@@ -18,11 +19,10 @@ import EmployeeSearchSelectEmail from "../employee-search-select/employee-search
|
||||
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
|
||||
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
|
||||
import "./report-center-modal.styles.scss";
|
||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
reportCenterModal: selectReportCenter,
|
||||
@@ -389,5 +389,7 @@ const restrictedReports = [
|
||||
{ key: "job_costing_ro_date_detail", days: 183 },
|
||||
{ key: "job_costing_ro_estimator", days: 183 },
|
||||
{ key: "job_lifecycle_date_detail", days: 183 },
|
||||
{ key: "job_lifecycle_date_summary", days: 183 }
|
||||
{ key: "job_lifecycle_date_summary", days: 183 },
|
||||
{ key: "customer_list", days: 183 },
|
||||
{ key: "customer_list_excel", days: 183 }
|
||||
];
|
||||
|
||||
@@ -28,7 +28,7 @@ export function ReportCenterModalContainer({ reportCenterModal, toggleModalVisib
|
||||
onOk={() => toggleModalVisible()}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
cancelButtonProps={{ style: { display: "none" } }}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
width="80%"
|
||||
>
|
||||
<RbacWrapperComponent action="shop:reportcenter">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Icon from "@ant-design/icons";
|
||||
import { Card, Popover, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import { groupBy } from "lodash";
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MdFileDownload, MdFileUpload } from "react-icons/md";
|
||||
import { connect } from "react-redux";
|
||||
@@ -26,21 +26,12 @@ const mapStateToProps = createStructuredSelector({
|
||||
calculating: selectScheduleLoadCalculating
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export function ScheduleCalendarHeaderComponent({
|
||||
bodyshop,
|
||||
label,
|
||||
refetch,
|
||||
date,
|
||||
load,
|
||||
calculating,
|
||||
events,
|
||||
...otherProps
|
||||
}) {
|
||||
export function ScheduleCalendarHeaderComponent({ bodyshop, label, refetch, date, load, calculating, events }) {
|
||||
const ATSToday = useMemo(() => {
|
||||
if (!events) return [];
|
||||
return _.groupBy(
|
||||
return groupBy(
|
||||
events.filter((e) => !e.vacation && e.isintake && dayjs(date).isSame(dayjs(e.start), "day")),
|
||||
"job.alt_transport"
|
||||
);
|
||||
@@ -155,7 +146,11 @@ export function ScheduleCalendarHeaderComponent({
|
||||
<Space size="small">
|
||||
<Icon component={MdFileDownload} style={{ color: "green" }} />
|
||||
<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>
|
||||
</Space>
|
||||
</Popover>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Form, Modal } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import {
|
||||
CANCEL_APPOINTMENT_BY_ID,
|
||||
@@ -19,9 +19,9 @@ import { selectSchedule } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { DateTimeFormat } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import ScheduleJobModalComponent from "./schedule-job-modal.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -72,7 +72,7 @@ export function ScheduleJobModalContainer({
|
||||
variables: { jobid: jobId },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
skip: !open || !!!jobId
|
||||
skip: !open || !jobId
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -93,12 +93,12 @@ export function ScheduleJobModalContainer({
|
||||
logImEXEvent("schedule_new_appointment");
|
||||
|
||||
setLoading(true);
|
||||
if (!!previousEvent) {
|
||||
if (previousEvent) {
|
||||
const cancelAppt = await cancelAppointment({
|
||||
variables: { appid: previousEvent }
|
||||
});
|
||||
|
||||
if (!!cancelAppt.errors) {
|
||||
if (cancelAppt.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.canceling", {
|
||||
message: JSON.stringify(cancelAppt.errors)
|
||||
@@ -146,7 +146,7 @@ export function ScheduleJobModalContainer({
|
||||
});
|
||||
}
|
||||
|
||||
if (!!appt.errors) {
|
||||
if (appt.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.saving", {
|
||||
message: JSON.stringify(appt.errors)
|
||||
@@ -172,7 +172,7 @@ export function ScheduleJobModalContainer({
|
||||
}
|
||||
});
|
||||
|
||||
if (!!jobUpdate.errors) {
|
||||
if (jobUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.saving", {
|
||||
message: JSON.stringify(jobUpdate.errors)
|
||||
@@ -209,7 +209,7 @@ export function ScheduleJobModalContainer({
|
||||
onOk={() => form.submit()}
|
||||
width={"90%"}
|
||||
maskClosable={false}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
okButtonProps={{
|
||||
loading: loading
|
||||
}}
|
||||
@@ -222,9 +222,9 @@ export function ScheduleJobModalContainer({
|
||||
initialValues={{
|
||||
notifyCustomer: !!(job && job.ownr_ea),
|
||||
email: (job && job.ownr_ea) || "",
|
||||
start: null,
|
||||
// smartDates: [],
|
||||
scheduled_completion: null,
|
||||
start: context.scheduled_in,
|
||||
scheduled_completion: context.scheduled_completion ,
|
||||
color: context.color,
|
||||
alt_transport: context.alt_transport,
|
||||
note: context.note
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Card } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import _ from "lodash";
|
||||
import { round } from "lodash";
|
||||
import dayjs from "../../utils/day";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
Area,
|
||||
@@ -29,7 +28,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardChart);
|
||||
@@ -40,7 +39,7 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
|
||||
const data = listOfBusDays.reduce((acc, val) => {
|
||||
//Sum up the current day.
|
||||
let dayhrs;
|
||||
if (!!sbEntriesByDate[val]) {
|
||||
if (sbEntriesByDate[val]) {
|
||||
dayhrs = sbEntriesByDate[val].reduce(
|
||||
(dayAcc, dayVal) => {
|
||||
return {
|
||||
@@ -61,9 +60,9 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
|
||||
|
||||
const theValue = {
|
||||
date: dayjs(val).format("D ddd"),
|
||||
paintHrs: _.round(dayhrs.painthrs, 1),
|
||||
bodyHrs: _.round(dayhrs.bodyhrs, 1),
|
||||
accTargetHrs: _.round(
|
||||
paintHrs: round(dayhrs.painthrs, 1),
|
||||
bodyHrs: round(dayhrs.bodyhrs, 1),
|
||||
accTargetHrs: round(
|
||||
Utils.AsOfDateTargetHours(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget + bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
val
|
||||
@@ -72,14 +71,14 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
1
|
||||
),
|
||||
accHrs: _.round(
|
||||
accHrs: round(
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accHrs + dayhrs.painthrs + dayhrs.bodyhrs
|
||||
: dayhrs.painthrs + dayhrs.bodyhrs,
|
||||
1
|
||||
),
|
||||
sales: _.round(dayhrs.sales, 2),
|
||||
accSales: _.round(acc.length > 0 ? acc[acc.length - 1].accSales + dayhrs.sales : dayhrs.sales, 2)
|
||||
sales: round(dayhrs.sales, 2),
|
||||
accSales: round(acc.length > 0 ? acc[acc.length - 1].accSales + dayhrs.sales : dayhrs.sales, 2)
|
||||
};
|
||||
|
||||
return [...acc, theValue];
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import { Col, Row } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { Col, Row, Spin } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import ScoreboardChart from "../scoreboard-chart/scoreboard-chart.component";
|
||||
import ScoreboardLastDays from "../scoreboard-last-days/scoreboard-last-days.component";
|
||||
import ScoreboardTargetsTable from "../scoreboard-targets-table/scoreboard-targets-table.component";
|
||||
|
||||
import { useApolloClient, useQuery } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_BLOCKED_DAYS, QUERY_SCOREBOARD } from "../../graphql/scoreboard.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import dayjs from "../../utils/day";
|
||||
import {
|
||||
clearHolidays,
|
||||
clearWorkingWeekdays,
|
||||
setHolidays,
|
||||
setWorkingWeekdays
|
||||
} from "../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
const mapDispatchToProps = () => ({});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardDisplayComponent);
|
||||
|
||||
export function ScoreboardDisplayComponent({ bodyshop }) {
|
||||
@@ -26,63 +28,76 @@ export function ScoreboardDisplayComponent({ bodyshop }) {
|
||||
start: dayjs().startOf("month"),
|
||||
end: dayjs().endOf("month")
|
||||
},
|
||||
pollInterval: 60000*5
|
||||
pollInterval: 60000 * 5
|
||||
});
|
||||
|
||||
const { data } = scoreboardSubscription;
|
||||
const client = useApolloClient();
|
||||
const scoreBoardlist = (data && data.scoreboard) || [];
|
||||
|
||||
const scoreBoardlist = data?.scoreboard || [];
|
||||
const sbEntriesByDate = {};
|
||||
|
||||
scoreBoardlist.forEach((i) => {
|
||||
const entryDate = i.date;
|
||||
if (!!!sbEntriesByDate[entryDate]) {
|
||||
if (!sbEntriesByDate[entryDate]) {
|
||||
sbEntriesByDate[entryDate] = [];
|
||||
}
|
||||
sbEntriesByDate[entryDate].push(i);
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(true); // Loading state
|
||||
|
||||
useEffect(() => {
|
||||
//Update the locals.
|
||||
async function setDayJSSettings() {
|
||||
let appointments;
|
||||
try {
|
||||
let appointments;
|
||||
|
||||
if (!bodyshop.scoreboard_target.ignoreblockeddays) {
|
||||
const { data } = await client.query({
|
||||
query: GET_BLOCKED_DAYS,
|
||||
variables: {
|
||||
start: dayjs().startOf("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"))
|
||||
if (!bodyshop.scoreboard_target.ignoreblockeddays) {
|
||||
const { data } = await client.query({
|
||||
query: GET_BLOCKED_DAYS,
|
||||
variables: {
|
||||
start: dayjs().startOf("month"),
|
||||
end: dayjs().endOf("month")
|
||||
}
|
||||
: {}),
|
||||
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();
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
clearHolidays();
|
||||
clearWorkingWeekdays();
|
||||
};
|
||||
}, [client, bodyshop]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Row justify="center" align="middle" style={{ minHeight: "100vh" }}>
|
||||
<Spin size="large" />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<ScoreboardTargetsTable scoreBoardlist={scoreBoardlist} />
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<ScoreboardLastDays sbEntriesByDate={sbEntriesByDate} />
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<ScoreboardChart sbEntriesByDate={sbEntriesByDate} />
|
||||
</Col>
|
||||
@@ -92,27 +107,12 @@ export function ScoreboardDisplayComponent({ bodyshop }) {
|
||||
|
||||
function translateSettingsToWorkingDays(workingdays) {
|
||||
const days = [];
|
||||
|
||||
if (workingdays.monday) {
|
||||
days.push(1);
|
||||
}
|
||||
if (workingdays.tuesday) {
|
||||
days.push(2);
|
||||
}
|
||||
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);
|
||||
}
|
||||
if (workingdays.monday) days.push(1);
|
||||
if (workingdays.tuesday) days.push(2);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
<>
|
||||
<Modal
|
||||
open={state.open}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
width="80%"
|
||||
closable={false}
|
||||
cancelButtonProps={{ style: { display: "none" } }}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
@@ -10,7 +9,7 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -26,7 +25,7 @@ export function ScoreboardLastDays({ bodyshop, sbEntriesByDate }) {
|
||||
<Row>
|
||||
{ArrayOfDate.map((a) => (
|
||||
<Col span={2} key={a}>
|
||||
{!!sbEntriesByDate ? <ScoreboardDayStat date={a} entries={sbEntriesByDate[a] || []} /> : <LoadingSkeleton />}
|
||||
{sbEntriesByDate ? <ScoreboardDayStat date={a} entries={sbEntriesByDate[a] || []} /> : <LoadingSkeleton />}
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { CalendarOutlined } from "@ant-design/icons";
|
||||
import { Card, Col, Divider, Row, Statistic } from "antd";
|
||||
import _ from "lodash";
|
||||
import { groupBy } from "lodash";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -13,7 +13,7 @@ import * as Util from "./scoreboard-targets-table.util";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const values = useMemo(() => {
|
||||
const dateHash = _.groupBy(scoreBoardlist, "date");
|
||||
const dateHash = groupBy(scoreBoardlist, "date");
|
||||
|
||||
let ret = {
|
||||
todayBody: 0,
|
||||
@@ -213,4 +213,5 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardTargetsTable);
|
||||
|
||||
@@ -1,29 +1,172 @@
|
||||
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) =>
|
||||
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) =>
|
||||
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();
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* 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 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 = () => {
|
||||
const days = [];
|
||||
let dateStart = dayjs().startOf("month");
|
||||
@@ -36,6 +179,13 @@ export const ListOfDaysInCurrentMonth = () => {
|
||||
return days;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a list of all days between two dates.
|
||||
* @param start
|
||||
* @param end
|
||||
* @returns {*[]}
|
||||
* @constructor
|
||||
*/
|
||||
export const ListDaysBetween = ({ start, end }) => {
|
||||
const days = [];
|
||||
let dateStart = dayjs(start);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
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 { useForm } from "antd/es/form/Form";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useEffect } from "react";
|
||||
import queryString from "query-string";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import {
|
||||
CHECK_EMPLOYEE_NUMBER,
|
||||
@@ -20,19 +22,17 @@ import {
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CiecaSelect from "../../utils/Ciecaselect";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
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 LayoutFormRow from "../layout-form-row/layout-form-row.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({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => {
|
||||
.then(() => {
|
||||
notification["success"]({
|
||||
message: t("employees.successes.save")
|
||||
});
|
||||
@@ -120,13 +120,13 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
title: t("employees.fields.vacation.start"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
||||
render: (text) => <DateFormatter>{text}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("employees.fields.vacation.end"),
|
||||
dataIndex: "end",
|
||||
key: "end",
|
||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
||||
render: (text) => <DateFormatter>{text}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("employees.fields.vacation.length"),
|
||||
@@ -210,7 +210,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
() => ({
|
||||
async validator(rule, value) {
|
||||
if (value) {
|
||||
const response = await client.query({
|
||||
@@ -369,8 +369,9 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
id="add-employee-rate-button"
|
||||
>
|
||||
{t("employees.actions.newrate")}
|
||||
<span id="new-employee-rate">{t("employees.actions.newrate")}</span>
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
@@ -383,7 +384,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
title={() => <ShopEmployeeAddVacation employee={data && data.employees_by_pk} />}
|
||||
columns={columns}
|
||||
rowKey={"id"}
|
||||
dataSource={data ? data.employees_by_pk.employee_vacations : []}
|
||||
dataSource={data?.employees_by_pk?.employee_vacations ?? []}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Card, Tabs } from "antd";
|
||||
import React from "react";
|
||||
import queryString from "query-string";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
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 ShopInfoIntakeChecklistComponent from "./shop-info.intake.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 ShopInfoPartsScan from "./shop-info.parts-scan";
|
||||
import ShopInfoRbacComponent from "./shop-info.rbac.component";
|
||||
import ShopInfoResponsibilityCenterComponent from "./shop-info.responsibilitycenters.component";
|
||||
import ShopInfoRoGuard from "./shop-info.roguard.component";
|
||||
import ShopInfoROStatusComponent from "./shop-info.rostatus.component";
|
||||
import ShopInfoSchedulingComponent from "./shop-info.scheduling.component";
|
||||
import ShopInfoSpeedPrint from "./shop-info.speedprint.component";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
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 { 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({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent);
|
||||
@@ -158,7 +157,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
||||
return (
|
||||
<Card
|
||||
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")}
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Typography } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import PhoneNumberConsentList from "../phone-number-consent/phone-number-consent.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
function ShopInfoConsentComponent({ bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4}>{t("settings.title")}</Typography.Title>
|
||||
{<PhoneNumberConsentList bodyshop={bodyshop} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoConsentComponent);
|
||||
@@ -1,7 +1,6 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, DatePicker, Form, Input, InputNumber, Radio, Select, Space, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
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 LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const timeZonesList = Intl.supportedValuesOf("timeZone");
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
|
||||
@@ -144,236 +144,246 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<FeatureWrapper featureName="export" noauth={() => null}>
|
||||
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")} id="accountingsetup">
|
||||
<Form.Item label={t("bodyshop.labels.qbo")} valuePropName="checked" name={["accountingconfig", "qbo"]}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => (
|
||||
<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>
|
||||
{InstanceRenderManager({
|
||||
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
|
||||
label={t("bodyshop.labels.qbo_usa")}
|
||||
label={t("bodyshop.labels.2tiersetup")}
|
||||
shouldUpdate
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "qbo_usa"]}
|
||||
rules={[
|
||||
{
|
||||
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 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.printlater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "printlater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.emaillater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "emaillater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.inhousevendorid")}
|
||||
name={"inhousevendorid"}
|
||||
rules={[
|
||||
{
|
||||
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
|
||||
label={t("bodyshop.labels.2tiersetup")}
|
||||
shouldUpdate
|
||||
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
||||
name={["bill_tax_rates", "federal_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["accountingconfig", "tiers"]) === 2
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["accountingconfig", "twotierpref"]}
|
||||
>
|
||||
<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>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.printlater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "printlater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.emaillater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "emaillater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.inhousevendorid")}
|
||||
name={"inhousevendorid"}
|
||||
rules={[
|
||||
{
|
||||
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">
|
||||
)
|
||||
})}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
||||
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>
|
||||
{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 />
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item label={t("bodyshop.fields.state_tax_id")} name="state_tax_id">
|
||||
<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 />
|
||||
)}
|
||||
{ADPPayroll.treatment === "on" && (
|
||||
<Form.Item name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<FeatureWrapper featureName="scoreboard" noauth={() => null}>
|
||||
<LayoutFormRow header={t("bodyshop.labels.scoreboardsetup")} id="scoreboardsetup">
|
||||
<Form.Item
|
||||
@@ -823,7 +833,11 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
}}
|
||||
</Form.List>
|
||||
</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"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function ShopInfoNotificationsAutoadd({ bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Filter employee options to ensure active employees with valid IDs
|
||||
const employeeOptions = bodyshop?.employees?.filter((e) => e.active && e.id && typeof e.id === "string") || [];
|
||||
const employeeOptions = bodyshop?.employees?.filter((e) => e.active && e.user_email && e.id) || [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user