Compare commits
138 Commits
feature/IO
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 | ||
|
|
b539111be8 | ||
|
|
8a8bc5a6ed | ||
|
|
020db91105 | ||
|
|
1dd28af752 | ||
|
|
5ba192eee0 | ||
|
|
8109a12898 | ||
|
|
2deb7fd520 | ||
|
|
f6cd136679 | ||
|
|
e50cb86296 | ||
|
|
a5a01c44fa | ||
|
|
947e0705e4 | ||
|
|
aa8a6a837d | ||
|
|
5db440fc9c | ||
|
|
c299b9376a | ||
|
|
e5d530ea3e | ||
|
|
6da9850946 | ||
|
|
f62609f60c | ||
|
|
b2d8c66e5b | ||
|
|
3c4ed3ba0c | ||
|
|
2e7f827c3f | ||
|
|
dc82b39dc8 | ||
|
|
a9814c1eb1 | ||
|
|
bdb741caf8 | ||
|
|
f50b198c21 | ||
|
|
3495326de3 | ||
|
|
b5973085e7 | ||
|
|
8687214420 | ||
|
|
d61b89a1e5 | ||
|
|
468b42abd2 | ||
|
|
fc03e5f983 | ||
|
|
c4742e38ea | ||
|
|
99e1adbe13 | ||
|
|
eb5c797a43 | ||
|
|
0595c5545e | ||
|
|
12c87ed689 | ||
|
|
55944257aa | ||
|
|
03241778fa | ||
|
|
555b81fb14 |
File diff suppressed because it is too large
Load Diff
@@ -74,50 +74,8 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
<script>
|
<script>!function(w,d,i,s){function l(){if(!d.getElementById(i)){var f=d.getElementsByTagName(s)[0],e=d.createElement(s);e.type="text/javascript",e.async=!0,e.src="https://canny.io/sdk.js",f.parentNode.insertBefore(e,f)}}if("function"!=typeof w.Canny){var c=function(){c.q.push(arguments)};c.q=[],w.Canny=c,"complete"===d.readyState?l():w.attachEvent?w.attachEvent("onload",l):w.addEventListener("load",l,!1)}}(window,document,"canny-jssdk","script");</script>
|
||||||
!(function () {
|
|
||||||
"use strict";
|
|
||||||
var e = [
|
|
||||||
"debug",
|
|
||||||
"destroy",
|
|
||||||
"do",
|
|
||||||
"help",
|
|
||||||
"identify",
|
|
||||||
"is",
|
|
||||||
"off",
|
|
||||||
"on",
|
|
||||||
"ready",
|
|
||||||
"render",
|
|
||||||
"reset",
|
|
||||||
"safe",
|
|
||||||
"set"
|
|
||||||
];
|
|
||||||
if (window.noticeable) console.warn("Noticeable SDK code snippet loaded more than once");
|
|
||||||
else {
|
|
||||||
var n = (window.noticeable = window.noticeable || []);
|
|
||||||
|
|
||||||
function t(e) {
|
|
||||||
return function () {
|
|
||||||
var t = Array.prototype.slice.call(arguments);
|
|
||||||
return t.unshift(e), n.push(t), n;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
!(function () {
|
|
||||||
for (var o = 0; o < e.length; o++) {
|
|
||||||
var r = e[o];
|
|
||||||
n[r] = t(r);
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
(function () {
|
|
||||||
var e = document.createElement("script");
|
|
||||||
(e.async = !0), (e.src = "https://sdk.noticeable.io/l.js");
|
|
||||||
var n = document.head;
|
|
||||||
n.insertBefore(e, n.firstChild);
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
2759
client/package-lock.json
generated
2759
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,21 +12,21 @@
|
|||||||
"@apollo/client": "^3.13.6",
|
"@apollo/client": "^3.13.6",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||||
"@firebase/analytics": "^0.10.12",
|
"@firebase/analytics": "^0.10.16",
|
||||||
"@firebase/app": "^0.11.4",
|
"@firebase/app": "^0.13.0",
|
||||||
"@firebase/auth": "^1.10.0",
|
"@firebase/auth": "^1.10.6",
|
||||||
"@firebase/firestore": "^4.7.10",
|
"@firebase/firestore": "^4.7.16",
|
||||||
"@firebase/messaging": "^0.12.17",
|
"@firebase/messaging": "^0.12.21",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.6.1",
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"@sentry/cli": "^2.43.0",
|
"@sentry/cli": "^2.46.0",
|
||||||
"@sentry/react": "^9.11.0",
|
"@sentry/react": "^9.23.0",
|
||||||
"@sentry/vite-plugin": "^3.3.1",
|
"@sentry/vite-plugin": "^3.5.0",
|
||||||
"@splitsoftware/splitio-react": "^2.1.1",
|
"@splitsoftware/splitio-react": "^2.1.1",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"antd": "^5.24.6",
|
"antd": "^5.25.3",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^4.2.0",
|
"apollo-link-sentry": "^4.3.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -37,18 +37,19 @@
|
|||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.11.0",
|
||||||
"i18next": "^24.2.3",
|
"i18next": "^24.2.3",
|
||||||
"i18next-browser-languagedetector": "^8.0.4",
|
"i18next-browser-languagedetector": "^8.1.0",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.12.6",
|
"libphonenumber-js": "^1.12.8",
|
||||||
"logrocket": "^9.0.2",
|
"logrocket": "^9.0.2",
|
||||||
"markerjs2": "^2.32.4",
|
"markerjs2": "^2.32.4",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-url": "^8.0.1",
|
"normalize-url": "^8.0.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
|
"phone": "^3.1.59",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.1.1",
|
"query-string": "^9.2.0",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.18.0",
|
"react-big-calendar": "^1.18.0",
|
||||||
@@ -57,8 +58,8 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-drag-listview": "^2.0.0",
|
"react-drag-listview": "^2.0.0",
|
||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "^1.3.4",
|
"react-grid-layout": "1.3.4",
|
||||||
"react-i18next": "^15.4.1",
|
"react-i18next": "^15.5.2",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^6.30.0",
|
"react-router-dom": "^6.30.0",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtuoso": "^4.12.5",
|
"react-virtuoso": "^4.12.7",
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
@@ -77,9 +78,9 @@
|
|||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.86.3",
|
"sass": "^1.89.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.17",
|
"styled-components": "^6.1.18",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"use-memo-one": "^1.1.3",
|
"use-memo-one": "^1.1.3",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
@@ -129,18 +130,18 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^6.0.0",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.27.1",
|
||||||
"@dotenvx/dotenvx": "^1.39.1",
|
"@dotenvx/dotenvx": "^1.44.1",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.24.0",
|
"@eslint/js": "^9.27.0",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.51.1",
|
||||||
"@sentry/webpack-plugin": "^3.3.1",
|
"@sentry/webpack-plugin": "^3.5.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.5.0",
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.24.5",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
@@ -148,19 +149,19 @@
|
|||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"memfs": "^4.17.0",
|
"memfs": "^4.17.2",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"playwright": "^1.51.1",
|
"playwright": "^1.51.1",
|
||||||
"react-error-overlay": "^6.1.0",
|
"react-error-overlay": "^6.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^6.2.5",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-babel": "^1.3.0",
|
"vite-plugin-babel": "^1.3.1",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
"vite-plugin-pwa": "^1.0.0",
|
"vite-plugin-pwa": "^1.0.0",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"vitest": "^3.1.1",
|
"vitest": "^3.1.4",
|
||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,21 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
setPartsOrderContext: (context) =>
|
||||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "partsOrder"
|
||||||
|
})
|
||||||
|
),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
|
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
|
||||||
@@ -69,7 +82,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, b
|
|||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
title={t("bills.actions.return")}
|
title={t("bills.actions.return")}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function BillDetailEditcontainer() {
|
|||||||
delete search.billid;
|
delete search.billid;
|
||||||
history({ search: queryString.stringify(search) });
|
history({ search: queryString.stringify(search) });
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
open={search.billid}
|
open={search.billid}
|
||||||
>
|
>
|
||||||
<BillDetailEditComponent />
|
<BillDetailEditComponent />
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisi
|
|||||||
title={t("payments.labels.findermodal")}
|
title={t("payments.labels.findermodal")}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
onOk={() => toggleModalVisible()}
|
onOk={() => toggleModalVisible()}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
forceRender
|
forceRender
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
<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 React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
|
||||||
export default function CABCpvrtCalculator({ disabled, form }) {
|
export default function CABCpvrtCalculator({ disabled, form }) {
|
||||||
const [visibility, setVisibility] = useState(false);
|
const [visibility, setVisibility] = useState(false);
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
|
<Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
|
||||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||||
<CalculatorFilled />
|
<CalculatorFilled />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodys
|
|||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
width="80%"
|
width="80%"
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<CardPaymentModalComponent />
|
<CardPaymentModalComponent />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -34,16 +34,14 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
|||||||
|
|
||||||
SubscribeToTopicForFCMNotification();
|
SubscribeToTopicForFCMNotification();
|
||||||
|
|
||||||
//Register WS handlers
|
// Register WebSocket handlers
|
||||||
if (socket && socket.connected) {
|
if (socket && socket.connected) {
|
||||||
registerMessagingHandlers({ socket, client });
|
registerMessagingHandlers({ socket, client });
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (socket && socket.connected) {
|
|
||||||
unregisterMessagingHandlers({ socket });
|
unregisterMessagingHandlers({ socket });
|
||||||
}
|
};
|
||||||
};
|
}
|
||||||
}, [bodyshop, socket, t, client]);
|
}, [bodyshop, socket, t, client]);
|
||||||
|
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||||
|
|||||||
@@ -202,8 +202,6 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
text: message.text
|
text: message.text
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add cases for other known message types as needed
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Log a warning for unhandled message types
|
// Log a warning for unhandled message types
|
||||||
logLocal("handleMessageChanged - Unhandled message type", { type: message.type });
|
logLocal("handleMessageChanged - Unhandled message type", { type: message.type });
|
||||||
@@ -211,7 +209,7 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageRef; // Keep other messages unchanged
|
return messageRef;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,11 +243,8 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updatedList = existingList?.conversations
|
const updatedList = existingList?.conversations
|
||||||
? [
|
? [newConversation, ...existingList.conversations.filter((conv) => conv.id !== newConversation.id)]
|
||||||
newConversation,
|
: [newConversation]; // Prevent duplicates
|
||||||
...existingList.conversations.filter((conv) => conv.id !== newConversation.id) // Prevent duplicates
|
|
||||||
]
|
|
||||||
: [newConversation];
|
|
||||||
|
|
||||||
client.cache.writeQuery({
|
client.cache.writeQuery({
|
||||||
query: CONVERSATION_LIST_QUERY,
|
query: CONVERSATION_LIST_QUERY,
|
||||||
@@ -403,6 +398,7 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logLocal("handleConversationChanged - Unhandled type", { type });
|
logLocal("handleConversationChanged - Unhandled type", { type });
|
||||||
client.cache.modify({
|
client.cache.modify({
|
||||||
@@ -419,10 +415,95 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Existing handler for phone number opt-out
|
||||||
|
const handlePhoneNumberOptedOut = async (data) => {
|
||||||
|
const { bodyshopid, phone_number } = data;
|
||||||
|
logLocal("handlePhoneNumberOptedOut - Start", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.cache.modify({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fields: {
|
||||||
|
phone_number_opt_out(existing = [], { readField }) {
|
||||||
|
const phoneNumberExists = existing.some(
|
||||||
|
(ref) => readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid
|
||||||
|
);
|
||||||
|
|
||||||
|
if (phoneNumberExists) {
|
||||||
|
logLocal("handlePhoneNumberOptedOut - Phone number already in cache", { phone_number, bodyshopid });
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newOptOut = {
|
||||||
|
__typename: "phone_number_opt_out",
|
||||||
|
id: `temporary-${phone_number}-${Date.now()}`,
|
||||||
|
bodyshopid,
|
||||||
|
phone_number,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
return [...existing, newOptOut];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcast: true
|
||||||
|
});
|
||||||
|
|
||||||
|
client.cache.evict({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fieldName: "phone_number_opt_out",
|
||||||
|
args: { bodyshopid, search: phone_number }
|
||||||
|
});
|
||||||
|
client.cache.gc();
|
||||||
|
|
||||||
|
logLocal("handlePhoneNumberOptedOut - Cache updated successfully", data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating cache for phone number opt-out:", error);
|
||||||
|
logLocal("handlePhoneNumberOptedOut - Error", { error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// New handler for phone number opt-in
|
||||||
|
const handlePhoneNumberOptedIn = async (data) => {
|
||||||
|
const { bodyshopid, phone_number } = data;
|
||||||
|
logLocal("handlePhoneNumberOptedIn - Start", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update the Apollo cache for GET_PHONE_NUMBER_OPT_OUTS by removing the phone number
|
||||||
|
client.cache.modify({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fields: {
|
||||||
|
phone_number_opt_out(existing = [], { readField }) {
|
||||||
|
// Filter out the phone number from the opt-out list
|
||||||
|
return existing.filter(
|
||||||
|
(ref) => !(readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcast: true // Trigger UI updates
|
||||||
|
});
|
||||||
|
|
||||||
|
// Evict the cache entry to force a refetch on next query
|
||||||
|
client.cache.evict({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fieldName: "phone_number_opt_out",
|
||||||
|
args: { bodyshopid, search: phone_number }
|
||||||
|
});
|
||||||
|
client.cache.gc();
|
||||||
|
|
||||||
|
logLocal("handlePhoneNumberOptedIn - Cache updated successfully", data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating cache for phone number opt-in:", error);
|
||||||
|
logLocal("handlePhoneNumberOptedIn - Error", { error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
socket.on("new-message-summary", handleNewMessageSummary);
|
socket.on("new-message-summary", handleNewMessageSummary);
|
||||||
socket.on("new-message-detailed", handleNewMessageDetailed);
|
socket.on("new-message-detailed", handleNewMessageDetailed);
|
||||||
socket.on("message-changed", handleMessageChanged);
|
socket.on("message-changed", handleMessageChanged);
|
||||||
socket.on("conversation-changed", handleConversationChanged);
|
socket.on("conversation-changed", handleConversationChanged);
|
||||||
|
socket.on("phone-number-opted-out", handlePhoneNumberOptedOut);
|
||||||
|
socket.on("phone-number-opted-in", handlePhoneNumberOptedIn);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unregisterMessagingHandlers = ({ socket }) => {
|
export const unregisterMessagingHandlers = ({ socket }) => {
|
||||||
@@ -431,4 +512,6 @@ export const unregisterMessagingHandlers = ({ socket }) => {
|
|||||||
socket.off("new-message-detailed");
|
socket.off("new-message-detailed");
|
||||||
socket.off("message-changed");
|
socket.off("message-changed");
|
||||||
socket.off("conversation-changed");
|
socket.off("conversation-changed");
|
||||||
|
socket.off("phone-number-opted-out");
|
||||||
|
socket.off("phone-number-opted-in");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Badge, Card, List, Space, Tag } from "antd";
|
import { Badge, Card, List, Space, Tag, Tooltip } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -9,36 +9,62 @@ import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
|||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||||
import "./chat-conversation-list.styles.scss";
|
import "./chat-conversation-list.styles.scss";
|
||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { 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({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation
|
selectedConversation: selectSelectedConversation,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
||||||
});
|
});
|
||||||
|
|
||||||
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) {
|
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) {
|
||||||
// That comma is there for a reason, do not remove it
|
const { t } = useTranslation();
|
||||||
const [, forceUpdate] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
forceUpdate((prev) => !prev); // Toggle state to trigger re-render
|
forceUpdate((prev) => !prev);
|
||||||
}, 60000); // 1 minute in milliseconds
|
}, 60000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
return () => clearInterval(interval); // Cleanup on unmount
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Memoize the sorted conversation list
|
const sortedConversationList = useMemo(() => {
|
||||||
const sortedConversationList = React.useMemo(() => {
|
|
||||||
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
|
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
|
||||||
}, [conversationList]);
|
}, [conversationList]);
|
||||||
|
|
||||||
const renderConversation = (index) => {
|
const renderConversation = (index, t) => {
|
||||||
const item = sortedConversationList[index];
|
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 cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||||
const cardContentLeft =
|
const cardContentLeft =
|
||||||
item.job_conversations.length > 0
|
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 = () =>
|
const getCardStyle = () =>
|
||||||
item.id === selectedConversation
|
item.id === selectedConversation
|
||||||
@@ -73,9 +110,25 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
onClick={() => setSelectedConversation(item.id)}
|
onClick={() => setSelectedConversation(item.id)}
|
||||||
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
||||||
>
|
>
|
||||||
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
<Card style={getCardStyle()} variant={true} size="small" extra={cardExtra} title={cardTitle}>
|
||||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
<div
|
||||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: "70%",
|
||||||
|
textAlign: "left"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cardContentLeft}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: "30%",
|
||||||
|
textAlign: "right"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cardContentRight}
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
);
|
);
|
||||||
@@ -85,7 +138,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
<div className="chat-list-container">
|
<div className="chat-list-container">
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
data={sortedConversationList}
|
data={sortedConversationList}
|
||||||
itemContent={(index) => renderConversation(index)}
|
itemContent={(index) => renderConversation(index, t)}
|
||||||
style={{ height: "100%", width: "100%" }}
|
style={{ height: "100%", width: "100%" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
/* Add spacing and better alignment for items */
|
/* Add spacing and better alignment for items */
|
||||||
.chat-list-item {
|
.chat-list-item {
|
||||||
padding: 0.5rem 0; /* Add spacing between list items */
|
padding: 0.2rem 0; /* Add spacing between list items */
|
||||||
|
|
||||||
.ant-card {
|
.ant-card {
|
||||||
border-radius: 8px; /* Slight rounding for card edges */
|
border-radius: 8px; /* Slight rounding for card edges */
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
|||||||
userid
|
userid
|
||||||
created_at
|
created_at
|
||||||
read
|
read
|
||||||
|
is_system
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
data: message
|
data: message
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-document
|
|||||||
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
|
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
|
||||||
import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
import "./chat-media-selector.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
});
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
||||||
|
|
||||||
export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
|
export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
|
||||||
@@ -37,9 +38,8 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
variables: {
|
variables: {
|
||||||
jobId: conversation.job_conversations[0] && conversation.job_conversations[0].jobid
|
jobId: conversation.job_conversations[0]?.jobid
|
||||||
},
|
},
|
||||||
|
|
||||||
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
|
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,25 +56,25 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
//If Imageproxy is on, rely only on the LMS selector
|
//If Imageproxy is on, rely only on the LMS selector
|
||||||
//If not on, use the old methods.
|
//If not on, use the old methods.
|
||||||
const content = (
|
const content = (
|
||||||
<div>
|
<div className="media-selector-content">
|
||||||
{loading && <LoadingSpinner />}
|
{loading && <LoadingSpinner />}
|
||||||
{error && <AlertComponent message={error.message} type="error" />}
|
{error && <AlertComponent message={error.message} type="error" />}
|
||||||
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
||||||
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
<div className="error-message">{t("messaging.labels.maxtenimages")}</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{Imgproxy.treatment === "on" ? (
|
{Imgproxy.treatment === "on" ? (
|
||||||
<>
|
<>
|
||||||
{!bodyshop.uselocalmediaserver && (
|
{!bodyshop.uselocalmediaserver && (
|
||||||
<JobsDocumentImgproxyGalleryExternal
|
<JobsDocumentImgproxyGalleryExternal
|
||||||
jobId={conversation.job_conversations[0].jobid}
|
jobId={conversation.job_conversations[0]?.jobid}
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{bodyshop.uselocalmediaserver && open && (
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
<JobDocumentsLocalGalleryExternal
|
<JobDocumentsLocalGalleryExternal
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
jobId={conversation.job_conversations[0]?.jobid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -89,7 +89,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
{bodyshop.uselocalmediaserver && open && (
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
<JobDocumentsLocalGalleryExternal
|
<JobDocumentsLocalGalleryExternal
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
jobId={conversation.job_conversations[0]?.jobid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -100,12 +100,17 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
content={
|
content={
|
||||||
conversation.job_conversations.length === 0 ? <div>{t("messaging.errors.noattachedjobs")}</div> : content
|
conversation.job_conversations.length === 0 ? (
|
||||||
|
<div className="no-jobs-message">{t("messaging.errors.noattachedjobs")}</div>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)
|
||||||
}
|
}
|
||||||
title={t("messaging.labels.selectmedia")}
|
title={t("messaging.labels.selectmedia")}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={handleVisibleChange}
|
onOpenChange={handleVisibleChange}
|
||||||
|
overlayClassName="media-selector-popover"
|
||||||
>
|
>
|
||||||
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
||||||
<PictureFilled style={{ margin: "0 .5rem" }} />
|
<PictureFilled style={{ margin: "0 .5rem" }} />
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
.media-selector-popover {
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
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;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid layout for gallery components */
|
||||||
|
.media-selector-content .ant-image, /* Assuming gallery components use Ant Design's Image */
|
||||||
|
.media-selector-content .gallery-container { /* Fallback for custom gallery classes */
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
@@ -4,13 +4,16 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.archive-button {
|
.archive-button {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-title {
|
.chat-title {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages {
|
.messages {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -37,11 +40,13 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.chat-send-message-button{
|
|
||||||
|
.chat-send-message-button {
|
||||||
margin: 0.3rem;
|
margin: 0.3rem;
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-icon {
|
.message-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0.1rem;
|
bottom: 0.1rem;
|
||||||
@@ -125,6 +130,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.system {
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.5rem 10%;
|
||||||
|
|
||||||
|
.message {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
color: #555;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.virtuoso-container {
|
.virtuoso-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@@ -2,17 +2,29 @@ import Icon from "@ant-design/icons";
|
|||||||
import { Tooltip } from "antd";
|
import { Tooltip } from "antd";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import { MdDone, MdDoneAll } from "react-icons/md";
|
import { MdClose, MdDone, MdDoneAll } from "react-icons/md";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
export const renderMessage = (messages, index) => {
|
export const renderMessage = (messages, index) => {
|
||||||
const message = messages[index];
|
const message = messages[index];
|
||||||
|
const isSystem = message.is_system;
|
||||||
|
|
||||||
|
// Determine message class
|
||||||
|
const messageClass = isSystem ? "system messages" : message.isoutbound ? "mine messages" : "yours messages";
|
||||||
|
|
||||||
|
// Tooltip content based on message type
|
||||||
|
const tooltipTitle = isSystem ? (
|
||||||
|
i18n.t("consent.text_body")
|
||||||
|
) : (
|
||||||
|
<DateTimeFormatter>{message.created_at}</DateTimeFormatter>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className={`${message.isoutbound ? "mine messages" : "yours messages"}`}>
|
<div key={index} className={messageClass}>
|
||||||
<div className="message msgmargin">
|
<div className="message msgmargin">
|
||||||
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
<Tooltip title={tooltipTitle}>
|
||||||
<div>
|
<div>
|
||||||
|
{isSystem && <span className="system-label">System</span>}
|
||||||
{/* Render images if available */}
|
{/* Render images if available */}
|
||||||
{message.image && message.image_path?.length > 0 && (
|
{message.image && message.image_path?.length > 0 && (
|
||||||
<div className="message-images">
|
<div className="message-images">
|
||||||
@@ -26,20 +38,31 @@ export const renderMessage = (messages, index) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Render text if available */}
|
{/* Render text if available */}
|
||||||
{message.text && <div>{message.text}</div>}
|
{message.text && <div className="message-text">{message.text}</div>}
|
||||||
|
{/* Render date for system messages */}
|
||||||
|
{isSystem && (
|
||||||
|
<div className="system-date">
|
||||||
|
<DateTimeFormatter>{message.created_at}</DateTimeFormatter>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Message status icons */}
|
{/* Message status icons for non-system messages */}
|
||||||
{message.status && (message.status === "sent" || message.status === "delivered") && (
|
{!isSystem &&
|
||||||
<div className="message-status">
|
message.status &&
|
||||||
<Icon component={message.status === "sent" ? MdDone : MdDoneAll} className="message-icon" />
|
(message.status === "sent" || message.status === "delivered" || message.status === "failed") && (
|
||||||
</div>
|
<div className="message-status">
|
||||||
)}
|
<Icon
|
||||||
|
component={message.status === "sent" ? MdDone : message.status === "delivered" ? MdDoneAll : MdClose}
|
||||||
|
className="message-icon"
|
||||||
|
style={message.status === "failed" ? { color: "#ff0000" } : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Outbound message metadata for non-system messages */}
|
||||||
{/* Outbound message metadata */}
|
{!isSystem && message.isoutbound && (
|
||||||
{message.isoutbound && (
|
|
||||||
<div style={{ fontSize: 10 }}>
|
<div style={{ fontSize: 10 }}>
|
||||||
{i18n.t("messaging.labels.sentby", {
|
{i18n.t("messaging.labels.sentby", {
|
||||||
by: message.userid,
|
by: message.userid,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
import { ExclamationCircleOutlined, LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
||||||
import { Input, Spin } from "antd";
|
import { Alert, Input, Space, Spin, Tooltip } from "antd";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -10,6 +10,9 @@ import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
|
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
|
||||||
import ChatPresetsComponent from "../chat-presets/chat-presets.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({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -25,16 +28,24 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
||||||
const inputArea = useRef(null);
|
const inputArea = useRef(null);
|
||||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
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(() => {
|
useEffect(() => {
|
||||||
inputArea.current.focus();
|
inputArea.current.focus();
|
||||||
}, [isSending, setMessage]);
|
}, [isSending, setMessage]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleEnter = () => {
|
const handleEnter = () => {
|
||||||
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
||||||
if ((message === "" || !message) && selectedImages.length === 0) return;
|
if ((message === "" || !message) && selectedImages.length === 0) return;
|
||||||
|
if (isOptedOut) return; // Prevent sending if phone number is opted out
|
||||||
logImEXEvent("messaging_send_message");
|
logImEXEvent("messaging_send_message");
|
||||||
|
|
||||||
if (selectedImages.length < 11) {
|
if (selectedImages.length < 11) {
|
||||||
@@ -44,7 +55,8 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
messagingServiceSid: bodyshop.messagingservicesid,
|
messagingServiceSid: bodyshop.messagingservicesid,
|
||||||
conversationid: conversation.id,
|
conversationid: conversation.id,
|
||||||
selectedMedia: selectedImages,
|
selectedMedia: selectedImages,
|
||||||
imexshopid: bodyshop.imexshopid
|
imexshopid: bodyshop.imexshopid,
|
||||||
|
bodyshopid: bodyshop.id
|
||||||
};
|
};
|
||||||
sendMessage(newMessage);
|
sendMessage(newMessage);
|
||||||
setSelectedMedia(
|
setSelectedMedia(
|
||||||
@@ -56,47 +68,67 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="imex-flex-row" style={{ width: "100%" }}>
|
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
||||||
<ChatPresetsComponent className="imex-flex-row__margin" />
|
{isOptedOut && (
|
||||||
<ChatMediaSelector
|
<Tooltip title={t("consent.text_body")}>
|
||||||
conversation={conversation}
|
<Alert
|
||||||
selectedMedia={selectedMedia}
|
showIcon={true}
|
||||||
setSelectedMedia={setSelectedMedia}
|
icon={<ExclamationCircleOutlined />}
|
||||||
/>
|
message={t("messaging.errors.no_consent")}
|
||||||
<span style={{ flex: 1 }}>
|
type="error"
|
||||||
<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
|
|
||||||
/>
|
/>
|
||||||
}
|
</Tooltip>
|
||||||
/>
|
)}
|
||||||
</div>
|
<div className="imex-flex-row" style={{ width: "100%" }}>
|
||||||
|
{!isOptedOut && (
|
||||||
|
<>
|
||||||
|
<ChatPresetsComponent disabled={isSending} className="imex-flex-row__margin" />
|
||||||
|
<ChatMediaSelector
|
||||||
|
disabled={isSending}
|
||||||
|
conversation={conversation}
|
||||||
|
selectedMedia={selectedMedia}
|
||||||
|
setSelectedMedia={setSelectedMedia}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<span style={{ flex: 1 }}>
|
||||||
|
<Input.TextArea
|
||||||
|
className="imex-flex-row__margin imex-flex-row__grow"
|
||||||
|
allowClear
|
||||||
|
autoFocus
|
||||||
|
ref={inputArea}
|
||||||
|
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||||
|
value={message}
|
||||||
|
disabled={isSending || isOptedOut}
|
||||||
|
placeholder={t("messaging.labels.typeamessage")}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
onPressEnter={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!event.shiftKey && !isOptedOut) handleEnter();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{!isOptedOut && (
|
||||||
|
<SendOutlined
|
||||||
|
className="chat-send-message-button"
|
||||||
|
disabled={isSending || message === "" || !message}
|
||||||
|
onClick={handleEnter}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Spin
|
||||||
|
style={{ display: `${isSending ? "" : "none"}` }}
|
||||||
|
indicator={
|
||||||
|
<LoadingOutlined
|
||||||
|
style={{
|
||||||
|
fontSize: 24
|
||||||
|
}}
|
||||||
|
spin
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function ContractsFindModalContainer({
|
|||||||
title={t("contracts.labels.findermodal")}
|
title={t("contracts.labels.findermodal")}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
onOk={() => toggleModalVisible()}
|
onOk={() => toggleModalVisible()}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
forceRender
|
forceRender
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOv
|
|||||||
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
destroyOnClose={true}
|
destroyOnHidden
|
||||||
open={modalVisible}
|
open={modalVisible}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
width={"80%"}
|
width={"80%"}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
//To be used as a form element only.
|
//To be used as a form element only.
|
||||||
|
|
||||||
const EmployeeSearchSelect = ({ options, ...props }) => {
|
const EmployeeSearchSelect = ({ options, showEmail, ...props }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,12 +21,16 @@ const EmployeeSearchSelect = ({ options, ...props }) => {
|
|||||||
{options
|
{options
|
||||||
? options.map((o) => (
|
? options.map((o) => (
|
||||||
<Option key={o.id} value={o.id} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
|
<Option key={o.id} value={o.id} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
|
||||||
<Space>
|
<Space size="small">
|
||||||
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
{`${o.employee_number ?? ""} ${o.first_name} ${o.last_name}`}
|
||||||
|
<Tag color="green" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||||
<Tag color="green">
|
|
||||||
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
{showEmail && o.user_email ? (
|
||||||
|
<Tag color="blue" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||||
|
{o.user_email}
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
</Option>
|
</Option>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -81,8 +81,9 @@ export function HasFeatureAccess({ featureName, bodyshop, bypass, debug = false
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
bodyshop?.features?.allAccess ||
|
bodyshop?.features?.allAccess ||
|
||||||
bodyshop?.features?.[featureName] ||
|
(typeof bodyshop?.features?.[featureName] === "boolean"
|
||||||
dayjs(bodyshop?.features[featureName]).isAfter(dayjs())
|
? bodyshop?.features?.[featureName]
|
||||||
|
: dayjs(bodyshop?.features?.[featureName]).isAfter(dayjs()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
HomeFilled,
|
HomeFilled,
|
||||||
ImportOutlined,
|
ImportOutlined,
|
||||||
LineChartOutlined,
|
LineChartOutlined,
|
||||||
|
OneToOneOutlined,
|
||||||
PaperClipOutlined,
|
PaperClipOutlined,
|
||||||
PhoneOutlined,
|
PhoneOutlined,
|
||||||
PlusCircleOutlined,
|
PlusCircleOutlined,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
ToolFilled,
|
ToolFilled,
|
||||||
UnorderedListOutlined,
|
UnorderedListOutlined,
|
||||||
|
UsergroupAddOutlined,
|
||||||
UserOutlined
|
UserOutlined
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
@@ -40,6 +42,7 @@ import { RiSurveyLine } from "react-icons/ri";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
||||||
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
@@ -47,10 +50,10 @@ import { signOutStart } from "../../redux/user/user.actions";
|
|||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
||||||
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
|
||||||
|
|
||||||
// Redux mappings
|
// Redux mappings
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -98,6 +101,7 @@ function Header({
|
|||||||
const baseTitleRef = useRef(document.title || "");
|
const baseTitleRef = useRef(document.title || "");
|
||||||
const lastSetTitleRef = useRef("");
|
const lastSetTitleRef = useRef("");
|
||||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||||
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: unreadData,
|
data: unreadData,
|
||||||
@@ -640,17 +644,32 @@ function Header({
|
|||||||
label: t("menus.header.help"),
|
label: t("menus.header.help"),
|
||||||
onClick: () => window.open("https://help.imex.online/", "_blank")
|
onClick: () => window.open("https://help.imex.online/", "_blank")
|
||||||
},
|
},
|
||||||
...(InstanceRenderManager({ imex: true, rome: false })
|
{
|
||||||
? [
|
key: "remoteassist",
|
||||||
{
|
id: "header-remote-assist",
|
||||||
key: "rescue",
|
icon: <OneToOneOutlined />,
|
||||||
id: "header-rescue",
|
label: t("menus.header.remoteassist"),
|
||||||
icon: <CarFilled />,
|
children: [
|
||||||
label: t("menus.header.rescueme"),
|
...(InstanceRenderManager({ imex: true, rome: false })
|
||||||
onClick: () => window.open("https://imexrescue.com/", "_blank")
|
? [
|
||||||
}
|
{
|
||||||
]
|
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",
|
key: "shiftclock",
|
||||||
id: "header-shiftclock",
|
id: "header-shiftclock",
|
||||||
@@ -682,7 +701,7 @@ function Header({
|
|||||||
icon: unreadLoading ? (
|
icon: unreadLoading ? (
|
||||||
<Spin size="small" />
|
<Spin size="small" />
|
||||||
) : (
|
) : (
|
||||||
<Badge offset={[8, 0]} size="small" count={unreadCount}>
|
<Badge offset={[8, 0]} size="small" count={isEmployee ? unreadCount : 0}>
|
||||||
<BellFilled />
|
<BellFilled />
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export function InventoryUpsertModalContainer({ currentUser, bodyshop, inventory
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||||
<InventoryUpsertModal form={form} />
|
<InventoryUpsertModal form={form} />
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function ScheduleEventComponent({
|
|||||||
const [popOverVisible, setPopOverVisible] = useState(false);
|
const [popOverVisible, setPopOverVisible] = useState(false);
|
||||||
|
|
||||||
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
|
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
|
||||||
variables: { id: event.job.id },
|
variables: { id: event.job?.id },
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
if (data?.jobs_by_pk) {
|
if (data?.jobs_by_pk) {
|
||||||
const totalHours =
|
const totalHours =
|
||||||
@@ -83,6 +83,7 @@ export function ScheduleEventComponent({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchPolicy: "network-only"
|
fetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -394,30 +395,33 @@ export function ScheduleEventComponent({
|
|||||||
) : (
|
) : (
|
||||||
<ScheduleManualEvent event={event} />
|
<ScheduleManualEvent event={event} />
|
||||||
)}
|
)}
|
||||||
{event.isintake && HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
{event.job &&
|
||||||
<Link
|
(HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
||||||
to={{
|
<Link
|
||||||
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
to={{
|
||||||
search: `?appointmentId=${event.id}`
|
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
||||||
}}
|
search: `?appointmentId=${event.id}`
|
||||||
>
|
}}
|
||||||
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
>
|
||||||
</Link>
|
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
||||||
) : (
|
</Link>
|
||||||
<Popover //open={open}
|
) : (
|
||||||
content={popMenu}
|
<Popover //open={open}
|
||||||
open={popOverVisible}
|
content={popMenu}
|
||||||
onOpenChange={setPopOverVisible}
|
open={popOverVisible}
|
||||||
onClick={(e) => {
|
onOpenChange={setPopOverVisible}
|
||||||
getJobDetails();
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
if (event.job?.id) {
|
||||||
}}
|
e.stopPropagation();
|
||||||
getPopupContainer={(trigger) => trigger.parentNode}
|
getJobDetails();
|
||||||
trigger="click"
|
}
|
||||||
>
|
}}
|
||||||
<Button disabled={event.arrived}>{t("jobs.actions.intake_quick")}</Button>
|
getPopupContainer={(trigger) => trigger.parentNode}
|
||||||
</Popover>
|
trigger="click"
|
||||||
)}
|
>
|
||||||
|
<Button disabled={event.arrived}>{t("jobs.actions.intake_quick")}</Button>
|
||||||
|
</Popover>
|
||||||
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Card, Form, Input, Switch } from "antd";
|
import { Button, Card, Form, Input, Switch } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
@@ -9,7 +9,6 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { logImEXEvent } from "../../../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../../../firebase/firebase.utils";
|
||||||
import { MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED } from "../../../../graphql/appointments.queries";
|
import { MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED } from "../../../../graphql/appointments.queries";
|
||||||
import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
|
import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
|
||||||
import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
|
|
||||||
import { insertAuditTrail } from "../../../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../../../redux/application/application.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
|
||||||
@@ -32,7 +31,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED);
|
const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED);
|
||||||
const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED);
|
const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED);
|
||||||
const [updateOwner] = useMutation(UPDATE_OWNER);
|
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const { jobId } = useParams();
|
const { jobId } = useParams();
|
||||||
@@ -129,24 +127,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "intake" && job.owner && job.owner.id) {
|
|
||||||
//Updae Owner Allow to Text
|
|
||||||
const updateOwnerResult = await updateOwner({
|
|
||||||
variables: {
|
|
||||||
ownerId: job.owner.id,
|
|
||||||
owner: { allow_text_message: values.allow_text_message }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!!updateOwnerResult.errors) {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("checklist.errors.complete", {
|
|
||||||
error: JSON.stringify(result.errors)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (!!!result.errors) {
|
if (!!!result.errors) {
|
||||||
@@ -189,7 +169,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
|||||||
initialValues={{
|
initialValues={{
|
||||||
...(type === "intake" && {
|
...(type === "intake" && {
|
||||||
addToProduction: true,
|
addToProduction: true,
|
||||||
allow_text_message: job.owner && job.owner.allow_text_message,
|
|
||||||
scheduled_completion:
|
scheduled_completion:
|
||||||
(job && job.scheduled_completion && dayjs(job.scheduled_completion)) ||
|
(job && job.scheduled_completion && dayjs(job.scheduled_completion)) ||
|
||||||
(job &&
|
(job &&
|
||||||
@@ -228,14 +207,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
|||||||
>
|
>
|
||||||
<Switch disabled={readOnly} />
|
<Switch disabled={readOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
|
||||||
name="allow_text_message"
|
|
||||||
valuePropName="checked"
|
|
||||||
label={t("checklist.labels.allow_text_message")}
|
|
||||||
disabled={readOnly}
|
|
||||||
>
|
|
||||||
<Switch disabled={readOnly} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="scheduled_completion"
|
name="scheduled_completion"
|
||||||
label={t("jobs.fields.scheduled_completion")}
|
label={t("jobs.fields.scheduled_completion")}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function JobCostingModalContainer({ jobCostingModal, toggleModalVisible }
|
|||||||
}}
|
}}
|
||||||
cancelButtonProps={{ style: { display: "none" } }}
|
cancelButtonProps={{ style: { display: "none" } }}
|
||||||
width="90%"
|
width="90%"
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
{!costingData ? (
|
{!costingData ? (
|
||||||
<LoadingSpinner loading={true} />
|
<LoadingSpinner loading={true} />
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
setPrintCenterContext: (context) =>
|
||||||
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "printCenter"
|
||||||
|
})
|
||||||
|
),
|
||||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
dispatch(
|
dispatch(
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
@@ -87,7 +93,7 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTra
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{data ? (
|
{data ? (
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function JobEmployeeAssignments({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyTooltipOnHide content={popContent} open={visibility}>
|
<Popover destroyOnHidden content={popContent} open={visibility}>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<DataLabel label={t("jobs.fields.employee_body")}>
|
<DataLabel label={t("jobs.fields.employee_body")}>
|
||||||
{body ? (
|
{body ? (
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function JobReconciliationModalContainer({ reconciliationModal, toggleModalVisib
|
|||||||
onOk={handleCancel}
|
onOk={handleCancel}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
cancelButtonProps={{ display: "none" }}
|
cancelButtonProps={{ display: "none" }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
className="imex-reconciliation-modal"
|
className="imex-reconciliation-modal"
|
||||||
>
|
>
|
||||||
{loading && <LoadingSpinner loading={loading} />}
|
{loading && <LoadingSpinner loading={loading} />}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export default function JobWatcherToggleComponent({
|
|||||||
handleToggleSelf,
|
handleToggleSelf,
|
||||||
handleRemoveWatcher,
|
handleRemoveWatcher,
|
||||||
handleWatcherSelect,
|
handleWatcherSelect,
|
||||||
handleTeamSelect
|
handleTeamSelect,
|
||||||
|
isEmployee
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -66,22 +67,32 @@ export default function JobWatcherToggleComponent({
|
|||||||
<List>
|
<List>
|
||||||
<List.Item
|
<List.Item
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Tooltip title={!isEmployee ? t("notifications.tooltips.not-employee") : ""} placement="top">
|
||||||
type={isWatching ? "primary" : "default"}
|
<span>
|
||||||
danger={!isWatching}
|
<Button
|
||||||
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
|
type={isWatching ? "primary" : "default"}
|
||||||
size="medium"
|
danger={!isWatching}
|
||||||
onClick={handleToggleSelf}
|
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
|
||||||
loading={adding || removing}
|
size="medium"
|
||||||
>
|
onClick={handleToggleSelf}
|
||||||
{isWatching ? t("notifications.labels.unwatch") : t("notifications.labels.watch")}
|
loading={adding || removing}
|
||||||
</Button>
|
disabled={!isEmployee || adding || removing}
|
||||||
|
>
|
||||||
|
{isWatching ? t("notifications.labels.unwatch") : t("notifications.labels.watch")}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<List.Item.Meta>
|
<List.Item.Meta>
|
||||||
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
|
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
|
||||||
{t("notifications.labels.watching-issue")}
|
{t("notifications.labels.watching-issue")}
|
||||||
</Text>
|
</Text>
|
||||||
|
{!isEmployee && (
|
||||||
|
<Text type="danger" style={{ marginBottom: 8, display: "block" }}>
|
||||||
|
{t("notifications.tooltips.not-employee")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</List.Item.Meta>
|
</List.Item.Meta>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
</List>
|
</List>
|
||||||
@@ -98,12 +109,16 @@ export default function JobWatcherToggleComponent({
|
|||||||
<EmployeeSearchSelectComponent
|
<EmployeeSearchSelectComponent
|
||||||
style={{ minWidth: "100%" }}
|
style={{ minWidth: "100%" }}
|
||||||
options={
|
options={
|
||||||
bodyshop?.employees?.filter((e) =>
|
bodyshop?.employees?.filter(
|
||||||
jobWatchers.every((w) => w.user_email !== e.user_email && e.active && e.user_email)
|
(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")}
|
placeholder={t("notifications.labels.employee-search")}
|
||||||
value={selectedWatcher}
|
value={selectedWatcher}
|
||||||
|
showEmail={true}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSelectedWatcher(value);
|
setSelectedWatcher(value);
|
||||||
handleWatcherSelect(value);
|
handleWatcherSelect(value);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -21,13 +22,14 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
splitKey: bodyshop && bodyshop.imexshopid
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
const userEmail = currentUser.email;
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
const jobid = job.id;
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [selectedWatcher, setSelectedWatcher] = useState(null);
|
const [selectedWatcher, setSelectedWatcher] = useState(null);
|
||||||
const [selectedTeam, setSelectedTeam] = useState(null);
|
const [selectedTeam, setSelectedTeam] = useState(null);
|
||||||
|
|
||||||
|
const userEmail = currentUser.email;
|
||||||
|
const jobid = job.id;
|
||||||
|
|
||||||
// Fetch current watchers with refetch capability
|
// Fetch current watchers with refetch capability
|
||||||
const {
|
const {
|
||||||
data: watcherData,
|
data: watcherData,
|
||||||
@@ -139,13 +141,13 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleToggleSelf = useCallback(async () => {
|
const handleToggleSelf = useCallback(async () => {
|
||||||
if (adding || removing) return;
|
if (adding || removing || !isEmployee) return;
|
||||||
if (isWatching) {
|
if (isWatching) {
|
||||||
await removeWatcher({ variables: { jobid, userEmail } });
|
await removeWatcher({ variables: { jobid, userEmail } });
|
||||||
} else {
|
} else {
|
||||||
await addWatcher({ variables: { jobid, userEmail } });
|
await addWatcher({ variables: { jobid, userEmail } });
|
||||||
}
|
}
|
||||||
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing]);
|
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing, isEmployee]);
|
||||||
|
|
||||||
const handleRemoveWatcher = useCallback(
|
const handleRemoveWatcher = useCallback(
|
||||||
async (email) => {
|
async (email) => {
|
||||||
@@ -187,7 +189,16 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
setSelectedTeam(null);
|
setSelectedTeam(null);
|
||||||
return;
|
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]
|
[jobWatchers, addWatcher, jobid, adding]
|
||||||
);
|
);
|
||||||
@@ -212,6 +223,7 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
handleWatcherSelect={handleWatcherSelect}
|
handleWatcherSelect={handleWatcherSelect}
|
||||||
handleTeamSelect={handleTeamSelect}
|
handleTeamSelect={handleTeamSelect}
|
||||||
currentUser={currentUser}
|
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">
|
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
|
||||||
<DateTimePicker />
|
<DateTimePicker />
|
||||||
</Form.Item>
|
</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">
|
<Form.Item label={t("jobs.fields.date_scheduled")} name="date_scheduled">
|
||||||
<DateTimePicker />
|
<DateTimePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { Col, Row } from "antd";
|
|||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import {
|
import {
|
||||||
DELETE_AVAILABLE_JOB,
|
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 { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
|
||||||
import HeaderFields from "./jobs-available-supplement.headerfields";
|
import HeaderFields from "./jobs-available-supplement.headerfields";
|
||||||
import JobsAvailableTableComponent from "./jobs-available-table.component";
|
import JobsAvailableTableComponent from "./jobs-available-table.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -195,7 +195,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
|
|
||||||
await deleteJob({
|
await deleteJob({
|
||||||
variables: { id: estData.id }
|
variables: { id: estData.id }
|
||||||
}).then((r) => {
|
}).then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
setInsertLoading(false);
|
setInsertLoading(false);
|
||||||
});
|
});
|
||||||
@@ -315,7 +315,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
|
|
||||||
deleteJob({
|
deleteJob({
|
||||||
variables: { id: estData.id }
|
variables: { id: estData.id }
|
||||||
}).then((r) => {
|
}).then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
setInsertLoading(false);
|
setInsertLoading(false);
|
||||||
});
|
});
|
||||||
@@ -372,7 +372,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
loadEstData({ variables: { id: record.id } });
|
loadEstData({ variables: { id: record.id } });
|
||||||
modalSearchState[1](record.clm_no);
|
modalSearchState[1](record.clm_no);
|
||||||
setJobModalVisible(true);
|
setJobModalVisible(true);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -456,7 +456,7 @@ function replaceEmpty(someObj, replaceValue = null) {
|
|||||||
return JSON.parse(temp);
|
return JSON.parse(temp);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function CheckTaxRatesUSA(estData, bodyshop) {
|
async function CheckTaxRatesUSA(estData) {
|
||||||
if (!estData.parts_tax_rates?.PAM) {
|
if (!estData.parts_tax_rates?.PAM) {
|
||||||
estData.parts_tax_rates.PAM = estData.parts_tax_rates.PAC;
|
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.
|
//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.
|
//This needs to be done before cleansing unq_seq since some misc prices could move over.
|
||||||
estData.joblines.data.forEach((line) => {
|
estData.joblines.data.forEach((line) => {
|
||||||
@@ -585,6 +585,9 @@ function ResolveCCCLineIssues(estData, bodyshop) {
|
|||||||
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
|
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
|
||||||
line.mod_lbr_ty = "LAR";
|
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 React, { useContext } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||||
@@ -129,13 +129,6 @@ export default function JobsCreateOwnerInfoNewComponent() {
|
|||||||
<Form.Item label={t("owners.fields.preferred_contact")} name={["owner", "data", "preferred_contact"]}>
|
<Form.Item label={t("owners.fields.preferred_contact")} name={["owner", "data", "preferred_contact"]}>
|
||||||
<Input disabled={!state.owner.new} />
|
<Input disabled={!state.owner.new} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
|
||||||
label={t("owners.fields.allow_text_message")}
|
|
||||||
valuePropName="checked"
|
|
||||||
name={["owner", "data", "allow_text_message"]}
|
|
||||||
>
|
|
||||||
<Switch disabled={!state.owner.new} />
|
|
||||||
</Form.Item>
|
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
|
|||||||
open={open}
|
open={open}
|
||||||
placement="left"
|
placement="left"
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
destroyTooltipOnHide
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<SearchOutlined style={{ cursor: "pointer" }} />
|
<SearchOutlined style={{ cursor: "pointer" }} />
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import FormRow from "../layout-form-row/layout-form-row.component";
|
import FormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -40,6 +41,20 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
<Form.Item label={t("jobs.fields.date_rentalresp")} name="date_rentalresp">
|
<Form.Item label={t("jobs.fields.date_rentalresp")} name="date_rentalresp">
|
||||||
<DateTimePicker disabled={jobRO} />
|
<DateTimePicker disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
|
||||||
|
<DateTimePicker
|
||||||
|
disabled={true}
|
||||||
|
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={true}
|
||||||
|
value={job.estimate_approved ? dayjs(job.estimate_approved) : null}
|
||||||
|
placeholder={t("general.labels.na")}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
|
|
||||||
<FormRow header={t("jobs.forms.scheddates")}>
|
<FormRow header={t("jobs.forms.scheddates")}>
|
||||||
@@ -76,21 +91,15 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
<DateTimePicker disabled={jobRO} />
|
<DateTimePicker disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item shouldUpdate>
|
<Form.Item shouldUpdate>
|
||||||
{() => {
|
{() => (
|
||||||
return (
|
<Form.Item
|
||||||
<Form.Item
|
label={t("jobs.fields.actual_completion")}
|
||||||
label={t("jobs.fields.actual_completion")}
|
name="actual_completion"
|
||||||
name="actual_completion"
|
rules={[{ required: jobInPostProduction }]}
|
||||||
rules={[
|
>
|
||||||
{
|
<DateTimePicker disabled={jobRO} />
|
||||||
required: jobInPostProduction
|
</Form.Item>
|
||||||
}
|
)}
|
||||||
]}
|
|
||||||
>
|
|
||||||
<DateTimePicker disabled={jobRO} />
|
|
||||||
</Form.Item>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.scheduled_delivery")} name="scheduled_delivery">
|
<Form.Item label={t("jobs.fields.scheduled_delivery")} name="scheduled_delivery">
|
||||||
<DateTimePicker disabled={jobRO} />
|
<DateTimePicker disabled={jobRO} />
|
||||||
@@ -103,15 +112,12 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
|
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true || jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true || jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
|
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true || jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale">
|
<Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true || jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
import { Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -188,6 +187,12 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
<Form.Item label={t("jobs.fields.tlos_ind")} name="tlos_ind" valuePropName="checked">
|
<Form.Item label={t("jobs.fields.tlos_ind")} name="tlos_ind" valuePropName="checked">
|
||||||
<Switch disabled={jobRO} />
|
<Switch disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.hit_and_run")} name="hit_and_run" valuePropName="checked">
|
||||||
|
<Switch disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.acv_amount")} name="acv_amount">
|
||||||
|
<CurrencyInput disabled={jobRO} min={0} />
|
||||||
|
</Form.Item>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...lossColDamage}>
|
<Col {...lossColDamage}>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
||||||
@@ -32,7 +33,6 @@ import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
|||||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -1078,17 +1078,26 @@ export function JobsDetailHeaderActions({
|
|||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "deletejob",
|
key: "deletejob",
|
||||||
id: "job-actions-deletejob",
|
id: "job-actions-deletejob",
|
||||||
label: (
|
label:
|
||||||
<Popconfirm
|
job.job_watchers.length === 0 ? (
|
||||||
title={t("jobs.labels.deleteconfirm")}
|
<Popconfirm
|
||||||
okText={t("general.labels.yes")}
|
title={t("jobs.labels.deleteconfirm")}
|
||||||
cancelText={t("general.labels.no")}
|
okText={t("general.labels.yes")}
|
||||||
onClick={(e) => e.stopPropagation()}
|
cancelText={t("general.labels.no")}
|
||||||
onConfirm={handleDeleteJob}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
onConfirm={handleDeleteJob}
|
||||||
{t("menus.jobsactions.deletejob")}
|
>
|
||||||
</Popconfirm>
|
{t("menus.jobsactions.deletejob")}
|
||||||
)
|
</Popconfirm>
|
||||||
|
) : (
|
||||||
|
<Popconfirm
|
||||||
|
title={t("jobs.labels.deletewatchers")}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
showCancel={false}
|
||||||
|
>
|
||||||
|
{t("menus.jobsactions.deletejob")}
|
||||||
|
</Popconfirm>
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1109,8 +1118,8 @@ export function JobsDetailHeaderActions({
|
|||||||
<RbacWrapper action="jobs:void" noauth>
|
<RbacWrapper action="jobs:void" noauth>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t("jobs.labels.voidjob")}
|
title={t("jobs.labels.voidjob")}
|
||||||
okText="Yes"
|
okText={t("general.labels.yes")}
|
||||||
cancelText="No"
|
cancelText={t("general.labels.no")}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onConfirm={handleVoidJob}
|
onConfirm={handleVoidJob}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -167,7 +167,18 @@ export function JobsDetailHeaderActionsToggleProduction({
|
|||||||
<FormDateTimePickerComponent disabled={jobRO} />
|
<FormDateTimePickerComponent disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name={["actual_delivery"]} label={t("jobs.fields.actual_delivery")}>
|
<Form.Item
|
||||||
|
name={["actual_delivery"]}
|
||||||
|
label={t("jobs.fields.actual_delivery")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: bodyshop.deliverchecklist.actual_delivery
|
||||||
|
? bodyshop.deliverchecklist.actual_delivery
|
||||||
|
: false
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
<FormDateTimePickerComponent disabled={jobRO} />
|
<FormDateTimePickerComponent disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled } from "@ant-design/icons";
|
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled } from "@ant-design/icons";
|
||||||
import { Card, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
|
import { useMutation } from "@apollo/client";
|
||||||
import React, { useState } from "react";
|
import { Card, Checkbox, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
|
||||||
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter, DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||||
import DataLabel from "../data-label/data-label.component";
|
import DataLabel from "../data-label/data-label.component";
|
||||||
@@ -21,7 +27,6 @@ import ProductionListColumnComment from "../production-list-columns/production-l
|
|||||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||||
import "./jobs-detail-header.styles.scss";
|
import "./jobs-detail-header.styles.scss";
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -29,41 +34,73 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
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 = {
|
const colSpan = {
|
||||||
xs: {
|
xs: { span: 24 },
|
||||||
span: 24
|
sm: { span: 24 },
|
||||||
},
|
md: { span: 12 },
|
||||||
sm: {
|
lg: { span: 6 },
|
||||||
span: 24
|
xl: { span: 6 }
|
||||||
},
|
|
||||||
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 { t } = useTranslation();
|
||||||
|
const { notification } = useNotification();
|
||||||
const [notesClamped, setNotesClamped] = useState(true);
|
const [notesClamped, setNotesClamped] = useState(true);
|
||||||
const vehicleTitle = `${job.v_model_yr || ""} ${job.v_color || ""}
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
${job.v_make_desc || ""}
|
const vehicleTitle =
|
||||||
${job.v_model_desc || ""}`.trim();
|
`${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 bodyHrs = job.joblines.filter((j) => j.mod_lbr_ty !== "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
||||||
const refinishHrs = job.joblines
|
const refinishHrs = job.joblines
|
||||||
.filter((line) => line.mod_lbr_ty === "LAR")
|
.filter((line) => line.mod_lbr_ty === "LAR")
|
||||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
||||||
|
|
||||||
const ownerTitle = OwnerNameDisplayFunction(job).trim();
|
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 (
|
return (
|
||||||
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
|
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
|
||||||
<Col {...colSpan}>
|
<Col {...colSpan}>
|
||||||
@@ -72,11 +109,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
<DataLabel label={t("jobs.fields.status")}>
|
<DataLabel label={t("jobs.fields.status")}>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{job.status}
|
{job.status}
|
||||||
{job.inproduction && (
|
{job.inproduction && <Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>}
|
||||||
<Tag color="#f50" key="production">
|
|
||||||
{t("jobs.labels.inproduction")}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||||
{job.iouparent && (
|
{job.iouparent && (
|
||||||
<Link to={`/manage/jobs/${job.iouparent}`}>
|
<Link to={`/manage/jobs/${job.iouparent}`}>
|
||||||
@@ -110,7 +143,6 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
<span style={{ margin: "0rem .5rem" }}>/</span>
|
<span style={{ margin: "0rem .5rem" }}>/</span>
|
||||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
|
|
||||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||||
{job.alt_transport}
|
{job.alt_transport}
|
||||||
<JobAltTransportChange job={job} />
|
<JobAltTransportChange job={job} />
|
||||||
@@ -127,11 +159,39 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
))}
|
))}
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
||||||
<ProductionListColumnProductionNote record={job} />
|
<ProductionListColumnProductionNote record={job} />
|
||||||
</DataLabel>
|
</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>
|
<Space wrap>
|
||||||
{job.special_coverage_policy && (
|
{job.special_coverage_policy && (
|
||||||
<Tag color="tomato">
|
<Tag color="tomato">
|
||||||
@@ -149,6 +209,14 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
</Space>
|
</Space>
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
|
{job.hit_and_run && (
|
||||||
|
<Tag color="green">
|
||||||
|
<Space>
|
||||||
|
<WarningFilled />
|
||||||
|
<span>{t("jobs.fields.hit_and_run")}</span>
|
||||||
|
</Space>
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function JobsDocumentsContainer({
|
|||||||
variables: { jobId: jobId },
|
variables: { jobId: jobId },
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
skip: Imgproxy.treatment === "on" || !!billId
|
skip: !!billId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default connect(
|
|||||||
<Modal
|
<Modal
|
||||||
title={t("jobs.labels.existing_jobs")}
|
title={t("jobs.labels.existing_jobs")}
|
||||||
width={"80%"}
|
width={"80%"}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
okButtonProps={{ disabled: selectedJob ? false : true }}
|
okButtonProps={{ disabled: selectedJob ? false : true }}
|
||||||
{...modalProps}
|
{...modalProps}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,7 +20,14 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
|
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 }) {
|
export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleModalVisible, insertAuditTrail }) {
|
||||||
@@ -123,7 +130,7 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||||
<NoteUpsertModalComponent form={form} />
|
<NoteUpsertModalComponent form={form} />
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Virtuoso } from "react-virtuoso";
|
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 { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import "./notification-center.styles.scss";
|
import "./notification-center.styles.scss";
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
import { forwardRef, useRef, useEffect } from "react";
|
import { forwardRef, useEffect, useRef } from "react";
|
||||||
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
@@ -26,7 +26,8 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
markAllRead,
|
markAllRead,
|
||||||
loadMore,
|
loadMore,
|
||||||
onNotificationClick,
|
onNotificationClick,
|
||||||
unreadCount
|
unreadCount,
|
||||||
|
isEmployee
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
@@ -93,7 +94,12 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
) : (
|
) : (
|
||||||
<EyeOutlined className="notification-toggle-icon" />
|
<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>
|
</Space>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
||||||
@@ -106,14 +112,20 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Virtuoso
|
{!isEmployee ? (
|
||||||
ref={virtuosoRef}
|
<div style={{ padding: 10 }}>
|
||||||
style={{ height: "400px", width: "100%" }}
|
<Alert message={t("notifications.labels.employee-notification")} type="warning" />
|
||||||
data={notifications}
|
</div>
|
||||||
totalCount={notifications.length}
|
) : (
|
||||||
endReached={loadMore}
|
<Virtuoso
|
||||||
itemContent={renderNotification}
|
ref={virtuosoRef}
|
||||||
/>
|
style={{ height: "400px", width: "100%" }}
|
||||||
|
data={notifications}
|
||||||
|
totalCount={notifications.length}
|
||||||
|
endReached={loadMore}
|
||||||
|
itemContent={renderNotification}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { connect } from "react-redux";
|
|||||||
import NotificationCenterComponent from "./notification-center.component";
|
import NotificationCenterComponent from "./notification-center.component";
|
||||||
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
||||||
import { createStructuredSelector } from "reselect";
|
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 day from "../../utils/day.js";
|
||||||
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.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
|
// This will be used to poll for notifications when the socket is disconnected
|
||||||
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
||||||
@@ -17,17 +18,18 @@ const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
|||||||
* @param onClose
|
* @param onClose
|
||||||
* @param bodyshop
|
* @param bodyshop
|
||||||
* @param unreadCount
|
* @param unreadCount
|
||||||
|
* @param currentUser
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
|
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount, currentUser }) => {
|
||||||
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
||||||
const notificationRef = useRef(null);
|
const notificationRef = useRef(null);
|
||||||
|
|
||||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||||
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
|
||||||
const baseWhereClause = useMemo(() => {
|
const baseWhereClause = useMemo(() => {
|
||||||
return { associationid: { _eq: userAssociationId } };
|
return { associationid: { _eq: userAssociationId } };
|
||||||
@@ -51,7 +53,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
fetchPolicy: "cache-and-network",
|
fetchPolicy: "cache-and-network",
|
||||||
notifyOnNetworkStatusChange: true,
|
notifyOnNetworkStatusChange: true,
|
||||||
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
||||||
skip: !userAssociationId,
|
skip: !userAssociationId || !isEmployee,
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error(`Error polling Notifications: ${err?.message || ""}`);
|
console.error(`Error polling Notifications: ${err?.message || ""}`);
|
||||||
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
||||||
@@ -71,7 +73,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
}, [visible, onClose]);
|
}, [visible, onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.notifications) {
|
if (data?.notifications && isEmployee) {
|
||||||
const processedNotifications = data.notifications
|
const processedNotifications = data.notifications
|
||||||
.map((notif) => {
|
.map((notif) => {
|
||||||
let scenarioText;
|
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));
|
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||||
setNotifications(processedNotifications);
|
setNotifications(processedNotifications);
|
||||||
|
} else if (!isEmployee) {
|
||||||
|
setNotifications([]); // Clear notifications if not an employee
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data, isEmployee]);
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
if (!queryLoading && data?.notifications.length) {
|
if (!queryLoading && data?.notifications.length && isEmployee) {
|
||||||
setIsLoading(true); // Show spinner during fetchMore
|
setIsLoading(true); // Show spinner during fetchMore
|
||||||
fetchMore({
|
fetchMore({
|
||||||
variables: { offset: data.notifications.length, where: whereClause },
|
variables: { offset: data.notifications.length, where: whereClause },
|
||||||
@@ -121,13 +125,14 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false)); // Hide spinner when done
|
.finally(() => setIsLoading(false)); // Hide spinner when done
|
||||||
}
|
}
|
||||||
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause]);
|
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause, isEmployee]);
|
||||||
|
|
||||||
const handleToggleUnreadOnly = (value) => {
|
const handleToggleUnreadOnly = (value) => {
|
||||||
setShowUnreadOnly(value);
|
setShowUnreadOnly(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkAllRead = useCallback(() => {
|
const handleMarkAllRead = useCallback(() => {
|
||||||
|
if (!isEmployee) return; // Do nothing if not an employee
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
markAllNotificationsRead()
|
markAllNotificationsRead()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -147,7 +152,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
})
|
})
|
||||||
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
|
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly]);
|
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly, isEmployee]);
|
||||||
|
|
||||||
const handleNotificationClick = useCallback(
|
const handleNotificationClick = useCallback(
|
||||||
(notificationId) => {
|
(notificationId) => {
|
||||||
@@ -170,17 +175,18 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && !isConnected) {
|
if (visible && !isConnected && isEmployee) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
refetch()
|
refetch()
|
||||||
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
|
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}
|
}
|
||||||
}, [visible, isConnected, refetch]);
|
}, [visible, isConnected, refetch, isEmployee]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationCenterComponent
|
<NotificationCenterComponent
|
||||||
ref={notificationRef}
|
ref={notificationRef}
|
||||||
|
isEmployee={isEmployee}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
notifications={notifications}
|
notifications={notifications}
|
||||||
@@ -196,7 +202,8 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(NotificationCenterContainer);
|
export default connect(mapStateToProps, null)(NotificationCenterContainer);
|
||||||
|
|||||||
@@ -1,32 +1,41 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button, Card, Checkbox, Form, Space, Table } from "antd";
|
import { Alert, Button, Card, Checkbox, Divider, Form, Space, Switch, Table, Typography } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import { QUERY_NOTIFICATION_SETTINGS, UPDATE_NOTIFICATION_SETTINGS } from "../../graphql/user.queries.js";
|
import {
|
||||||
|
QUERY_NOTIFICATION_SETTINGS,
|
||||||
|
UPDATE_NOTIFICATION_SETTINGS,
|
||||||
|
UPDATE_NOTIFICATIONS_AUTOADD
|
||||||
|
} from "../../graphql/user.queries.js";
|
||||||
import { notificationScenarios } from "../../utils/jobNotificationScenarios.js";
|
import { notificationScenarios } from "../../utils/jobNotificationScenarios.js";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
|
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifications Settings Form
|
* Notifications Settings Form
|
||||||
* @param currentUser
|
* @param currentUser
|
||||||
|
* @param bodyshop
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const NotificationSettingsForm = ({ currentUser }) => {
|
const NotificationSettingsForm = ({ currentUser, bodyshop }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [initialValues, setInitialValues] = useState({});
|
const [initialValues, setInitialValues] = useState({});
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
|
const [autoAddEnabled, setAutoAddEnabled] = useState(false);
|
||||||
|
const [initialAutoAdd, setInitialAutoAdd] = useState(false);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
|
||||||
// Fetch notification settings.
|
// Fetch notification settings and notifications_autoadd
|
||||||
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
@@ -34,13 +43,16 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
skip: !currentUser
|
skip: !currentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateNotificationSettings, { loading: saving }] = useMutation(UPDATE_NOTIFICATION_SETTINGS);
|
const [updateNotificationSettings, { loading: savingSettings }] = useMutation(UPDATE_NOTIFICATION_SETTINGS);
|
||||||
|
const [updateNotificationsAutoAdd, { loading: savingAutoAdd }] = useMutation(UPDATE_NOTIFICATIONS_AUTOADD);
|
||||||
|
|
||||||
// Populate form with fetched data.
|
// Populate form with fetched data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.associations?.length > 0) {
|
if (data?.associations?.length > 0) {
|
||||||
const settings = data.associations[0].notification_settings || {};
|
const settings = data.associations[0].notification_settings || {};
|
||||||
// Ensure each scenario has an object with { app, email, fcm }.
|
const autoAdd = data.associations[0].notifications_autoadd ?? false;
|
||||||
|
|
||||||
|
// Ensure each scenario has an object with { app, email, fcm }
|
||||||
const formattedValues = notificationScenarios.reduce((acc, scenario) => {
|
const formattedValues = notificationScenarios.reduce((acc, scenario) => {
|
||||||
acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false };
|
acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false };
|
||||||
return acc;
|
return acc;
|
||||||
@@ -48,32 +60,66 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
|
|
||||||
setInitialValues(formattedValues);
|
setInitialValues(formattedValues);
|
||||||
form.setFieldsValue(formattedValues);
|
form.setFieldsValue(formattedValues);
|
||||||
setIsDirty(false); // Reset dirty state when new data loads.
|
setAutoAddEnabled(autoAdd);
|
||||||
|
setInitialAutoAdd(autoAdd);
|
||||||
|
setIsDirty(false); // Reset dirty state when new data loads
|
||||||
}
|
}
|
||||||
}, [data, form]);
|
}, [data, form]);
|
||||||
|
|
||||||
|
// Handle toggle of notifications_autoadd
|
||||||
|
const handleAutoAddToggle = async (checked) => {
|
||||||
|
if (data?.associations?.length > 0) {
|
||||||
|
const userId = data.associations[0].id;
|
||||||
|
try {
|
||||||
|
const result = await updateNotificationsAutoAdd({
|
||||||
|
variables: { id: userId, autoadd: checked }
|
||||||
|
});
|
||||||
|
if (!result?.errors) {
|
||||||
|
setAutoAddEnabled(checked);
|
||||||
|
setInitialAutoAdd(checked);
|
||||||
|
notification.success({ message: t("notifications.labels.auto-add-success") });
|
||||||
|
setIsDirty(false); // Reset dirty state if only auto-add was changed
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to update auto-add setting");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setAutoAddEnabled(!checked); // Revert on error
|
||||||
|
notification.error({ message: t("notifications.labels.auto-add-failure") });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle save of notification settings
|
||||||
const handleSave = async (values) => {
|
const handleSave = async (values) => {
|
||||||
if (data?.associations?.length > 0) {
|
if (data?.associations?.length > 0) {
|
||||||
const userId = data.associations[0].id;
|
const userId = data.associations[0].id;
|
||||||
// Save the updated notification settings.
|
try {
|
||||||
const result = await updateNotificationSettings({ variables: { id: userId, ns: values } });
|
const result = await updateNotificationSettings({ variables: { id: userId, ns: values } });
|
||||||
if (!result?.errors) {
|
if (!result?.errors) {
|
||||||
notification.success({ message: t("notifications.labels.notification-settings-success") });
|
notification.success({ message: t("notifications.labels.notification-settings-success") });
|
||||||
setInitialValues(values);
|
setInitialValues(values);
|
||||||
setIsDirty(false);
|
setIsDirty(false);
|
||||||
} else {
|
} else {
|
||||||
|
throw new Error("Failed to update notification settings");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
notification.error({ message: t("notifications.labels.notification-settings-failure") });
|
notification.error({ message: t("notifications.labels.notification-settings-failure") });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mark the form as dirty on any manual change.
|
// Mark the form as dirty on any manual change
|
||||||
const handleFormChange = () => {
|
const handleFormChange = () => {
|
||||||
setIsDirty(true);
|
setIsDirty(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if auto-add has changed
|
||||||
|
const isAutoAddDirty = autoAddEnabled !== initialAutoAdd;
|
||||||
|
|
||||||
|
// Handle reset of form and auto-add
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
form.setFieldsValue(initialValues);
|
form.setFieldsValue(initialValues);
|
||||||
|
setAutoAddEnabled(initialAutoAdd);
|
||||||
setIsDirty(false);
|
setIsDirty(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,17 +185,30 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
title={t("notifications.labels.notificationscenarios")}
|
title={t("notifications.labels.notificationscenarios")}
|
||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="default" onClick={handleReset} disabled={!isDirty}>
|
<Typography.Text type="secondary">{t("notifications.labels.auto-add")}</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
checked={autoAddEnabled}
|
||||||
|
onChange={handleAutoAddToggle}
|
||||||
|
loading={savingAutoAdd}
|
||||||
|
// checkedChildren={t("notifications.labels.auto-add-on")}
|
||||||
|
// unCheckedChildren={t("notifications.labels.auto-add-off")}
|
||||||
|
/>
|
||||||
|
<Button type="default" onClick={handleReset} disabled={!isDirty && !isAutoAddDirty}>
|
||||||
{t("general.actions.clear")}
|
{t("general.actions.clear")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="primary" htmlType="submit" disabled={!isDirty} loading={savingSettings}>
|
||||||
<Button type="primary" htmlType="submit" disabled={!isDirty} loading={saving}>
|
|
||||||
{t("notifications.labels.save")}
|
{t("notifications.labels.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</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" />
|
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
|
||||||
|
<Divider />
|
||||||
</Card>
|
</Card>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
@@ -158,11 +217,13 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
NotificationSettingsForm.propTypes = {
|
NotificationSettingsForm.propTypes = {
|
||||||
currentUser: PropTypes.shape({
|
currentUser: PropTypes.shape({
|
||||||
email: PropTypes.string.isRequired
|
email: PropTypes.string.isRequired
|
||||||
}).isRequired
|
}).isRequired,
|
||||||
|
bodyshop: PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(NotificationSettingsForm);
|
export default connect(mapStateToProps)(NotificationSettingsForm);
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Form, Input, Switch } from "antd";
|
import { Form, Input, Tooltip } from "antd";
|
||||||
import React from "react";
|
import { CloseCircleFilled } from "@ant-design/icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
||||||
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
import { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||||
|
|
||||||
export default function OwnerDetailFormComponent({ form, loading }) {
|
export default function OwnerDetailFormComponent({ form, loading, isPhone1OptedOut, isPhone2OptedOut }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getFieldValue } = form;
|
const { getFieldValue } = form;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormFieldsChanged form={form} />
|
<FormFieldsChanged form={form} />
|
||||||
@@ -26,7 +27,7 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("owners.fields.accountingid")} name="accountingid">
|
<Form.Item label={t("owners.fields.accountingid")} name="accountingid">
|
||||||
<Input disabled/>
|
<Input disabled />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("owners.forms.address")}>
|
<LayoutFormRow header={t("owners.forms.address")}>
|
||||||
@@ -50,9 +51,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("owners.forms.contact")}>
|
<LayoutFormRow header={t("owners.forms.contact")}>
|
||||||
<Form.Item label={t("owners.fields.allow_text_message")} name="allow_text_message" valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("owners.fields.ownr_ea")}
|
label={t("owners.fields.ownr_ea")}
|
||||||
name="ownr_ea"
|
name="ownr_ea"
|
||||||
@@ -65,19 +63,55 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
>
|
>
|
||||||
<FormItemEmail email={getFieldValue("ownr_ea")} />
|
<FormItemEmail email={getFieldValue("ownr_ea")} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("owners.fields.ownr_ph1")} style={{ marginBottom: 0 }}>
|
||||||
label={t("owners.fields.ownr_ph1")}
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
name="ownr_ph1"
|
<Form.Item
|
||||||
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]}
|
name="ownr_ph1"
|
||||||
>
|
noStyle
|
||||||
<FormItemPhone />
|
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]}
|
||||||
|
>
|
||||||
|
<Input style={{ flex: 1, minWidth: "150px" }} />
|
||||||
|
</Form.Item>
|
||||||
|
{isPhone1OptedOut && (
|
||||||
|
<Tooltip title={t("consent.text_body")}>
|
||||||
|
<CloseCircleFilled
|
||||||
|
style={{
|
||||||
|
color: "#ff4d4f",
|
||||||
|
fontSize: 16,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: "100%"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("owners.fields.ownr_ph2")} style={{ marginBottom: 0 }}>
|
||||||
label={t("owners.fields.ownr_ph2")}
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
name="ownr_ph2"
|
<Form.Item
|
||||||
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]}
|
name="ownr_ph2"
|
||||||
>
|
noStyle
|
||||||
<FormItemPhone />
|
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]}
|
||||||
|
>
|
||||||
|
<Input style={{ flex: 1, minWidth: "150px" }} />
|
||||||
|
</Form.Item>
|
||||||
|
{isPhone2OptedOut && (
|
||||||
|
<Tooltip title={t("consent.text_body")}>
|
||||||
|
<CloseCircleFilled
|
||||||
|
style={{
|
||||||
|
color: "#ff4d4f",
|
||||||
|
fontSize: 16,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: "100%"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("owners.fields.preferred_contact")} name="preferred_contact">
|
<Form.Item label={t("owners.fields.preferred_contact")} name="preferred_contact">
|
||||||
<Input />
|
<Input />
|
||||||
|
|||||||
@@ -1,69 +1,115 @@
|
|||||||
import { Button, Form, Popconfirm } from "antd";
|
import { Button, Form, Popconfirm } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useApolloClient, useMutation } from "@apollo/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries";
|
import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors"; // Adjust path
|
||||||
|
import { phoneNumberOptOutService } from "../../utils/phoneOptOutService.js"; // Adjust path
|
||||||
import OwnerDetailFormComponent from "./owner-detail-form.component";
|
import OwnerDetailFormComponent from "./owner-detail-form.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { phone } from "phone"; // Import phone utility for formatting
|
||||||
|
|
||||||
function OwnerDetailFormContainer({ owner, refetch }) {
|
// Connect to Redux to access bodyshop
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
function OwnerDetailFormContainer({ owner, refetch, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const history = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [optedOutPhones, setOptedOutPhones] = useState(new Set());
|
||||||
const [updateOwner] = useMutation(UPDATE_OWNER);
|
const [updateOwner] = useMutation(UPDATE_OWNER);
|
||||||
const [deleteOwner] = useMutation(DELETE_OWNER);
|
const [deleteOwner] = useMutation(DELETE_OWNER);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
|
// Fetch opt-out status on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchOptOutStatus = async () => {
|
||||||
|
if (bodyshop?.id && bodyshop?.messagingservicesid && (owner?.ownr_ph1 || owner?.ownr_ph2)) {
|
||||||
|
const phoneNumbers = [owner.ownr_ph1, owner.ownr_ph2].filter(Boolean);
|
||||||
|
const optOutSet = await phoneNumberOptOutService(apolloClient, bodyshop.id, phoneNumbers);
|
||||||
|
setOptedOutPhones(optOutSet);
|
||||||
|
} else {
|
||||||
|
setOptedOutPhones(new Set());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchOptOutStatus();
|
||||||
|
}, [apolloClient, bodyshop?.id, bodyshop?.messagingservicesid, owner?.ownr_ph1, owner?.ownr_ph2]);
|
||||||
|
|
||||||
|
// Reset form fields when owner changes
|
||||||
|
useEffect(() => {
|
||||||
|
form.setFieldsValue({
|
||||||
|
ownr_ph1: owner?.ownr_ph1,
|
||||||
|
ownr_ph2: owner?.ownr_ph2,
|
||||||
|
...owner
|
||||||
|
});
|
||||||
|
}, [owner, form]);
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await deleteOwner({
|
try {
|
||||||
variables: { id: owner.id }
|
const result = await deleteOwner({
|
||||||
});
|
variables: { id: owner.id }
|
||||||
console.log(result);
|
});
|
||||||
if (result.errors) {
|
if (result.errors) {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
|
message: t("owners.errors.deleting", {
|
||||||
|
error: JSON.stringify(result.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification.success({
|
||||||
|
message: t("owners.successes.delete")
|
||||||
|
});
|
||||||
|
navigate(`/manage/owners`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
message: t("owners.errors.deleting", {
|
message: t("owners.errors.deleting", {
|
||||||
error: JSON.stringify(result.errors)
|
error: error.message
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else {
|
|
||||||
notification["success"]({
|
|
||||||
message: t("owners.successes.delete")
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
history(`/manage/owners`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await updateOwner({
|
try {
|
||||||
variables: { ownerId: owner.id, owner: values }
|
const result = await updateOwner({
|
||||||
});
|
variables: { ownerId: owner.id, owner: values }
|
||||||
|
});
|
||||||
if (!!result.errors) {
|
if (result.errors) {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
|
message: t("owners.errors.saving", {
|
||||||
|
error: JSON.stringify(result.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification.success({
|
||||||
|
message: t("owners.successes.save")
|
||||||
|
});
|
||||||
|
if (refetch) await refetch();
|
||||||
|
form.resetFields();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
message: t("owners.errors.saving", {
|
message: t("owners.errors.saving", {
|
||||||
error: JSON.stringify(result.errors)
|
error: error.message
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notification["success"]({
|
|
||||||
message: t("owners.successes.save")
|
|
||||||
});
|
|
||||||
|
|
||||||
if (refetch) await refetch();
|
|
||||||
form.resetFields();
|
|
||||||
form.resetFields();
|
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -72,6 +118,7 @@ function OwnerDetailFormContainer({ owner, refetch }) {
|
|||||||
title={t("menus.header.owners")}
|
title={t("menus.header.owners")}
|
||||||
extra={[
|
extra={[
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
|
key="delete"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
disabled={owner.jobs.length !== 0}
|
disabled={owner.jobs.length !== 0}
|
||||||
@@ -81,16 +128,29 @@ function OwnerDetailFormContainer({ owner, refetch }) {
|
|||||||
{t("general.actions.delete")}
|
{t("general.actions.delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
<Button type="primary" loading={loading} onClick={() => form.submit()}>
|
<Button key="save" type="primary" loading={loading} onClick={() => form.submit()}>
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Form form={form} onFinish={handleFinish} autoComplete="off" layout="vertical" initialValues={owner}>
|
<Form form={form} onFinish={handleFinish} autoComplete="off" layout="vertical" initialValues={owner}>
|
||||||
<OwnerDetailFormComponent loading={loading} form={form} />
|
<OwnerDetailFormComponent
|
||||||
|
loading={loading}
|
||||||
|
form={form}
|
||||||
|
isPhone1OptedOut={
|
||||||
|
bodyshop?.messagingservicesid &&
|
||||||
|
owner?.ownr_ph1 &&
|
||||||
|
optedOutPhones.has(phone(owner.ownr_ph1, "CA").phoneNumber?.replace(/^\+1/, ""))
|
||||||
|
}
|
||||||
|
isPhone2OptedOut={
|
||||||
|
bodyshop?.messagingservicesid &&
|
||||||
|
owner?.ownr_ph2 &&
|
||||||
|
optedOutPhones.has(phone(owner.ownr_ph2, "CA").phoneNumber?.replace(/^\+1/, ""))
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OwnerDetailFormContainer;
|
export default connect(mapStateToProps)(OwnerDetailFormContainer);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function PartsOrderBackorderEta({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
|
<Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
|
||||||
<DateFormatter>{backordered_eta}</DateFormatter>
|
<DateFormatter>{backordered_eta}</DateFormatter>
|
||||||
{isAlreadyBackordered && <CalendarFilled style={{ cursor: "pointer" }} onClick={handlePopover} />}
|
{isAlreadyBackordered && <CalendarFilled style={{ cursor: "pointer" }} onClick={handlePopover} />}
|
||||||
{loading && <Spin />}
|
{loading && <Spin />}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function PartsOrderLineBackorderButton({ partsOrderStatus, partsLineId, j
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
|
<Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
|
||||||
<Button loading={loading} onClick={handlePopover}>
|
<Button loading={loading} onClick={handlePopover}>
|
||||||
{isAlreadyBackordered ? t("parts_orders.actions.receive") : t("parts_orders.actions.backordered")}
|
{isAlreadyBackordered ? t("parts_orders.actions.receive") : t("parts_orders.actions.backordered")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ export function PartsOrderModalContainer({
|
|||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
okButtonProps={{ loading: saving }}
|
okButtonProps={{ loading: saving }}
|
||||||
cancelButtonProps={{ loading: saving }}
|
cancelButtonProps={{ loading: saving }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
width="75%"
|
width="75%"
|
||||||
forceRender
|
forceRender
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function PartsQueueDetailCard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{data ? (
|
{data ? (
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function PartsReceiveModalContainer({ partsReceiveModal, toggleModalVisib
|
|||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
okButtonProps={{ loading: loading }}
|
okButtonProps={{ loading: loading }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
forceRender
|
forceRender
|
||||||
width="50%"
|
width="50%"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop }) {
|
|||||||
<Modal
|
<Modal
|
||||||
title={!context || (context && !context.id) ? t("payments.labels.new") : t("payments.labels.edit")}
|
title={!context || (context && !context.id) ? t("payments.labels.new") : t("payments.labels.edit")}
|
||||||
open={open}
|
open={open}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
okText={t("general.actions.save")}
|
okText={t("general.actions.save")}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
width="50%"
|
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")}
|
okText={t("general.actions.close")}
|
||||||
width="90%"
|
width="90%"
|
||||||
title={t("printcenter.labels.title")}
|
title={t("printcenter.labels.title")}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<PrintCenterModalComponent context={context} />
|
<PrintCenterModalComponent context={context} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from "react";
|
|
||||||
import { Card, Form, Select } from "antd";
|
import { Card, Form, Select } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const FilterSettings = ({
|
const FilterSettings = ({
|
||||||
selectedMdInsCos,
|
selectedMdInsCos,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Card, Checkbox, Col, Form, Row } from "antd";
|
import { Card, Checkbox, Col, Form, Row } from "antd";
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const InformationSettings = ({ t }) => (
|
const InformationSettings = ({ t }) => (
|
||||||
<Card title={t("production.settings.information")}>
|
<Card title={t("production.settings.information")} style={{ maxWidth: "100%", overflowX: "auto" }}>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]} wrap>
|
||||||
{[
|
{[
|
||||||
"model_info",
|
"model_info",
|
||||||
"ownr_nm",
|
"ownr_nm",
|
||||||
@@ -21,7 +20,7 @@ const InformationSettings = ({ t }) => (
|
|||||||
"subtotal",
|
"subtotal",
|
||||||
"tasks"
|
"tasks"
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<Col span={4} key={item}>
|
<Col xs={24} sm={12} md={8} lg={6} key={item}>
|
||||||
<Form.Item name={item} valuePropName="checked">
|
<Form.Item name={item} valuePropName="checked">
|
||||||
<Checkbox>{t(`production.labels.${item}`)}</Checkbox>
|
<Checkbox>{t(`production.labels.${item}`)}</Checkbox>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { Card, Col, Form, Radio, Row } from "antd";
|
import { Card, Col, Form, Radio, Row } from "antd";
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
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({
|
||||||
<Card title={t("production.settings.layout")}>
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const LayoutSettings = ({ t, bodyshop }) => (
|
||||||
|
<Card title={t("production.settings.layout")} style={{ maxWidth: "100%", overflowX: "auto" }}>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
@@ -31,14 +38,18 @@ const LayoutSettings = ({ t }) => (
|
|||||||
{ value: false, label: t("production.labels.wide") }
|
{ value: false, label: t("production.labels.wide") }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
...(HasFeatureAccess({ bodyshop, featureName: "smartscheduling" })
|
||||||
name: "cardcolor",
|
? [
|
||||||
label: t("production.labels.cardcolor"),
|
{
|
||||||
options: [
|
name: "cardcolor",
|
||||||
{ value: true, label: t("production.labels.on") },
|
label: t("production.labels.cardcolor"),
|
||||||
{ value: false, label: t("production.labels.off") }
|
options: [
|
||||||
]
|
{ value: true, label: t("production.labels.on") },
|
||||||
},
|
{ value: false, label: t("production.labels.off") }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
name: "kiosk",
|
name: "kiosk",
|
||||||
label: t("production.labels.kiosk_mode"),
|
label: t("production.labels.kiosk_mode"),
|
||||||
@@ -48,9 +59,9 @@ const LayoutSettings = ({ t }) => (
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
].map(({ name, label, options }) => (
|
].map(({ name, label, options }) => (
|
||||||
<Col span={4} key={name}>
|
<Col xs={24} sm={16} md={10} lg={8} key={name}>
|
||||||
<Form.Item name={name} label={label}>
|
<Form.Item name={name} label={label}>
|
||||||
<Radio.Group>
|
<Radio.Group style={{ display: "flex", flexWrap: "nowrap" }}>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<Radio.Button key={option.value.toString()} value={option.value}>
|
<Radio.Button key={option.value.toString()} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
@@ -68,4 +79,4 @@ LayoutSettings.propTypes = {
|
|||||||
t: PropTypes.func.isRequired
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LayoutSettings;
|
export default connect(mapStateToProps)(LayoutSettings);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { Card, Checkbox, Form } from "antd";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
import { DragDropContext, Draggable, Droppable } from "../trello-board/dnd/lib/index.js";
|
import { DragDropContext, Draggable, Droppable } from "../trello-board/dnd/lib/index.js";
|
||||||
import { statisticsItems } from "./defaultKanbanSettings.js";
|
import { statisticsItems } from "./defaultKanbanSettings.js";
|
||||||
import { Card, Checkbox, Form } from "antd";
|
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChanges }) => {
|
const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChanges }) => {
|
||||||
const onDragEnd = (result) => {
|
const onDragEnd = (result) => {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
|
import { SettingOutlined } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Card, Col, Form, Popover, Row, Tabs } from "antd";
|
import { Button, Card, Col, Form, Popover, Row, Tabs } from "antd";
|
||||||
|
import { isFunction } from "lodash";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
|
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
|
||||||
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
|
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
|
||||||
import LayoutSettings from "./LayoutSettings.jsx";
|
|
||||||
import InformationSettings from "./InformationSettings.jsx";
|
|
||||||
import StatisticsSettings from "./StatisticsSettings.jsx";
|
|
||||||
import FilterSettings from "./FilterSettings.jsx";
|
import FilterSettings from "./FilterSettings.jsx";
|
||||||
import PropTypes from "prop-types";
|
import InformationSettings from "./InformationSettings.jsx";
|
||||||
import { isFunction } from "lodash";
|
import LayoutSettings from "./LayoutSettings.jsx";
|
||||||
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
|
import StatisticsSettings from "./StatisticsSettings.jsx";
|
||||||
import { SettingOutlined } from "@ant-design/icons";
|
|
||||||
|
|
||||||
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
|
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@@ -87,7 +87,7 @@ function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bod
|
|||||||
};
|
};
|
||||||
|
|
||||||
const overlay = (
|
const overlay = (
|
||||||
<Card style={{ minWidth: "80vw" }}>
|
<Card style={{ maxWidth: "80vw", width: "100%"}}>
|
||||||
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
|
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultActiveKey="1"
|
defaultActiveKey="1"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const Board = ({ id, className, orientation, cardSettings, ...additionalProps })
|
|||||||
default:
|
default:
|
||||||
return cardSizesVertical.small;
|
return cardSizesVertical.small;
|
||||||
}
|
}
|
||||||
}, [cardSettings]);
|
}, [cardSettings?.cardSize]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -100,24 +100,67 @@ const BoardContainer = ({
|
|||||||
const onLaneDrag = useCallback(
|
const onLaneDrag = useCallback(
|
||||||
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
|
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
setDragTime(source.droppableId);
|
|
||||||
if (!type || type !== "lane" || !source || !destination || isEqual(source, destination)) return;
|
|
||||||
|
|
||||||
|
// 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);
|
setIsProcessing(true);
|
||||||
|
|
||||||
dispatch(
|
// Handle valid drop to a different lane or position
|
||||||
actions.moveCardAcrossLanes({
|
if (destination && !isEqual(source, destination)) {
|
||||||
fromLaneId: source.droppableId,
|
dispatch(
|
||||||
toLaneId: destination.droppableId,
|
actions.moveCardAcrossLanes({
|
||||||
cardId: draggableId,
|
fromLaneId: source.droppableId,
|
||||||
index: destination.index
|
toLaneId: destination.droppableId,
|
||||||
})
|
cardId: draggableId,
|
||||||
);
|
index: destination.index
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Same-lane drop or no destination, revert to original position
|
||||||
|
dispatch(
|
||||||
|
actions.moveCardAcrossLanes({
|
||||||
|
fromLaneId: source.droppableId,
|
||||||
|
toLaneId: source.droppableId,
|
||||||
|
cardId: draggableId,
|
||||||
|
index: source.index
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error in onLaneDrag", err);
|
console.error("Error in onLaneDrag", err);
|
||||||
|
// Ensure revert on error
|
||||||
|
dispatch(
|
||||||
|
actions.moveCardAcrossLanes({
|
||||||
|
fromLaneId: source.droppableId,
|
||||||
|
toLaneId: source.droppableId,
|
||||||
|
cardId: draggableId,
|
||||||
|
index: source.index
|
||||||
|
})
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,21 +120,22 @@ const Lane = ({
|
|||||||
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
|
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
|
||||||
const FinalComponent = collapsed ? "div" : Component;
|
const FinalComponent = collapsed ? "div" : Component;
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
useWindowScroll: true,
|
data: renderedCards,
|
||||||
data: renderedCards
|
customScrollParent: laneRef.current
|
||||||
};
|
};
|
||||||
|
|
||||||
const verticalProps = {
|
const verticalProps = {
|
||||||
...commonProps,
|
...commonProps,
|
||||||
listClassName: "grid-container",
|
listClassName: "grid-container",
|
||||||
itemClassName: "grid-item",
|
itemClassName: "grid-item",
|
||||||
customScrollParent: laneRef.current,
|
|
||||||
components: {
|
components: {
|
||||||
List: ListComponent,
|
List: ListComponent,
|
||||||
Item: ItemComponent
|
Item: ItemComponent
|
||||||
},
|
},
|
||||||
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>,
|
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>,
|
||||||
overscan: { main: 10, reverse: 10 }
|
overscan: { main: 10, reverse: 10 },
|
||||||
|
// Ensure a minimum height for empty lanes to allow dropping
|
||||||
|
style: renderedCards.length === 0 ? { minHeight: "5px" } : {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const horizontalProps = {
|
const horizontalProps = {
|
||||||
@@ -142,7 +143,6 @@ const Lane = ({
|
|||||||
components: { Item: HeightPreservingItem },
|
components: { Item: HeightPreservingItem },
|
||||||
overscan: { main: 3, reverse: 3 },
|
overscan: { main: 3, reverse: 3 },
|
||||||
itemContent: (index, item) => renderDraggable(index, item),
|
itemContent: (index, item) => renderDraggable(index, item),
|
||||||
scrollerRef: provided.innerRef,
|
|
||||||
style: {
|
style: {
|
||||||
minWidth: maxCardWidth,
|
minWidth: maxCardWidth,
|
||||||
minHeight: maxLaneHeight
|
minHeight: maxLaneHeight
|
||||||
@@ -151,8 +151,6 @@ const Lane = ({
|
|||||||
|
|
||||||
const componentProps = orientation === "vertical" ? verticalProps : horizontalProps;
|
const componentProps = orientation === "vertical" ? verticalProps : horizontalProps;
|
||||||
|
|
||||||
// If the lane is collapsed, we want to render a div instead of the virtualized list, and we want to set the height to the max height of the lane so that
|
|
||||||
// the lane doesn't shrink when collapsed (in horizontal mode)
|
|
||||||
const finalComponentProps = collapsed
|
const finalComponentProps = collapsed
|
||||||
? orientation === "horizontal"
|
? orientation === "horizontal"
|
||||||
? {
|
? {
|
||||||
@@ -163,9 +161,8 @@ const Lane = ({
|
|||||||
: {}
|
: {}
|
||||||
: componentProps;
|
: componentProps;
|
||||||
|
|
||||||
// If the lane is horizontal and collapsed, we want to render a placeholder so that the lane doesn't shrink to 0 height and grows when
|
// Always render placeholder for empty lanes in vertical mode to ensure droppable area
|
||||||
// a card is dragged over it
|
const shouldRenderPlaceholder = orientation === "vertical" ? collapsed || renderedCards.length === 0 : collapsed;
|
||||||
const shouldRenderPlaceholder = orientation !== "horizontal" && (collapsed || renderedCards.length === 0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeightMemoryWrapper
|
<HeightMemoryWrapper
|
||||||
@@ -180,13 +177,14 @@ const Lane = ({
|
|||||||
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
|
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
{...provided.droppableProps}
|
ref={laneRef}
|
||||||
ref={provided.innerRef}
|
style={{ height: "100%", width: "100%" }}
|
||||||
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
|
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
|
||||||
style={{ ...provided.droppableProps.style }}
|
|
||||||
>
|
>
|
||||||
<FinalComponent {...finalComponentProps} />
|
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...provided.droppableProps.style }}>
|
||||||
{shouldRenderPlaceholder && provided.placeholder}
|
<FinalComponent {...finalComponentProps} />
|
||||||
|
{shouldRenderPlaceholder && provided.placeholder}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HeightMemoryWrapper>
|
</HeightMemoryWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
|||||||
if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]);
|
if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyTooltipOnHide content={popContent} open={visibility}>
|
<Popover destroyOnHidden content={popContent} open={visibility}>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
{record[type] ? (
|
{record[type] ? (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import React, { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { QUERY_ACTIVE_EMPLOYEES, QUERY_ACTIVE_EMPLOYEES_WITH_EMAIL } from "../../graphql/employees.queries";
|
import { QUERY_ACTIVE_EMPLOYEES, QUERY_ACTIVE_EMPLOYEES_WITH_EMAIL } from "../../graphql/employees.queries";
|
||||||
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
||||||
import { selectReportCenter } from "../../redux/modals/modals.selectors";
|
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 EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
|
||||||
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-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 VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||||
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
|
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
|
||||||
import "./report-center-modal.styles.scss";
|
import "./report-center-modal.styles.scss";
|
||||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
reportCenterModal: selectReportCenter,
|
reportCenterModal: selectReportCenter,
|
||||||
@@ -389,5 +389,7 @@ const restrictedReports = [
|
|||||||
{ key: "job_costing_ro_date_detail", days: 183 },
|
{ key: "job_costing_ro_date_detail", days: 183 },
|
||||||
{ key: "job_costing_ro_estimator", days: 183 },
|
{ key: "job_costing_ro_estimator", days: 183 },
|
||||||
{ key: "job_lifecycle_date_detail", 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()}
|
onOk={() => toggleModalVisible()}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
cancelButtonProps={{ style: { display: "none" } }}
|
cancelButtonProps={{ style: { display: "none" } }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
width="80%"
|
width="80%"
|
||||||
>
|
>
|
||||||
<RbacWrapperComponent action="shop:reportcenter">
|
<RbacWrapperComponent action="shop:reportcenter">
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export function ScheduleJobModalContainer({
|
|||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
width={"90%"}
|
width={"90%"}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
okButtonProps={{
|
okButtonProps={{
|
||||||
loading: loading
|
loading: loading
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
|||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
open={state.open}
|
open={state.open}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
width="80%"
|
width="80%"
|
||||||
closable={false}
|
closable={false}
|
||||||
cancelButtonProps={{ style: { display: "none" } }}
|
cancelButtonProps={{ style: { display: "none" } }}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Card, Tabs } from "antd";
|
import { Button, Card, Tabs } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -24,6 +23,8 @@ import ShopInfoRoGuard from "./shop-info.roguard.component";
|
|||||||
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
import ShopInfoNotificationsAutoadd from "./shop-info.notifications-autoadd.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -41,6 +42,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
|||||||
names: ["CriticalPartsScanning", "Enhanced_Payroll"],
|
names: ["CriticalPartsScanning", "Enhanced_Payroll"],
|
||||||
splitKey: bodyshop.imexshopid
|
splitKey: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
const { scenarioNotificationsOn } = useSocket();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
@@ -137,9 +139,21 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
key: "intellipay",
|
key: "intellipay",
|
||||||
label: InstanceRenderManager({ rome: t("bodyshop.labels.romepay"), imex: t("bodyshop.labels.imexpay") }),
|
label: InstanceRenderManager({
|
||||||
|
rome: t("bodyshop.labels.romepay"),
|
||||||
|
imex: t("bodyshop.labels.imexpay")
|
||||||
|
}),
|
||||||
children: <ShopInfoIntellipay form={form} />
|
children: <ShopInfoIntellipay form={form} />
|
||||||
}
|
},
|
||||||
|
...(scenarioNotificationsOn
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "notifications_autoadd",
|
||||||
|
label: t("bodyshop.labels.notifications.followers"),
|
||||||
|
children: <ShopInfoNotificationsAutoadd form={form} bodyshop={bodyshop} />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Form, Typography } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component.jsx";
|
||||||
|
|
||||||
|
const { Text, Paragraph } = Typography;
|
||||||
|
|
||||||
|
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.user_email && e.id) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Paragraph>{t("bodyshop.fields.notifications.description")}</Paragraph>
|
||||||
|
<Text type="secondary">{t("bodyshop.labels.notifications.followers")}</Text>
|
||||||
|
{employeeOptions.length > 0 ? (
|
||||||
|
<Form.Item
|
||||||
|
name="notification_followers"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
type: "array",
|
||||||
|
message: t("general.validation.array")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: async (_, value) => {
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return Promise.resolve(); // Allow empty array
|
||||||
|
}
|
||||||
|
const hasInvalid = value.some((id) => id == null || typeof id !== "string" || id.trim() === "");
|
||||||
|
if (hasInvalid) {
|
||||||
|
return Promise.reject(new Error(t("bodyshop.fields.notifications.invalid_followers")));
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<EmployeeSearchSelectComponent
|
||||||
|
style={{ minWidth: "100%" }}
|
||||||
|
mode="multiple"
|
||||||
|
options={employeeOptions}
|
||||||
|
placeholder={t("bodyshop.fields.notifications.placeholder")}
|
||||||
|
showEmail={true}
|
||||||
|
onChange={(value) => {
|
||||||
|
// Filter out null or invalid values before passing to Form
|
||||||
|
const cleanedValue = value?.filter((id) => id != null && typeof id === "string" && id.trim() !== "");
|
||||||
|
return cleanedValue;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Text type="secondary">{t("bodyshop.fields.no_employees_available")}</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,23 +25,6 @@ export function ShopTemplateTestRender({ bodyshop, query, emailEditorRef, style
|
|||||||
|
|
||||||
emailEditorRef.current.exportHtml(async (data) => {
|
emailEditorRef.current.exportHtml(async (data) => {
|
||||||
try {
|
try {
|
||||||
// const inlineHtml = await axios.post("/render/inlinecss", {
|
|
||||||
// html: data.html,
|
|
||||||
// url: `${window.location.protocol}://${window.location.host}/`,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const { data: contextData } = await client.query({
|
|
||||||
// query: gql(query),
|
|
||||||
// variables: variables,
|
|
||||||
//
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const renderResponse = await axios.post("/render", {
|
|
||||||
// view: inlineHtml.data,
|
|
||||||
// context: { ...contextData, bodyshop: bodyshop },
|
|
||||||
// });
|
|
||||||
// displayTemplateInWindowNoprint(renderResponse.data);
|
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
|
|||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}}
|
}}
|
||||||
okButtonProps={{ disabled: !isTouched }}
|
okButtonProps={{ disabled: !isTouched }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{data ? (
|
{data ? (
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default function TimeTicketCalculatorComponent({
|
|||||||
open={visible}
|
open={visible}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
placement="right"
|
placement="right"
|
||||||
destroyTooltipOnHide
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Button onClick={(e) => e.preventDefault()}>
|
<Button onClick={(e) => e.preventDefault()}>
|
||||||
<Space>
|
<Space>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function TimeTicketListTeamPay({ bodyshop, context, actions }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal width={"80%"} open={visible} destroyOnClose onOk={handleOk} onCancel={() => setVisible(false)}>
|
<Modal width={"80%"} open={visible} destroyOnHidden onOk={handleOk} onCancel={() => setVisible(false)}>
|
||||||
<Form layout="vertical" form={form} initialValues={{ jobid: jobId }}>
|
<Form layout="vertical" form={form} initialValues={{ jobid: jobId }}>
|
||||||
<LayoutFormRow grow noDivider>
|
<LayoutFormRow grow noDivider>
|
||||||
<Form.Item shouldUpdate>
|
<Form.Item shouldUpdate>
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
|||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
id="time-ticket-modal"
|
id="time-ticket-modal"
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export function TimeTickeTaskModalContainer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export function UpdateAlert({ updateAvailable }) {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col sm={24} md={8} lg={6}>
|
<Col sm={24} md={8} lg={6}>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => window.open("https://imex-online.noticeable.news/", "_blank")}>
|
<Button onClick={() => window.open("https://shopmanagement.canny.io/changelog", "_blank")}>
|
||||||
{i18n.t("general.actions.viewreleasenotes")}
|
{i18n.t("general.actions.viewreleasenotes")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button loading={loading} type="primary" onClick={() => ReloadNewVersion()}>
|
<Button loading={loading} type="primary" onClick={() => ReloadNewVersion()}>
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export const QUERY_BODYSHOP = gql`
|
|||||||
use_paint_scale_data
|
use_paint_scale_data
|
||||||
intellipay_config
|
intellipay_config
|
||||||
md_ro_guard
|
md_ro_guard
|
||||||
|
notification_followers
|
||||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -271,6 +272,7 @@ export const UPDATE_SHOP = gql`
|
|||||||
md_tasks_presets
|
md_tasks_presets
|
||||||
intellipay_config
|
intellipay_config
|
||||||
md_ro_guard
|
md_ro_guard
|
||||||
|
notification_followers
|
||||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -310,7 +312,6 @@ export const QUERY_INTAKE_CHECKLIST = gql`
|
|||||||
intakechecklist
|
intakechecklist
|
||||||
status
|
status
|
||||||
owner {
|
owner {
|
||||||
allow_text_message
|
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) {
|
labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const CONVERSATION_SUBSCRIPTION_BY_PK = gql`
|
|||||||
id
|
id
|
||||||
status
|
status
|
||||||
text
|
text
|
||||||
|
is_system
|
||||||
isoutbound
|
isoutbound
|
||||||
image
|
image
|
||||||
image_path
|
image_path
|
||||||
@@ -77,6 +78,7 @@ export const GET_CONVERSATION_DETAILS = gql`
|
|||||||
id
|
id
|
||||||
status
|
status
|
||||||
text
|
text
|
||||||
|
is_system
|
||||||
isoutbound
|
isoutbound
|
||||||
image
|
image
|
||||||
image_path
|
image_path
|
||||||
|
|||||||
@@ -423,6 +423,7 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
actual_completion
|
actual_completion
|
||||||
actual_delivery
|
actual_delivery
|
||||||
actual_in
|
actual_in
|
||||||
|
acv_amount
|
||||||
adjustment_bottom_line
|
adjustment_bottom_line
|
||||||
alt_transport
|
alt_transport
|
||||||
area_of_damage
|
area_of_damage
|
||||||
@@ -511,6 +512,7 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
est_ph1
|
est_ph1
|
||||||
flat_rate_ats
|
flat_rate_ats
|
||||||
federal_tax_rate
|
federal_tax_rate
|
||||||
|
hit_and_run
|
||||||
id
|
id
|
||||||
inproduction
|
inproduction
|
||||||
ins_addr1
|
ins_addr1
|
||||||
@@ -683,6 +685,8 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
scheduled_delivery
|
scheduled_delivery
|
||||||
scheduled_in
|
scheduled_in
|
||||||
selling_dealer
|
selling_dealer
|
||||||
|
estimate_approved
|
||||||
|
estimate_sent_approval
|
||||||
selling_dealer_contact
|
selling_dealer_contact
|
||||||
servicing_dealer
|
servicing_dealer
|
||||||
servicing_dealer_contact
|
servicing_dealer_contact
|
||||||
@@ -870,7 +874,6 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
|||||||
}
|
}
|
||||||
owner {
|
owner {
|
||||||
id
|
id
|
||||||
allow_text_message
|
|
||||||
preferred_contact
|
preferred_contact
|
||||||
tax_number
|
tax_number
|
||||||
}
|
}
|
||||||
@@ -927,6 +930,8 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
|||||||
date_exported
|
date_exported
|
||||||
date_repairstarted
|
date_repairstarted
|
||||||
date_scheduled
|
date_scheduled
|
||||||
|
estimate_sent_approval
|
||||||
|
estimate_approved
|
||||||
date_estimated
|
date_estimated
|
||||||
employee_body_rel {
|
employee_body_rel {
|
||||||
id
|
id
|
||||||
@@ -1075,6 +1080,8 @@ export const UPDATE_JOB = gql`
|
|||||||
date_repairstarted
|
date_repairstarted
|
||||||
date_void
|
date_void
|
||||||
date_lost_sale
|
date_lost_sale
|
||||||
|
estimate_sent_approval
|
||||||
|
estimate_approved
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2063,7 +2070,6 @@ export const QUERY_JOB_CHECKLISTS = gql`
|
|||||||
production_vars
|
production_vars
|
||||||
owner {
|
owner {
|
||||||
id
|
id
|
||||||
allow_text_message
|
|
||||||
}
|
}
|
||||||
bodyshop {
|
bodyshop {
|
||||||
id
|
id
|
||||||
@@ -2420,7 +2426,6 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
|||||||
ownr_ph2
|
ownr_ph2
|
||||||
owner {
|
owner {
|
||||||
id
|
id
|
||||||
allow_text_message
|
|
||||||
preferred_contact
|
preferred_contact
|
||||||
tax_number
|
tax_number
|
||||||
}
|
}
|
||||||
@@ -2429,6 +2434,8 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
|||||||
plate_st
|
plate_st
|
||||||
po_number
|
po_number
|
||||||
production_vars
|
production_vars
|
||||||
|
estimate_sent_approval
|
||||||
|
estimate_approved
|
||||||
ro_number
|
ro_number
|
||||||
scheduled_completion
|
scheduled_completion
|
||||||
scheduled_delivery
|
scheduled_delivery
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export const QUERY_OWNER_BY_ID = gql`
|
|||||||
owners_by_pk(id: $id) {
|
owners_by_pk(id: $id) {
|
||||||
id
|
id
|
||||||
accountingid
|
accountingid
|
||||||
allow_text_message
|
|
||||||
ownr_addr1
|
ownr_addr1
|
||||||
ownr_addr2
|
ownr_addr2
|
||||||
ownr_co_nm
|
ownr_co_nm
|
||||||
@@ -104,7 +103,6 @@ export const QUERY_ALL_OWNERS = gql`
|
|||||||
query QUERY_ALL_OWNERS {
|
query QUERY_ALL_OWNERS {
|
||||||
owners {
|
owners {
|
||||||
id
|
id
|
||||||
allow_text_message
|
|
||||||
created_at
|
created_at
|
||||||
ownr_addr1
|
ownr_addr1
|
||||||
ownr_addr2
|
ownr_addr2
|
||||||
@@ -129,7 +127,6 @@ export const QUERY_ALL_OWNERS_PAGINATED = gql`
|
|||||||
query QUERY_ALL_OWNERS_PAGINATED($search: String, $offset: Int, $limit: Int, $order: [owners_order_by!]!) {
|
query QUERY_ALL_OWNERS_PAGINATED($search: String, $offset: Int, $limit: Int, $order: [owners_order_by!]!) {
|
||||||
search_owners(args: { search: $search }, offset: $offset, limit: $limit, order_by: $order) {
|
search_owners(args: { search: $search }, offset: $offset, limit: $limit, order_by: $order) {
|
||||||
id
|
id
|
||||||
allow_text_message
|
|
||||||
created_at
|
created_at
|
||||||
ownr_addr1
|
ownr_addr1
|
||||||
ownr_addr2
|
ownr_addr2
|
||||||
|
|||||||
64
client/src/graphql/phone-number-opt-out.queries.js
Normal file
64
client/src/graphql/phone-number-opt-out.queries.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
|
export const GET_PHONE_NUMBER_OPT_OUT = gql`
|
||||||
|
query GET_PHONE_NUMBER_OPT_OUT($bodyshopid: uuid!, $phone_number: String!) {
|
||||||
|
phone_number_opt_out(where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _eq: $phone_number } }) {
|
||||||
|
id
|
||||||
|
bodyshopid
|
||||||
|
phone_number
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_PHONE_NUMBER_OPT_OUTS = gql`
|
||||||
|
query GET_PHONE_NUMBER_OPT_OUTS($bodyshopid: uuid!, $search: String) {
|
||||||
|
phone_number_opt_out(
|
||||||
|
where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _ilike: $search } }
|
||||||
|
order_by: [{ phone_number: asc }, { updated_at: desc }]
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
bodyshopid
|
||||||
|
phone_number
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_PHONE_NUMBER_OPT_OUTS_BY_NUMBERS = gql`
|
||||||
|
query GET_PHONE_NUMBER_OPT_OUTS_BY_NUMBERS($bodyshopid: uuid!, $phone_numbers: [String!]) {
|
||||||
|
phone_number_opt_out(
|
||||||
|
where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _in: $phone_numbers } }
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
bodyshopid
|
||||||
|
phone_number
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SEARCH_OWNERS_BY_PHONE_NUMBERS = gql`
|
||||||
|
query SEARCH_OWNERS_BY_PHONE_NUMBERS($bodyshopid: uuid!, $phone_numbers: [String!]) {
|
||||||
|
owners(
|
||||||
|
where: {
|
||||||
|
shopid: { _eq: $bodyshopid },
|
||||||
|
_or: [
|
||||||
|
{ ownr_ph1: { _in: $phone_numbers } },
|
||||||
|
{ ownr_ph2: { _in: $phone_numbers } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_co_nm
|
||||||
|
ownr_ph1
|
||||||
|
ownr_ph2
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -91,6 +91,7 @@ export const QUERY_NOTIFICATION_SETTINGS = gql`
|
|||||||
associations(where: { _and: { useremail: { _eq: $email }, active: { _eq: true } } }) {
|
associations(where: { _and: { useremail: { _eq: $email }, active: { _eq: true } } }) {
|
||||||
id
|
id
|
||||||
notification_settings
|
notification_settings
|
||||||
|
notifications_autoadd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -103,3 +104,12 @@ export const UPDATE_NOTIFICATION_SETTINGS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const UPDATE_NOTIFICATIONS_AUTOADD = gql`
|
||||||
|
mutation UPDATE_NOTIFICATIONS_AUTOADD($id: uuid!, $autoadd: Boolean!) {
|
||||||
|
update_associations_by_pk(pk_columns: { id: $id }, _set: { notifications_autoadd: $autoadd }) {
|
||||||
|
id
|
||||||
|
notifications_autoadd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
42
client/src/pages/feature-request/feature-request.page.jsx
Normal file
42
client/src/pages/feature-request/feature-request.page.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||||
|
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function FeedbackPage({ setBreadcrumbs, setSelectedHeader }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = t("titles.feature-request", {
|
||||||
|
app: InstanceRenderManager({
|
||||||
|
imex: "$t(titles.imexonline)",
|
||||||
|
rome: "$t(titles.romeonline)"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
setBreadcrumbs([{ link: "/manage/feature-request", label: t("titles.bc.feature-request") }]);
|
||||||
|
}, [t, setBreadcrumbs, setSelectedHeader]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function RenderCanny() {
|
||||||
|
const ssoToken = await axios.post("/sso/canny");
|
||||||
|
window.Canny("render", {
|
||||||
|
boardToken: "bba97b06-70db-0334-dee7-8108d73ef614",
|
||||||
|
basePath: `/manage/feature-request`, // See step 2
|
||||||
|
ssoToken: ssoToken.data, // See step 3,
|
||||||
|
theme: "light" // options: light [default], dark, auto
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RenderCanny();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div data-canny />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(FeedbackPage);
|
||||||
@@ -114,7 +114,6 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
|||||||
if (!!!job.ownerid) {
|
if (!!!job.ownerid) {
|
||||||
ownerData = job.owner.data;
|
ownerData = job.owner.data;
|
||||||
ownerData.shopid = bodyshop.id;
|
ownerData.shopid = bodyshop.id;
|
||||||
delete ownerData.allow_text_message;
|
|
||||||
delete ownerData.preferred_contact;
|
delete ownerData.preferred_contact;
|
||||||
delete job.ownerid;
|
delete job.ownerid;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FloatButton, Layout, Spin } from "antd";
|
import { Button, FloatButton, Layout, Space, Spin } from "antd";
|
||||||
|
import { AlertOutlined, BulbOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
// import preval from "preval.macro";
|
// import preval from "preval.macro";
|
||||||
import React, { lazy, Suspense, useEffect, useState } from "react";
|
import React, { lazy, Suspense, useEffect, useState } from "react";
|
||||||
@@ -19,7 +20,6 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
|
|||||||
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
|
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
|
||||||
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
||||||
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
||||||
import { requestForToken } from "../../firebase/firebase.utils";
|
|
||||||
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
||||||
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||||
@@ -56,6 +56,7 @@ const ContractCreatePage = lazy(() => import("../contract-create/contract-create
|
|||||||
const ContractDetailPage = lazy(() => import("../contract-detail/contract-detail.page.container"));
|
const ContractDetailPage = lazy(() => import("../contract-detail/contract-detail.page.container"));
|
||||||
const ContractsList = lazy(() => import("../contracts/contracts.page.container"));
|
const ContractsList = lazy(() => import("../contracts/contracts.page.container"));
|
||||||
const BillsListPage = lazy(() => import("../bills/bills.page.container"));
|
const BillsListPage = lazy(() => import("../bills/bills.page.container"));
|
||||||
|
const FeatureRequestPage = lazy(() => import("../feature-request/feature-request.page.jsx"));
|
||||||
|
|
||||||
const JobCostingModal = lazy(() => import("../../components/job-costing-modal/job-costing-modal.container"));
|
const JobCostingModal = lazy(() => import("../../components/job-costing-modal/job-costing-modal.container"));
|
||||||
const ReportCenterModal = lazy(() => import("../../components/report-center-modal/report-center-modal.container"));
|
const ReportCenterModal = lazy(() => import("../../components/report-center-modal/report-center-modal.container"));
|
||||||
@@ -180,15 +181,12 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [alerts, displayedAlertIds, notification]);
|
}, [alerts, displayedAlertIds, notification]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const widgetId = InstanceRenderManager({
|
window.Canny("initChangelog", {
|
||||||
imex: "IABVNO4scRKY11XBQkNr",
|
appID: "680bd2c7ee501290377f6686",
|
||||||
rome: "mQdqARMzkZRUVugJ6TdS"
|
position: "top",
|
||||||
});
|
align: "left",
|
||||||
window.noticeable.render("widget", widgetId);
|
theme: "light" // options: light [default], dark, auto
|
||||||
requestForToken().catch((error) => {
|
|
||||||
console.error(`Unable to request for token.`, error);
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -480,6 +478,8 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
// element={<ShopTemplates />}
|
// element={<ShopTemplates />}
|
||||||
// />
|
// />
|
||||||
}
|
}
|
||||||
|
<Route path="/feature-request/*" index element={<FeatureRequestPage />} />
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/shop/vendors"
|
path="/shop/vendors"
|
||||||
element={
|
element={
|
||||||
@@ -669,7 +669,12 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
margin: "1rem 0rem"
|
margin: "1rem 0rem"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex" }}>
|
<Link to="/manage/feature-request">
|
||||||
|
<Button icon={<BulbOutlined />} type="text">
|
||||||
|
{t("general.labels.feature-request")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Space>
|
||||||
<WssStatusDisplayComponent />
|
<WssStatusDisplayComponent />
|
||||||
<div onClick={broadcastMessage}>
|
<div onClick={broadcastMessage}>
|
||||||
{`${InstanceRenderManager({
|
{`${InstanceRenderManager({
|
||||||
@@ -677,8 +682,10 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
rome: t("titles.romeonline")
|
rome: t("titles.romeonline")
|
||||||
})} - ${import.meta.env.VITE_APP_GIT_SHA_DATE}`}
|
})} - ${import.meta.env.VITE_APP_GIT_SHA_DATE}`}
|
||||||
</div>
|
</div>
|
||||||
<div id="noticeable-widget" style={{ marginLeft: "1rem" }} />
|
<Button icon={<AlertOutlined />} data-canny-changelog type="text">
|
||||||
</div>
|
{t("general.labels.changelog")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
||||||
Disclaimer & Notices
|
Disclaimer & Notices
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import ShopCsiConfig from "../../components/shop-csi-config/shop-csi-config.comp
|
|||||||
import ShopEmployeesContainer from "../../components/shop-employees/shop-employees.container";
|
import ShopEmployeesContainer from "../../components/shop-employees/shop-employees.container";
|
||||||
import ShopInfoContainer from "../../components/shop-info/shop-info.container";
|
import ShopInfoContainer from "../../components/shop-info/shop-info.container";
|
||||||
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
|
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
|
||||||
|
import ShopInfoConsentComponent from "../../components/shop-info/shop-info.consent.component";
|
||||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
||||||
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
|
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
|
||||||
|
|
||||||
@@ -91,6 +91,16 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
|
|||||||
children: <ShopCsiConfig />
|
children: <ShopCsiConfig />
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bodyshop.messagingservicesid) {
|
||||||
|
// Add Consent Settings tab
|
||||||
|
items.push({
|
||||||
|
key: "consent",
|
||||||
|
label: t("bodyshop.labels.consent_settings"),
|
||||||
|
children: <ShopInfoConsentComponent bodyshop={bodyshop} />
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RbacWrapper action="shop:config">
|
<RbacWrapper action="shop:config">
|
||||||
<Tabs activeKey={search.tab} onChange={(key) => history({ search: `?tab=${key}` })} items={items} />
|
<Tabs activeKey={search.tab} onChange={(key) => history({ search: `?tab=${key}` })} items={items} />
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ const userReducer = (state = INITIAL_STATE, action) => {
|
|||||||
...action.payload //Spread current user details in.
|
...action.payload //Spread current user details in.
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
case UserActionTypes.SET_SHOP_DETAILS:
|
case UserActionTypes.SET_SHOP_DETAILS:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -126,6 +125,7 @@ const userReducer = (state = INITIAL_STATE, action) => {
|
|||||||
...state,
|
...state,
|
||||||
imexshopid: action.payload
|
imexshopid: action.payload
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "@firebase/auth";
|
} from "@firebase/auth";
|
||||||
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
||||||
import { getToken } from "@firebase/messaging";
|
import { getToken } from "@firebase/messaging";
|
||||||
import * as Sentry from "@sentry/browser";
|
import * as Sentry from "@sentry/react";
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@@ -335,20 +335,12 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
InstanceRenderManager({
|
window.$crisp.push(["set", "user:company", [payload.shopname]]);
|
||||||
executeFunction: true,
|
window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]);
|
||||||
args: [],
|
if (authRecord[0] && authRecord[0].user.validemail) {
|
||||||
imex: () => {
|
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
||||||
window.$crisp.push(["set", "user:company", [payload.shopname]]);
|
}
|
||||||
window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]);
|
|
||||||
if (authRecord[0] && authRecord[0].user.validemail) {
|
|
||||||
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rome: () => {
|
|
||||||
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
payload.features?.allAccess === true
|
payload.features?.allAccess === true
|
||||||
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
||||||
: (() => {
|
: (() => {
|
||||||
@@ -359,6 +351,14 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
|||||||
);
|
);
|
||||||
window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]);
|
window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
InstanceRenderManager({
|
||||||
|
executeFunction: true,
|
||||||
|
args: [],
|
||||||
|
rome: () => {
|
||||||
|
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Couldnt find $crisp.", error.message);
|
console.warn("Couldnt find $crisp.", error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,6 @@
|
|||||||
"intellipay_config": {
|
"intellipay_config": {
|
||||||
"cash_discount_percentage": "Cash Discount %",
|
"cash_discount_percentage": "Cash Discount %",
|
||||||
"enable_cash_discount": "Enable Cash Discounting",
|
"enable_cash_discount": "Enable Cash Discounting",
|
||||||
"payment_type": "Payment Type Map",
|
|
||||||
"payment_map": {
|
"payment_map": {
|
||||||
"amex": "American Express",
|
"amex": "American Express",
|
||||||
"disc": "Discover",
|
"disc": "Discover",
|
||||||
@@ -344,7 +343,8 @@
|
|||||||
"jcb": "JCB",
|
"jcb": "JCB",
|
||||||
"mast": "MasterCard",
|
"mast": "MasterCard",
|
||||||
"visa": "Visa"
|
"visa": "Visa"
|
||||||
}
|
},
|
||||||
|
"payment_type": "Payment Type Map"
|
||||||
},
|
},
|
||||||
"invoice_federal_tax_rate": "Invoices - Federal Tax Rate",
|
"invoice_federal_tax_rate": "Invoices - Federal Tax Rate",
|
||||||
"invoice_local_tax_rate": "Invoices - Local Tax Rate",
|
"invoice_local_tax_rate": "Invoices - Local Tax Rate",
|
||||||
@@ -648,9 +648,15 @@
|
|||||||
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
|
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
|
||||||
"uselocalmediaserver": "Use Local Media Server?",
|
"uselocalmediaserver": "Use Local Media Server?",
|
||||||
"website": "Website",
|
"website": "Website",
|
||||||
"zip_post": "Zip/Postal Code"
|
"zip_post": "Zip/Postal Code",
|
||||||
|
"notifications": {
|
||||||
|
"description": "Select employees to automatically follow new jobs and receive notifications for job updates.",
|
||||||
|
"placeholder": "Search for employees",
|
||||||
|
"invalid_followers": "Invalid selection. Please select valid employees."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"consent_settings": "Phone Number Opt-Out List",
|
||||||
"2tiername": "Name => RO",
|
"2tiername": "Name => RO",
|
||||||
"2tiersetup": "2 Tier Setup",
|
"2tiersetup": "2 Tier Setup",
|
||||||
"2tiersource": "Source => RO",
|
"2tiersource": "Source => RO",
|
||||||
@@ -728,7 +734,10 @@
|
|||||||
"ssbuckets": "Job Size Definitions",
|
"ssbuckets": "Job Size Definitions",
|
||||||
"systemsettings": "System Settings",
|
"systemsettings": "System Settings",
|
||||||
"task-presets": "Task Presets",
|
"task-presets": "Task Presets",
|
||||||
"workingdays": "Working Days"
|
"workingdays": "Working Days",
|
||||||
|
"notifications": {
|
||||||
|
"followers": "Notifications"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
"contains": "Contains",
|
"contains": "Contains",
|
||||||
@@ -766,7 +775,6 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addtoproduction": "Add Job to Production?",
|
"addtoproduction": "Add Job to Production?",
|
||||||
"allow_text_message": "Permission to Text?",
|
|
||||||
"checklist": "Checklist",
|
"checklist": "Checklist",
|
||||||
"printpack": "Job Intake Print Pack",
|
"printpack": "Job Intake Print Pack",
|
||||||
"removefromproduction": "Remove Job from Production?"
|
"removefromproduction": "Remove Job from Production?"
|
||||||
@@ -1236,6 +1244,7 @@
|
|||||||
"areyousure": "Are you sure?",
|
"areyousure": "Are you sure?",
|
||||||
"barcode": "Barcode",
|
"barcode": "Barcode",
|
||||||
"cancel": "Are you sure you want to cancel? Your changes will not be saved.",
|
"cancel": "Are you sure you want to cancel? Your changes will not be saved.",
|
||||||
|
"changelog": "Change Log",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"confirmpassword": "Confirm Password",
|
"confirmpassword": "Confirm Password",
|
||||||
"created_at": "Created At",
|
"created_at": "Created At",
|
||||||
@@ -1245,6 +1254,7 @@
|
|||||||
"errors": "Errors",
|
"errors": "Errors",
|
||||||
"excel": "Excel",
|
"excel": "Excel",
|
||||||
"exceptiontitle": "An error has occurred.",
|
"exceptiontitle": "An error has occurred.",
|
||||||
|
"feature-request": "Have a feature request?",
|
||||||
"friday": "Friday",
|
"friday": "Friday",
|
||||||
"globalsearch": "Global Search",
|
"globalsearch": "Global Search",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
@@ -1323,9 +1333,9 @@
|
|||||||
"notfoundtitle": "We couldn't find what you're looking for...",
|
"notfoundtitle": "We couldn't find what you're looking for...",
|
||||||
"partnernotrunning": "{{app}} has detected that the partner is not running. Please ensure it is running to enable full functionality.",
|
"partnernotrunning": "{{app}} has detected that the partner is not running. Please ensure it is running to enable full functionality.",
|
||||||
"rbacunauth": "You are not authorized to view this content. Please reach out to your shop manager to change your access level.",
|
"rbacunauth": "You are not authorized to view this content. Please reach out to your shop manager to change your access level.",
|
||||||
|
"submit-for-testing": "Submitted Job for testing successfully.",
|
||||||
"unsavedchanges": "You have unsaved changes.",
|
"unsavedchanges": "You have unsaved changes.",
|
||||||
"unsavedchangespopup": "You have unsaved changes. Are you sure you want to leave?",
|
"unsavedchangespopup": "You have unsaved changes. Are you sure you want to leave?"
|
||||||
"submit-for-testing": "Submitted Job for testing successfully."
|
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"dateRangeExceeded": "The date range has been exceeded.",
|
"dateRangeExceeded": "The date range has been exceeded.",
|
||||||
@@ -1636,9 +1646,12 @@
|
|||||||
"actual_completion": "Actual Completion",
|
"actual_completion": "Actual Completion",
|
||||||
"actual_delivery": "Actual Delivery",
|
"actual_delivery": "Actual Delivery",
|
||||||
"actual_in": "Actual In",
|
"actual_in": "Actual In",
|
||||||
|
"acv_amount": "ACV Amount",
|
||||||
"adjustment_bottom_line": "Adjustments",
|
"adjustment_bottom_line": "Adjustments",
|
||||||
"adjustmenthours": "Adjustment Hours",
|
"adjustmenthours": "Adjustment Hours",
|
||||||
"alt_transport": "Alt. Trans.",
|
"alt_transport": "Alt. Trans.",
|
||||||
|
"estimate_sent_approval": "Estimate Sent for Approval",
|
||||||
|
"estimate_approved": "Estimate Approved",
|
||||||
"area_of_damage_impact": {
|
"area_of_damage_impact": {
|
||||||
"10": "Left Front Side",
|
"10": "Left Front Side",
|
||||||
"11": "Left Front Corner",
|
"11": "Left Front Corner",
|
||||||
@@ -1761,9 +1774,10 @@
|
|||||||
"est_ct_ln": "Estimator Last Name",
|
"est_ct_ln": "Estimator Last Name",
|
||||||
"est_ea": "Estimator Email",
|
"est_ea": "Estimator Email",
|
||||||
"est_ph1": "Estimator Phone #",
|
"est_ph1": "Estimator Phone #",
|
||||||
"flat_rate_ats": "Flat Rate ATS?",
|
|
||||||
"federal_tax_payable": "Federal Tax Payable",
|
"federal_tax_payable": "Federal Tax Payable",
|
||||||
"federal_tax_rate": "Federal Tax Rate",
|
"federal_tax_rate": "Federal Tax Rate",
|
||||||
|
"flat_rate_ats": "Flat Rate ATS?",
|
||||||
|
"hit_and_run": "Hit and Run",
|
||||||
"ins_addr1": "Insurance Co. Address",
|
"ins_addr1": "Insurance Co. Address",
|
||||||
"ins_city": "Insurance Co. City",
|
"ins_city": "Insurance Co. City",
|
||||||
"ins_co_id": "Insurance Co. ID",
|
"ins_co_id": "Insurance Co. ID",
|
||||||
@@ -1943,6 +1957,8 @@
|
|||||||
"scheddates": "Schedule Dates"
|
"scheddates": "Schedule Dates"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"sent": "",
|
||||||
|
"approved": "",
|
||||||
"accountsreceivable": "Accounts Receivable",
|
"accountsreceivable": "Accounts Receivable",
|
||||||
"act_price_ppc": "New Part Price",
|
"act_price_ppc": "New Part Price",
|
||||||
"actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).",
|
"actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).",
|
||||||
@@ -2015,9 +2031,10 @@
|
|||||||
"stands": "Stands",
|
"stands": "Stands",
|
||||||
"waived": "Waived"
|
"waived": "Waived"
|
||||||
},
|
},
|
||||||
"deleteconfirm": "Are you sure you want to delete this Job? This cannot be undone. ",
|
"deleteconfirm": "Are you sure you want to delete this Job? This cannot be undone.",
|
||||||
"deletedelivery": "Delete Delivery Checklist",
|
"deletedelivery": "Delete Delivery Checklist",
|
||||||
"deleteintake": "Delete Intake Checklist",
|
"deleteintake": "Delete Intake Checklist",
|
||||||
|
"deletewatchers": "Remove Watchers before deleting this Job.",
|
||||||
"deliverchecklist": "Deliver Checklist",
|
"deliverchecklist": "Deliver Checklist",
|
||||||
"difference": "Difference",
|
"difference": "Difference",
|
||||||
"diskscan": "Scan Disk for Estimates",
|
"diskscan": "Scan Disk for Estimates",
|
||||||
@@ -2285,8 +2302,10 @@
|
|||||||
"productionlist": "Production Board - List",
|
"productionlist": "Production Board - List",
|
||||||
"readyjobs": "Ready Jobs",
|
"readyjobs": "Ready Jobs",
|
||||||
"recent": "Recent Items",
|
"recent": "Recent Items",
|
||||||
|
"remoteassist": "Remote Assist",
|
||||||
"reportcenter": "Report Center",
|
"reportcenter": "Report Center",
|
||||||
"rescueme": "Rescue me!",
|
"rescueme": "Rescue Me!",
|
||||||
|
"rescuemezoho": "Remote Me In!",
|
||||||
"schedule": "Schedule",
|
"schedule": "Schedule",
|
||||||
"scoreboard": "Scoreboard",
|
"scoreboard": "Scoreboard",
|
||||||
"search": {
|
"search": {
|
||||||
@@ -2317,8 +2336,8 @@
|
|||||||
"duplicate": "Duplicate this Job",
|
"duplicate": "Duplicate this Job",
|
||||||
"duplicatenolines": "Duplicate this Job without Repair Data",
|
"duplicatenolines": "Duplicate this Job without Repair Data",
|
||||||
"newcccontract": "Create Courtesy Car Contract",
|
"newcccontract": "Create Courtesy Car Contract",
|
||||||
"void": "Void Job",
|
"submit-for-testing": "Submit for Testing",
|
||||||
"submit-for-testing": "Submit for Testing"
|
"void": "Void Job"
|
||||||
},
|
},
|
||||||
"jobsdetail": {
|
"jobsdetail": {
|
||||||
"claimdetail": "Claim Details",
|
"claimdetail": "Claim Details",
|
||||||
@@ -2361,7 +2380,8 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
|
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
|
||||||
"noattachedjobs": "No Jobs have been associated to this conversation. ",
|
"noattachedjobs": "No Jobs have been associated to this conversation. ",
|
||||||
"updatinglabel": "Error updating label. {{error}}"
|
"updatinglabel": "Error updating label. {{error}}",
|
||||||
|
"no_consent": "This phone number has opted-out of Messaging."
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addlabel": "Add a label to this conversation.",
|
"addlabel": "Add a label to this conversation.",
|
||||||
@@ -2377,7 +2397,8 @@
|
|||||||
"selectmedia": "Select Media",
|
"selectmedia": "Select Media",
|
||||||
"sentby": "Sent by {{by}} at {{time}}",
|
"sentby": "Sent by {{by}} at {{time}}",
|
||||||
"typeamessage": "Send a message...",
|
"typeamessage": "Send a message...",
|
||||||
"unarchive": "Unarchive"
|
"unarchive": "Unarchive",
|
||||||
|
"no_consent": "Opted-out"
|
||||||
},
|
},
|
||||||
"render": {
|
"render": {
|
||||||
"conversation_list": "Conversation List"
|
"conversation_list": "Conversation List"
|
||||||
@@ -2424,6 +2445,66 @@
|
|||||||
"updated": "Note updated successfully."
|
"updated": "Note updated successfully."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"actions": {
|
||||||
|
"remove": "Remove"
|
||||||
|
},
|
||||||
|
"aria": {
|
||||||
|
"toggle": "Toggle Watching Job"
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"app": "App",
|
||||||
|
"email": "Email",
|
||||||
|
"fcm": "Push"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"auto-add": "Automatically watch Jobs I import",
|
||||||
|
"auto-add-success": "Auto watcher status successfully changed.",
|
||||||
|
"auto-add-failure": "Something went wrong updating your auto watcher status.",
|
||||||
|
"add-watchers": "Add Watchers",
|
||||||
|
"add-watchers-team": "Add Team Members",
|
||||||
|
"employee-search": "Search for an Employee",
|
||||||
|
"mark-all-read": "Mark All Read",
|
||||||
|
"new-notification-title": "New Notification:",
|
||||||
|
"no-watchers": "No Watchers",
|
||||||
|
"notification-center": "Notification Center",
|
||||||
|
"notification-popup-title": "Changes for Job #{{ro_number}}",
|
||||||
|
"notification-settings-failure": "Error saving Notification Settings. {{error}}",
|
||||||
|
"notification-settings-success": "Notification Settings saved successfully.",
|
||||||
|
"notificationscenarios": "Job Notification Scenarios",
|
||||||
|
"ro-number": "RO #{{ro_number}}",
|
||||||
|
"save": "Save Scenarios",
|
||||||
|
"scenario": "Scenario",
|
||||||
|
"show-unread-only": "Show Unread Only",
|
||||||
|
"teams-search": "Search for a Team",
|
||||||
|
"unwatch": "Unwatch",
|
||||||
|
"watch": "Watch",
|
||||||
|
"watching-issue": "Watching",
|
||||||
|
"employee-notification": "Notifications are disabled because you do not have an associated Employee record."
|
||||||
|
},
|
||||||
|
"scenarios": {
|
||||||
|
"alternate-transport-changed": "Alternate Transport Changed",
|
||||||
|
"bill-posted": "Bill Posted",
|
||||||
|
"critical-parts-status-changed": "Critical Parts Status Changed",
|
||||||
|
"intake-delivery-checklist-completed": "Intake or Delivery Checklist Completed",
|
||||||
|
"job-added-to-production": "Job Added to Production",
|
||||||
|
"job-assigned-to-me": "Job Assigned to Me",
|
||||||
|
"job-status-change": "Job Status Changed",
|
||||||
|
"new-media-added-reassigned": "New Media Added or Reassigned",
|
||||||
|
"new-note-added": "New Note Added",
|
||||||
|
"new-time-ticket-posted": "New Time Ticket Posted",
|
||||||
|
"part-marked-back-ordered": "Part Marked Back Ordered",
|
||||||
|
"payment-collected-completed": "Payment Collected / Completed",
|
||||||
|
"schedule-dates-changed": "Schedule Dates Changed",
|
||||||
|
"supplement-imported": "Supplement Imported",
|
||||||
|
"tasks-updated-created": "Tasks Updated / Created"
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"job-watchers": "Job Watchers",
|
||||||
|
"not-employee": "You need to be an employee to watch this job. Reach out to your admin to get set up!",
|
||||||
|
"not-employee-notifications": "You must be an employee to receive notifications"
|
||||||
|
}
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"labels": {
|
"labels": {
|
||||||
"noownerinfo": "No owner information."
|
"noownerinfo": "No owner information."
|
||||||
@@ -2442,7 +2523,6 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"accountingid": "Accounting ID",
|
"accountingid": "Accounting ID",
|
||||||
"address": "Address",
|
"address": "Address",
|
||||||
"allow_text_message": "Permission to Text?",
|
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"note": "Owner Note",
|
"note": "Owner Note",
|
||||||
"ownr_addr1": "Address",
|
"ownr_addr1": "Address",
|
||||||
@@ -3023,6 +3103,7 @@
|
|||||||
"credits_not_received_date_vendorid": "Credits not Received by Vendor",
|
"credits_not_received_date_vendorid": "Credits not Received by Vendor",
|
||||||
"csi": "CSI Responses",
|
"csi": "CSI Responses",
|
||||||
"customer_list": "Customer List",
|
"customer_list": "Customer List",
|
||||||
|
"customer_list_excel": "Customer List - Excel",
|
||||||
"cycle_time_analysis": "Cycle Time Analysis",
|
"cycle_time_analysis": "Cycle Time Analysis",
|
||||||
"estimates_written_converted": "Estimates Written/Converted",
|
"estimates_written_converted": "Estimates Written/Converted",
|
||||||
"estimator_detail": "Jobs by Estimator (Detail)",
|
"estimator_detail": "Jobs by Estimator (Detail)",
|
||||||
@@ -3420,6 +3501,7 @@
|
|||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"dms": "DMS Export",
|
"dms": "DMS Export",
|
||||||
"export-logs": "Export Logs",
|
"export-logs": "Export Logs",
|
||||||
|
"feature-request": "Feature Requet",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
"jobs": "Jobs",
|
"jobs": "Jobs",
|
||||||
"jobs-active": "Active Jobs",
|
"jobs-active": "Active Jobs",
|
||||||
@@ -3464,6 +3546,7 @@
|
|||||||
"dashboard": "Dashboard | {{app}}",
|
"dashboard": "Dashboard | {{app}}",
|
||||||
"dms": "DMS Export | {{app}}",
|
"dms": "DMS Export | {{app}}",
|
||||||
"export-logs": "Export Logs | {{app}}",
|
"export-logs": "Export Logs | {{app}}",
|
||||||
|
"feature-request": "Feature Request | {{app}}",
|
||||||
"imexonline": "ImEX Online",
|
"imexonline": "ImEX Online",
|
||||||
"inventory": "Inventory | {{app}}",
|
"inventory": "Inventory | {{app}}",
|
||||||
"jobs": "Active Jobs | {{app}}",
|
"jobs": "Active Jobs | {{app}}",
|
||||||
@@ -3681,10 +3764,10 @@
|
|||||||
"users": {
|
"users": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"signinerror": {
|
"signinerror": {
|
||||||
|
"auth/invalid-email": "A user with this email does not exist.",
|
||||||
"auth/user-disabled": "User account disabled. ",
|
"auth/user-disabled": "User account disabled. ",
|
||||||
"auth/user-not-found": "A user with this email does not exist.",
|
"auth/user-not-found": "A user with this email does not exist.",
|
||||||
"auth/wrong-password": "The email and password combination you provided is incorrect.",
|
"auth/wrong-password": "The email and password combination you provided is incorrect."
|
||||||
"auth/invalid-email": "A user with this email does not exist."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3785,59 +3868,17 @@
|
|||||||
"unique_vendor_name": "You must enter a unique vendor name."
|
"unique_vendor_name": "You must enter a unique vendor name."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"consent": {
|
||||||
"labels": {
|
"phone_number": "Phone Number",
|
||||||
"notification-center": "Notification Center",
|
"associated_owners": "Associated Owners",
|
||||||
"scenario": "Scenario",
|
"created_at": "Opt-Out Date",
|
||||||
"notificationscenarios": "Job Notification Scenarios",
|
"no_owners": "No Associated Owners",
|
||||||
"save": "Save Scenarios",
|
"phone_1": "Phone 1",
|
||||||
"watching-issue": "Watching",
|
"phone_2": "Phone 2",
|
||||||
"add-watchers": "Add Watchers",
|
"text_body": "Users can opt out of receiving SMS messages by replying with keywords such as STOP, UNSUBSCRIBE, CANCEL, END, QUIT, STOPALL, REVOKE and OPTOUT. To opt back in, users can reply with START, YES, or UNSTOP. Even after opting out, users can still send messages to us, which will be received and processed as needed. Ensure customers are informed to reply with these keywords to manage their messaging preferences. After opting out, users receive a confirmation message and will not receive further messages until they opt back in."
|
||||||
"employee-search": "Search for an Employee",
|
},
|
||||||
"teams-search": "Search for a Team",
|
"settings": {
|
||||||
"add-watchers-team": "Add Team Members",
|
"title": "Phone Number Opt-Out List"
|
||||||
"new-notification-title": "New Notification:",
|
|
||||||
"show-unread-only": "Show Unread Only",
|
|
||||||
"mark-all-read": "Mark All Read",
|
|
||||||
"notification-popup-title": "Changes for Job #{{ro_number}}",
|
|
||||||
"ro-number": "RO #{{ro_number}}",
|
|
||||||
"no-watchers": "No Watchers",
|
|
||||||
"notification-settings-success": "Notification Settings saved successfully.",
|
|
||||||
"notification-settings-failure": "Error saving Notification Settings. {{error}}",
|
|
||||||
"watch": "Watch",
|
|
||||||
"unwatch": "Unwatch"
|
|
||||||
},
|
|
||||||
"actions": {
|
|
||||||
"remove": "Remove"
|
|
||||||
},
|
|
||||||
"aria": {
|
|
||||||
"toggle": "Toggle Watching Job"
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"job-watchers": "Job Watchers"
|
|
||||||
},
|
|
||||||
"scenarios": {
|
|
||||||
"job-assigned-to-me": "Job Assigned to Me",
|
|
||||||
"bill-posted": "Bill Posted",
|
|
||||||
"critical-parts-status-changed": "Critical Parts Status Changed",
|
|
||||||
"part-marked-back-ordered": "Part Marked Back Ordered",
|
|
||||||
"new-note-added": "New Note Added",
|
|
||||||
"supplement-imported": "Supplement Imported",
|
|
||||||
"schedule-dates-changed": "Schedule Dates Changed",
|
|
||||||
"tasks-updated-created": "Tasks Updated / Created",
|
|
||||||
"new-media-added-reassigned": "New Media Added or Reassigned",
|
|
||||||
"new-time-ticket-posted": "New Time Ticket Posted",
|
|
||||||
"intake-delivery-checklist-completed": "Intake or Delivery Checklist Completed",
|
|
||||||
"job-added-to-production": "Job Added to Production",
|
|
||||||
"job-status-change": "Job Status Changed",
|
|
||||||
"payment-collected-completed": "Payment Collected / Completed",
|
|
||||||
"alternate-transport-changed": "Alternate Transport Changed"
|
|
||||||
},
|
|
||||||
"channels": {
|
|
||||||
"app": "App",
|
|
||||||
"email": "Email",
|
|
||||||
"fcm": "Push"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,6 @@
|
|||||||
"intellipay_config": {
|
"intellipay_config": {
|
||||||
"cash_discount_percentage": "",
|
"cash_discount_percentage": "",
|
||||||
"enable_cash_discount": "",
|
"enable_cash_discount": "",
|
||||||
"payment_type": "",
|
|
||||||
"payment_map": {
|
"payment_map": {
|
||||||
"amex": "American Express",
|
"amex": "American Express",
|
||||||
"disc": "Discover",
|
"disc": "Discover",
|
||||||
@@ -344,7 +343,8 @@
|
|||||||
"jcb": "JCB",
|
"jcb": "JCB",
|
||||||
"mast": "MasterCard",
|
"mast": "MasterCard",
|
||||||
"visa": "Visa"
|
"visa": "Visa"
|
||||||
}
|
},
|
||||||
|
"payment_type": ""
|
||||||
},
|
},
|
||||||
"invoice_federal_tax_rate": "",
|
"invoice_federal_tax_rate": "",
|
||||||
"invoice_local_tax_rate": "",
|
"invoice_local_tax_rate": "",
|
||||||
@@ -648,9 +648,15 @@
|
|||||||
"use_paint_scale_data": "",
|
"use_paint_scale_data": "",
|
||||||
"uselocalmediaserver": "",
|
"uselocalmediaserver": "",
|
||||||
"website": "",
|
"website": "",
|
||||||
"zip_post": ""
|
"zip_post": "",
|
||||||
|
"notifications": {
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "",
|
||||||
|
"invalid_followers": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"consent_settings": "",
|
||||||
"2tiername": "",
|
"2tiername": "",
|
||||||
"2tiersetup": "",
|
"2tiersetup": "",
|
||||||
"2tiersource": "",
|
"2tiersource": "",
|
||||||
@@ -728,7 +734,10 @@
|
|||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
"task-presets": "",
|
"task-presets": "",
|
||||||
"workingdays": ""
|
"workingdays": "",
|
||||||
|
"notifications": {
|
||||||
|
"followers": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
"contains": "",
|
"contains": "",
|
||||||
@@ -766,7 +775,6 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addtoproduction": "",
|
"addtoproduction": "",
|
||||||
"allow_text_message": "",
|
|
||||||
"checklist": "",
|
"checklist": "",
|
||||||
"printpack": "",
|
"printpack": "",
|
||||||
"removefromproduction": ""
|
"removefromproduction": ""
|
||||||
@@ -1236,6 +1244,7 @@
|
|||||||
"areyousure": "",
|
"areyousure": "",
|
||||||
"barcode": "código de barras",
|
"barcode": "código de barras",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
|
"changelog": "",
|
||||||
"clear": "",
|
"clear": "",
|
||||||
"confirmpassword": "",
|
"confirmpassword": "",
|
||||||
"created_at": "",
|
"created_at": "",
|
||||||
@@ -1245,6 +1254,7 @@
|
|||||||
"errors": "",
|
"errors": "",
|
||||||
"excel": "",
|
"excel": "",
|
||||||
"exceptiontitle": "",
|
"exceptiontitle": "",
|
||||||
|
"feature-request": "",
|
||||||
"friday": "",
|
"friday": "",
|
||||||
"globalsearch": "",
|
"globalsearch": "",
|
||||||
"help": "",
|
"help": "",
|
||||||
@@ -1323,9 +1333,9 @@
|
|||||||
"notfoundtitle": "",
|
"notfoundtitle": "",
|
||||||
"partnernotrunning": "",
|
"partnernotrunning": "",
|
||||||
"rbacunauth": "",
|
"rbacunauth": "",
|
||||||
|
"submit-for-testing": "",
|
||||||
"unsavedchanges": "Usted tiene cambios no guardados.",
|
"unsavedchanges": "Usted tiene cambios no guardados.",
|
||||||
"unsavedchangespopup": "",
|
"unsavedchangespopup": ""
|
||||||
"submit-for-testing": ""
|
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"dateRangeExceeded": "",
|
"dateRangeExceeded": "",
|
||||||
@@ -1632,10 +1642,13 @@
|
|||||||
"voiding": ""
|
"voiding": ""
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"estimate_sent_approval": "",
|
||||||
|
"estimate_approved": "",
|
||||||
"active_tasks": "",
|
"active_tasks": "",
|
||||||
"actual_completion": "Realización real",
|
"actual_completion": "Realización real",
|
||||||
"actual_delivery": "Entrega real",
|
"actual_delivery": "Entrega real",
|
||||||
"actual_in": "Real en",
|
"actual_in": "Real en",
|
||||||
|
"acv_amount": "",
|
||||||
"adjustment_bottom_line": "Ajustes",
|
"adjustment_bottom_line": "Ajustes",
|
||||||
"adjustmenthours": "",
|
"adjustmenthours": "",
|
||||||
"alt_transport": "",
|
"alt_transport": "",
|
||||||
@@ -1761,9 +1774,10 @@
|
|||||||
"est_ct_ln": "Apellido del tasador",
|
"est_ct_ln": "Apellido del tasador",
|
||||||
"est_ea": "Correo electrónico del tasador",
|
"est_ea": "Correo electrónico del tasador",
|
||||||
"est_ph1": "Número de teléfono del tasador",
|
"est_ph1": "Número de teléfono del tasador",
|
||||||
"flat_rate_ats": "",
|
|
||||||
"federal_tax_payable": "Impuesto federal por pagar",
|
"federal_tax_payable": "Impuesto federal por pagar",
|
||||||
"federal_tax_rate": "",
|
"federal_tax_rate": "",
|
||||||
|
"flat_rate_ats": "",
|
||||||
|
"hit_and_run": "",
|
||||||
"ins_addr1": "Dirección de Insurance Co.",
|
"ins_addr1": "Dirección de Insurance Co.",
|
||||||
"ins_city": "Ciudad de seguros",
|
"ins_city": "Ciudad de seguros",
|
||||||
"ins_co_id": "ID de la compañía de seguros",
|
"ins_co_id": "ID de la compañía de seguros",
|
||||||
@@ -1943,6 +1957,8 @@
|
|||||||
"scheddates": ""
|
"scheddates": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"sent": "",
|
||||||
|
"approved": "",
|
||||||
"accountsreceivable": "",
|
"accountsreceivable": "",
|
||||||
"act_price_ppc": "",
|
"act_price_ppc": "",
|
||||||
"actual_completion_inferred": "",
|
"actual_completion_inferred": "",
|
||||||
@@ -2018,6 +2034,7 @@
|
|||||||
"deleteconfirm": "",
|
"deleteconfirm": "",
|
||||||
"deletedelivery": "",
|
"deletedelivery": "",
|
||||||
"deleteintake": "",
|
"deleteintake": "",
|
||||||
|
"deletewatchers": "",
|
||||||
"deliverchecklist": "",
|
"deliverchecklist": "",
|
||||||
"difference": "",
|
"difference": "",
|
||||||
"diskscan": "",
|
"diskscan": "",
|
||||||
@@ -2285,8 +2302,10 @@
|
|||||||
"productionlist": "",
|
"productionlist": "",
|
||||||
"readyjobs": "",
|
"readyjobs": "",
|
||||||
"recent": "",
|
"recent": "",
|
||||||
|
"remoteassist": "",
|
||||||
"reportcenter": "",
|
"reportcenter": "",
|
||||||
"rescueme": "",
|
"rescueme": "",
|
||||||
|
"rescuemezoho": "",
|
||||||
"schedule": "Programar",
|
"schedule": "Programar",
|
||||||
"scoreboard": "",
|
"scoreboard": "",
|
||||||
"search": {
|
"search": {
|
||||||
@@ -2317,8 +2336,8 @@
|
|||||||
"duplicate": "",
|
"duplicate": "",
|
||||||
"duplicatenolines": "",
|
"duplicatenolines": "",
|
||||||
"newcccontract": "",
|
"newcccontract": "",
|
||||||
"void": "",
|
"submit-for-testing": "",
|
||||||
"submit-for-testing": ""
|
"void": ""
|
||||||
},
|
},
|
||||||
"jobsdetail": {
|
"jobsdetail": {
|
||||||
"claimdetail": "Detalles de la reclamación",
|
"claimdetail": "Detalles de la reclamación",
|
||||||
@@ -2361,7 +2380,8 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"invalidphone": "",
|
"invalidphone": "",
|
||||||
"noattachedjobs": "",
|
"noattachedjobs": "",
|
||||||
"updatinglabel": ""
|
"updatinglabel": "",
|
||||||
|
"no_consent": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addlabel": "",
|
"addlabel": "",
|
||||||
@@ -2377,7 +2397,8 @@
|
|||||||
"selectmedia": "",
|
"selectmedia": "",
|
||||||
"sentby": "",
|
"sentby": "",
|
||||||
"typeamessage": "Enviar un mensaje...",
|
"typeamessage": "Enviar un mensaje...",
|
||||||
"unarchive": ""
|
"unarchive": "",
|
||||||
|
"no_consent": ""
|
||||||
},
|
},
|
||||||
"render": {
|
"render": {
|
||||||
"conversation_list": ""
|
"conversation_list": ""
|
||||||
@@ -2424,6 +2445,68 @@
|
|||||||
"updated": "Nota actualizada con éxito."
|
"updated": "Nota actualizada con éxito."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"actions": {
|
||||||
|
"remove": ""
|
||||||
|
},
|
||||||
|
"aria": {
|
||||||
|
"toggle": ""
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"app": "",
|
||||||
|
"email": "",
|
||||||
|
"fcm": ""
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"auto-add-on": "",
|
||||||
|
"auto-add-off": "",
|
||||||
|
"auto-add-success": "",
|
||||||
|
"auto-add-failure": "",
|
||||||
|
"auto-add-description": "",
|
||||||
|
"add-watchers": "",
|
||||||
|
"add-watchers-team": "",
|
||||||
|
"employee-search": "",
|
||||||
|
"mark-all-read": "",
|
||||||
|
"new-notification-title": "",
|
||||||
|
"no-watchers": "",
|
||||||
|
"notification-center": "",
|
||||||
|
"notification-popup-title": "",
|
||||||
|
"notification-settings-failure": "",
|
||||||
|
"notification-settings-success": "",
|
||||||
|
"notificationscenarios": "",
|
||||||
|
"ro-number": "",
|
||||||
|
"save": "",
|
||||||
|
"scenario": "",
|
||||||
|
"show-unread-only": "",
|
||||||
|
"teams-search": "",
|
||||||
|
"unwatch": "",
|
||||||
|
"watch": "",
|
||||||
|
"watching-issue": "",
|
||||||
|
"employee-notification": ""
|
||||||
|
},
|
||||||
|
"scenarios": {
|
||||||
|
"alternate-transport-changed": "",
|
||||||
|
"bill-posted": "",
|
||||||
|
"critical-parts-status-changed": "",
|
||||||
|
"intake-delivery-checklist-completed": "",
|
||||||
|
"job-added-to-production": "",
|
||||||
|
"job-assigned-to-me": "",
|
||||||
|
"job-status-change": "",
|
||||||
|
"new-media-added-reassigned": "",
|
||||||
|
"new-note-added": "",
|
||||||
|
"new-time-ticket-posted": "",
|
||||||
|
"part-marked-back-ordered": "",
|
||||||
|
"payment-collected-completed": "",
|
||||||
|
"schedule-dates-changed": "",
|
||||||
|
"supplement-imported": "",
|
||||||
|
"tasks-updated-created": ""
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"job-watchers": "",
|
||||||
|
"not-employee": "",
|
||||||
|
"not-employee-notifications": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"labels": {
|
"labels": {
|
||||||
"noownerinfo": ""
|
"noownerinfo": ""
|
||||||
@@ -2442,7 +2525,6 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"accountingid": "",
|
"accountingid": "",
|
||||||
"address": "Dirección",
|
"address": "Dirección",
|
||||||
"allow_text_message": "Permiso de texto?",
|
|
||||||
"name": "Nombre",
|
"name": "Nombre",
|
||||||
"note": "",
|
"note": "",
|
||||||
"ownr_addr1": "Dirección",
|
"ownr_addr1": "Dirección",
|
||||||
@@ -3023,6 +3105,7 @@
|
|||||||
"credits_not_received_date_vendorid": "",
|
"credits_not_received_date_vendorid": "",
|
||||||
"csi": "",
|
"csi": "",
|
||||||
"customer_list": "",
|
"customer_list": "",
|
||||||
|
"customer_list_excel": "",
|
||||||
"cycle_time_analysis": "",
|
"cycle_time_analysis": "",
|
||||||
"estimates_written_converted": "",
|
"estimates_written_converted": "",
|
||||||
"estimator_detail": "",
|
"estimator_detail": "",
|
||||||
@@ -3420,6 +3503,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"dms": "",
|
"dms": "",
|
||||||
"export-logs": "",
|
"export-logs": "",
|
||||||
|
"feature-request": "",
|
||||||
"inventory": "",
|
"inventory": "",
|
||||||
"jobs": "",
|
"jobs": "",
|
||||||
"jobs-active": "",
|
"jobs-active": "",
|
||||||
@@ -3464,6 +3548,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"dms": "",
|
"dms": "",
|
||||||
"export-logs": "",
|
"export-logs": "",
|
||||||
|
"feature-request": "",
|
||||||
"imexonline": "",
|
"imexonline": "",
|
||||||
"inventory": "",
|
"inventory": "",
|
||||||
"jobs": "Todos los trabajos | {{app}}",
|
"jobs": "Todos los trabajos | {{app}}",
|
||||||
@@ -3681,10 +3766,10 @@
|
|||||||
"users": {
|
"users": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"signinerror": {
|
"signinerror": {
|
||||||
|
"auth/invalid-email": "",
|
||||||
"auth/user-disabled": "",
|
"auth/user-disabled": "",
|
||||||
"auth/user-not-found": "",
|
"auth/user-not-found": "",
|
||||||
"auth/wrong-password": "",
|
"auth/wrong-password": ""
|
||||||
"auth/invalid-email": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3785,59 +3870,17 @@
|
|||||||
"unique_vendor_name": ""
|
"unique_vendor_name": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"consent": {
|
||||||
"labels": {
|
"phone_number": "",
|
||||||
"notification-center": "",
|
"associated_owners": "",
|
||||||
"scenario": "",
|
"created_at": "",
|
||||||
"notificationscenarios": "",
|
"no_owners": "",
|
||||||
"save": "",
|
"phone_1": "",
|
||||||
"watching-issue": "",
|
"phone_2": "",
|
||||||
"add-watchers": "",
|
"text_body": ""
|
||||||
"employee-search": "",
|
},
|
||||||
"teams-search": "",
|
"settings": {
|
||||||
"add-watchers-team": "",
|
"title": ""
|
||||||
"new-notification-title": "",
|
|
||||||
"show-unread-only": "",
|
|
||||||
"mark-all-read": "",
|
|
||||||
"notification-popup-title": "",
|
|
||||||
"ro-number": "",
|
|
||||||
"no-watchers": "",
|
|
||||||
"notification-settings-success": "",
|
|
||||||
"notification-settings-failure": "",
|
|
||||||
"watch": "",
|
|
||||||
"unwatch": ""
|
|
||||||
},
|
|
||||||
"actions": {
|
|
||||||
"remove": ""
|
|
||||||
},
|
|
||||||
"aria": {
|
|
||||||
"toggle": ""
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"job-watchers": ""
|
|
||||||
},
|
|
||||||
"scenarios": {
|
|
||||||
"job-assigned-to-me": "",
|
|
||||||
"bill-posted": "",
|
|
||||||
"critical-parts-status-changed": "",
|
|
||||||
"part-marked-back-ordered": "",
|
|
||||||
"new-note-added": "",
|
|
||||||
"supplement-imported": "",
|
|
||||||
"schedule-dates-changed": "",
|
|
||||||
"tasks-updated-created": "",
|
|
||||||
"new-media-added-reassigned": "",
|
|
||||||
"new-time-ticket-posted": "",
|
|
||||||
"intake-delivery-checklist-completed": "",
|
|
||||||
"job-added-to-production": "",
|
|
||||||
"job-status-change": "",
|
|
||||||
"payment-collected-completed": "",
|
|
||||||
"alternate-transport-changed": ""
|
|
||||||
},
|
|
||||||
"channels": {
|
|
||||||
"app": "",
|
|
||||||
"email": "",
|
|
||||||
"fcm": ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,6 @@
|
|||||||
"intellipay_config": {
|
"intellipay_config": {
|
||||||
"cash_discount_percentage": "",
|
"cash_discount_percentage": "",
|
||||||
"enable_cash_discount": "",
|
"enable_cash_discount": "",
|
||||||
"payment_type": "",
|
|
||||||
"payment_map": {
|
"payment_map": {
|
||||||
"amex": "American Express",
|
"amex": "American Express",
|
||||||
"disc": "Discover",
|
"disc": "Discover",
|
||||||
@@ -344,7 +343,8 @@
|
|||||||
"jcb": "JCB",
|
"jcb": "JCB",
|
||||||
"mast": "MasterCard",
|
"mast": "MasterCard",
|
||||||
"visa": "Visa"
|
"visa": "Visa"
|
||||||
}
|
},
|
||||||
|
"payment_type": ""
|
||||||
},
|
},
|
||||||
"invoice_federal_tax_rate": "",
|
"invoice_federal_tax_rate": "",
|
||||||
"invoice_local_tax_rate": "",
|
"invoice_local_tax_rate": "",
|
||||||
@@ -648,9 +648,15 @@
|
|||||||
"use_paint_scale_data": "",
|
"use_paint_scale_data": "",
|
||||||
"uselocalmediaserver": "",
|
"uselocalmediaserver": "",
|
||||||
"website": "",
|
"website": "",
|
||||||
"zip_post": ""
|
"zip_post": "",
|
||||||
|
"notifications": {
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "",
|
||||||
|
"invalid_followers": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"consent_settings": "",
|
||||||
"2tiername": "",
|
"2tiername": "",
|
||||||
"2tiersetup": "",
|
"2tiersetup": "",
|
||||||
"2tiersource": "",
|
"2tiersource": "",
|
||||||
@@ -728,7 +734,10 @@
|
|||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
"task-presets": "",
|
"task-presets": "",
|
||||||
"workingdays": ""
|
"workingdays": "",
|
||||||
|
"notifications": {
|
||||||
|
"followers": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
"contains": "",
|
"contains": "",
|
||||||
@@ -766,7 +775,6 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addtoproduction": "",
|
"addtoproduction": "",
|
||||||
"allow_text_message": "",
|
|
||||||
"checklist": "",
|
"checklist": "",
|
||||||
"printpack": "",
|
"printpack": "",
|
||||||
"removefromproduction": ""
|
"removefromproduction": ""
|
||||||
@@ -1236,6 +1244,7 @@
|
|||||||
"areyousure": "",
|
"areyousure": "",
|
||||||
"barcode": "code à barre",
|
"barcode": "code à barre",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
|
"changelog": "",
|
||||||
"clear": "",
|
"clear": "",
|
||||||
"confirmpassword": "",
|
"confirmpassword": "",
|
||||||
"created_at": "",
|
"created_at": "",
|
||||||
@@ -1245,6 +1254,7 @@
|
|||||||
"errors": "",
|
"errors": "",
|
||||||
"excel": "",
|
"excel": "",
|
||||||
"exceptiontitle": "",
|
"exceptiontitle": "",
|
||||||
|
"feature-request": "",
|
||||||
"friday": "",
|
"friday": "",
|
||||||
"globalsearch": "",
|
"globalsearch": "",
|
||||||
"help": "",
|
"help": "",
|
||||||
@@ -1323,10 +1333,9 @@
|
|||||||
"notfoundtitle": "",
|
"notfoundtitle": "",
|
||||||
"partnernotrunning": "",
|
"partnernotrunning": "",
|
||||||
"rbacunauth": "",
|
"rbacunauth": "",
|
||||||
|
"submit-for-testing": "",
|
||||||
"unsavedchanges": "Vous avez des changements non enregistrés.",
|
"unsavedchanges": "Vous avez des changements non enregistrés.",
|
||||||
"unsavedchangespopup": "",
|
"unsavedchangespopup": ""
|
||||||
"submit-for-testing": ""
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"dateRangeExceeded": "",
|
"dateRangeExceeded": "",
|
||||||
@@ -1633,10 +1642,13 @@
|
|||||||
"voiding": ""
|
"voiding": ""
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"estimate_sent_approval": "",
|
||||||
|
"estimate_approved": "",
|
||||||
"active_tasks": "",
|
"active_tasks": "",
|
||||||
"actual_completion": "Achèvement réel",
|
"actual_completion": "Achèvement réel",
|
||||||
"actual_delivery": "Livraison réelle",
|
"actual_delivery": "Livraison réelle",
|
||||||
"actual_in": "En réel",
|
"actual_in": "En réel",
|
||||||
|
"acv_amount": "",
|
||||||
"adjustment_bottom_line": "Ajustements",
|
"adjustment_bottom_line": "Ajustements",
|
||||||
"adjustmenthours": "",
|
"adjustmenthours": "",
|
||||||
"alt_transport": "",
|
"alt_transport": "",
|
||||||
@@ -1762,9 +1774,10 @@
|
|||||||
"est_ct_ln": "Nom de l'évaluateur",
|
"est_ct_ln": "Nom de l'évaluateur",
|
||||||
"est_ea": "Courriel de l'évaluateur",
|
"est_ea": "Courriel de l'évaluateur",
|
||||||
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
||||||
"flat_rate_ats": "",
|
|
||||||
"federal_tax_payable": "Impôt fédéral à payer",
|
"federal_tax_payable": "Impôt fédéral à payer",
|
||||||
"federal_tax_rate": "",
|
"federal_tax_rate": "",
|
||||||
|
"flat_rate_ats": "",
|
||||||
|
"hit_and_run": "",
|
||||||
"ins_addr1": "Adresse Insurance Co.",
|
"ins_addr1": "Adresse Insurance Co.",
|
||||||
"ins_city": "Insurance City",
|
"ins_city": "Insurance City",
|
||||||
"ins_co_id": "ID de la compagnie d'assurance",
|
"ins_co_id": "ID de la compagnie d'assurance",
|
||||||
@@ -1944,6 +1957,8 @@
|
|||||||
"scheddates": ""
|
"scheddates": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"sent": "",
|
||||||
|
"approved": "",
|
||||||
"accountsreceivable": "",
|
"accountsreceivable": "",
|
||||||
"act_price_ppc": "",
|
"act_price_ppc": "",
|
||||||
"actual_completion_inferred": "",
|
"actual_completion_inferred": "",
|
||||||
@@ -2019,6 +2034,7 @@
|
|||||||
"deleteconfirm": "",
|
"deleteconfirm": "",
|
||||||
"deletedelivery": "",
|
"deletedelivery": "",
|
||||||
"deleteintake": "",
|
"deleteintake": "",
|
||||||
|
"deletewatchers": "",
|
||||||
"deliverchecklist": "",
|
"deliverchecklist": "",
|
||||||
"difference": "",
|
"difference": "",
|
||||||
"diskscan": "",
|
"diskscan": "",
|
||||||
@@ -2286,8 +2302,10 @@
|
|||||||
"productionlist": "",
|
"productionlist": "",
|
||||||
"readyjobs": "",
|
"readyjobs": "",
|
||||||
"recent": "",
|
"recent": "",
|
||||||
|
"remoteassist": "",
|
||||||
"reportcenter": "",
|
"reportcenter": "",
|
||||||
"rescueme": "",
|
"rescueme": "",
|
||||||
|
"rescuemezoho": "",
|
||||||
"schedule": "Programme",
|
"schedule": "Programme",
|
||||||
"scoreboard": "",
|
"scoreboard": "",
|
||||||
"search": {
|
"search": {
|
||||||
@@ -2318,8 +2336,8 @@
|
|||||||
"duplicate": "",
|
"duplicate": "",
|
||||||
"duplicatenolines": "",
|
"duplicatenolines": "",
|
||||||
"newcccontract": "",
|
"newcccontract": "",
|
||||||
"void": "",
|
"submit-for-testing": "",
|
||||||
"submit-for-testing": ""
|
"void": ""
|
||||||
},
|
},
|
||||||
"jobsdetail": {
|
"jobsdetail": {
|
||||||
"claimdetail": "Détails de la réclamation",
|
"claimdetail": "Détails de la réclamation",
|
||||||
@@ -2362,7 +2380,8 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"invalidphone": "",
|
"invalidphone": "",
|
||||||
"noattachedjobs": "",
|
"noattachedjobs": "",
|
||||||
"updatinglabel": ""
|
"updatinglabel": "",
|
||||||
|
"no_consent": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addlabel": "",
|
"addlabel": "",
|
||||||
@@ -2378,7 +2397,8 @@
|
|||||||
"selectmedia": "",
|
"selectmedia": "",
|
||||||
"sentby": "",
|
"sentby": "",
|
||||||
"typeamessage": "Envoyer un message...",
|
"typeamessage": "Envoyer un message...",
|
||||||
"unarchive": ""
|
"unarchive": "",
|
||||||
|
"no_consent": ""
|
||||||
},
|
},
|
||||||
"render": {
|
"render": {
|
||||||
"conversation_list": ""
|
"conversation_list": ""
|
||||||
@@ -2425,6 +2445,68 @@
|
|||||||
"updated": "Remarque mise à jour avec succès."
|
"updated": "Remarque mise à jour avec succès."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"actions": {
|
||||||
|
"remove": ""
|
||||||
|
},
|
||||||
|
"aria": {
|
||||||
|
"toggle": ""
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"app": "",
|
||||||
|
"email": "",
|
||||||
|
"fcm": ""
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"auto-add-on": "",
|
||||||
|
"auto-add-off": "",
|
||||||
|
"auto-add-success": "",
|
||||||
|
"auto-add-failure": "",
|
||||||
|
"auto-add-description": "",
|
||||||
|
"add-watchers": "",
|
||||||
|
"add-watchers-team": "",
|
||||||
|
"employee-search": "",
|
||||||
|
"mark-all-read": "",
|
||||||
|
"new-notification-title": "",
|
||||||
|
"no-watchers": "",
|
||||||
|
"notification-center": "",
|
||||||
|
"notification-popup-title": "",
|
||||||
|
"notification-settings-failure": "",
|
||||||
|
"notification-settings-success": "",
|
||||||
|
"notificationscenarios": "",
|
||||||
|
"ro-number": "",
|
||||||
|
"save": "",
|
||||||
|
"scenario": "",
|
||||||
|
"show-unread-only": "",
|
||||||
|
"teams-search": "",
|
||||||
|
"unwatch": "",
|
||||||
|
"watch": "",
|
||||||
|
"watching-issue": "",
|
||||||
|
"employee-notification": ""
|
||||||
|
},
|
||||||
|
"scenarios": {
|
||||||
|
"alternate-transport-changed": "",
|
||||||
|
"bill-posted": "",
|
||||||
|
"critical-parts-status-changed": "",
|
||||||
|
"intake-delivery-checklist-completed": "",
|
||||||
|
"job-added-to-production": "",
|
||||||
|
"job-assigned-to-me": "",
|
||||||
|
"job-status-change": "",
|
||||||
|
"new-media-added-reassigned": "",
|
||||||
|
"new-note-added": "",
|
||||||
|
"new-time-ticket-posted": "",
|
||||||
|
"part-marked-back-ordered": "",
|
||||||
|
"payment-collected-completed": "",
|
||||||
|
"schedule-dates-changed": "",
|
||||||
|
"supplement-imported": "",
|
||||||
|
"tasks-updated-created": ""
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"job-watchers": "",
|
||||||
|
"not-employee": "",
|
||||||
|
"not-employee-notifications": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"labels": {
|
"labels": {
|
||||||
"noownerinfo": ""
|
"noownerinfo": ""
|
||||||
@@ -2443,7 +2525,6 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"accountingid": "",
|
"accountingid": "",
|
||||||
"address": "Adresse",
|
"address": "Adresse",
|
||||||
"allow_text_message": "Autorisation de texte?",
|
|
||||||
"name": "Prénom",
|
"name": "Prénom",
|
||||||
"note": "",
|
"note": "",
|
||||||
"ownr_addr1": "Adresse",
|
"ownr_addr1": "Adresse",
|
||||||
@@ -3024,6 +3105,7 @@
|
|||||||
"credits_not_received_date_vendorid": "",
|
"credits_not_received_date_vendorid": "",
|
||||||
"csi": "",
|
"csi": "",
|
||||||
"customer_list": "",
|
"customer_list": "",
|
||||||
|
"customer_list_excel": "",
|
||||||
"cycle_time_analysis": "",
|
"cycle_time_analysis": "",
|
||||||
"estimates_written_converted": "",
|
"estimates_written_converted": "",
|
||||||
"estimator_detail": "",
|
"estimator_detail": "",
|
||||||
@@ -3421,6 +3503,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"dms": "",
|
"dms": "",
|
||||||
"export-logs": "",
|
"export-logs": "",
|
||||||
|
"feature-request": "",
|
||||||
"inventory": "",
|
"inventory": "",
|
||||||
"jobs": "",
|
"jobs": "",
|
||||||
"jobs-active": "",
|
"jobs-active": "",
|
||||||
@@ -3465,6 +3548,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"dms": "",
|
"dms": "",
|
||||||
"export-logs": "",
|
"export-logs": "",
|
||||||
|
"feature-request": "",
|
||||||
"imexonline": "",
|
"imexonline": "",
|
||||||
"inventory": "",
|
"inventory": "",
|
||||||
"jobs": "Tous les emplois | {{app}}",
|
"jobs": "Tous les emplois | {{app}}",
|
||||||
@@ -3682,10 +3766,10 @@
|
|||||||
"users": {
|
"users": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"signinerror": {
|
"signinerror": {
|
||||||
|
"auth/invalid-email": "",
|
||||||
"auth/user-disabled": "",
|
"auth/user-disabled": "",
|
||||||
"auth/user-not-found": "",
|
"auth/user-not-found": "",
|
||||||
"auth/wrong-password": "",
|
"auth/wrong-password": ""
|
||||||
"auth/invalid-email": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3786,59 +3870,17 @@
|
|||||||
"unique_vendor_name": ""
|
"unique_vendor_name": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"consent": {
|
||||||
"labels": {
|
"phone_number": "Phone Number",
|
||||||
"notification-center": "",
|
"associated_owners": "Associated Owners",
|
||||||
"scenario": "",
|
"created_at": "Opt-Out Date",
|
||||||
"notificationscenarios": "",
|
"no_owners": "No Associated Owners",
|
||||||
"save": "",
|
"phone_1": "Phone 1",
|
||||||
"watching-issue": "",
|
"phone_2": "Phone 2",
|
||||||
"add-watchers": "",
|
"text_body": ""
|
||||||
"employee-search": "",
|
},
|
||||||
"teams-search": "",
|
"settings": {
|
||||||
"add-watchers-team": "",
|
"title": ""
|
||||||
"new-notification-title": "",
|
|
||||||
"show-unread-only": "",
|
|
||||||
"mark-all-read": "",
|
|
||||||
"notification-popup-title": "",
|
|
||||||
"ro-number": "",
|
|
||||||
"no-watchers": "",
|
|
||||||
"notification-settings-success": "",
|
|
||||||
"notification-settings-failure": "",
|
|
||||||
"watch": "",
|
|
||||||
"unwatch": ""
|
|
||||||
},
|
|
||||||
"actions": {
|
|
||||||
"remove": ""
|
|
||||||
},
|
|
||||||
"aria": {
|
|
||||||
"toggle": ""
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"job-watchers": ""
|
|
||||||
},
|
|
||||||
"scenarios": {
|
|
||||||
"job-assigned-to-me": "",
|
|
||||||
"bill-posted": "",
|
|
||||||
"critical-parts-status-changed": "",
|
|
||||||
"part-marked-back-ordered": "",
|
|
||||||
"new-note-added": "",
|
|
||||||
"supplement-imported": "",
|
|
||||||
"schedule-dates-changed": "",
|
|
||||||
"tasks-updated-created": "",
|
|
||||||
"new-media-added-reassigned": "",
|
|
||||||
"new-time-ticket-posted": "",
|
|
||||||
"intake-delivery-checklist-completed": "",
|
|
||||||
"job-added-to-production": "",
|
|
||||||
"job-status-change": "",
|
|
||||||
"payment-collected-completed": "",
|
|
||||||
"alternate-transport-changed": ""
|
|
||||||
},
|
|
||||||
"channels": {
|
|
||||||
"app": "",
|
|
||||||
"email": "",
|
|
||||||
"fcm": ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Tooltip } from "antd";
|
import { Tooltip } from "antd";
|
||||||
import dayjs from "../utils/day";
|
import dayjs from "../utils/day";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export function DateFormatter(props) {
|
export function DateFormatter(props) {
|
||||||
return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : null;
|
return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : null;
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ const onServiceWorkerUpdate = (registration) => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
window.open(
|
window.open(
|
||||||
InstanceRenderManager({
|
`https://shopmanagement.canny.io/changelog`,
|
||||||
imex: "https://imex-online.noticeable.news/",
|
|
||||||
rome: "https://rome-online.noticeable.news/"
|
|
||||||
}),
|
|
||||||
"_blank"
|
"_blank"
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user