Compare commits
238 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4631f50e5 | ||
|
|
110fad2abc | ||
|
|
b7456cecd4 | ||
|
|
84db1fe81b | ||
|
|
b539111be8 | ||
|
|
8a8bc5a6ed | ||
|
|
020db91105 | ||
|
|
1dd28af752 | ||
|
|
5ba192eee0 | ||
|
|
8109a12898 | ||
|
|
2deb7fd520 | ||
|
|
f6cd136679 | ||
|
|
e50cb86296 | ||
|
|
a5a01c44fa | ||
|
|
947e0705e4 | ||
|
|
aa8a6a837d | ||
|
|
5db440fc9c | ||
|
|
c299b9376a | ||
|
|
e5d530ea3e | ||
|
|
6da9850946 | ||
|
|
f62609f60c | ||
|
|
b2d8c66e5b | ||
|
|
3c4ed3ba0c | ||
|
|
2e7f827c3f | ||
|
|
dc82b39dc8 | ||
|
|
bdb741caf8 | ||
|
|
f50b198c21 | ||
|
|
3495326de3 | ||
|
|
b5973085e7 | ||
|
|
8687214420 | ||
|
|
d61b89a1e5 | ||
|
|
468b42abd2 | ||
|
|
fc03e5f983 | ||
|
|
c4742e38ea | ||
|
|
99e1adbe13 | ||
|
|
eb5c797a43 | ||
|
|
0595c5545e | ||
|
|
55944257aa | ||
|
|
03241778fa | ||
|
|
555b81fb14 | ||
|
|
a56b720e09 | ||
|
|
b89eede164 | ||
|
|
c21cc8d6b9 | ||
|
|
d02a6bc197 | ||
|
|
360c1ce82d | ||
|
|
a7ef02976c | ||
|
|
6a9e36ea4d | ||
|
|
5ebca3ff06 | ||
|
|
37d4c0a40f | ||
|
|
1969a92226 | ||
|
|
8840ffc9ba | ||
|
|
19e42ef397 | ||
|
|
c7eb026986 | ||
|
|
b0dcd3618e | ||
|
|
5f23f135f2 | ||
|
|
159ee7364d | ||
|
|
aa6ad109c9 | ||
|
|
f2a896d568 | ||
|
|
546ebba0bd | ||
|
|
0e75f54d6e | ||
|
|
30f34a17ea | ||
|
|
6035d94404 | ||
|
|
0b7a23d555 | ||
|
|
91fe1f4af9 | ||
|
|
f09cb7b247 | ||
|
|
35a7222f5e | ||
|
|
d444821cf7 | ||
|
|
b5cb520944 | ||
|
|
6814a3bc33 | ||
|
|
19c2b19abc | ||
|
|
22b011139d | ||
|
|
5b30daefe5 | ||
|
|
e015d3574a | ||
|
|
60140902d4 | ||
|
|
84f41b2c11 | ||
|
|
e8b9fcbc6e | ||
|
|
5adf591670 | ||
|
|
f55764e859 | ||
|
|
282fa787a9 | ||
|
|
037efff81c | ||
|
|
e26eb17d09 | ||
|
|
fbea9fde27 | ||
|
|
ce7cf6bdbe | ||
|
|
2c47e5d852 | ||
|
|
a6f809b20a | ||
|
|
2bcad68351 | ||
|
|
6b1b393804 | ||
|
|
c5181d1c5d | ||
|
|
e33ff2a45d | ||
|
|
9eb77964db | ||
|
|
0a68d2791d | ||
|
|
11928d9a7e | ||
|
|
c169bb5d5d | ||
|
|
3cc4f1c63e | ||
|
|
5237b1d535 | ||
|
|
cd56c50cf9 | ||
|
|
a18ce18d72 | ||
|
|
3691d32aaa | ||
|
|
5f66488410 | ||
|
|
d1be7f6e09 | ||
|
|
44f02f28a6 | ||
|
|
6d33622b4e | ||
|
|
f8b8e23ef4 | ||
|
|
db09d09428 | ||
|
|
451820a67c | ||
|
|
ba0ce5027e | ||
|
|
f777d26cc1 | ||
|
|
1463037878 | ||
|
|
7ddec0bb0f | ||
|
|
51c2d3351a | ||
|
|
8323fa6696 | ||
|
|
27a3932c08 | ||
|
|
add88659a4 | ||
|
|
320ad065d0 | ||
|
|
a9bc51949a | ||
|
|
39d1397221 | ||
|
|
b44b71072f | ||
|
|
f3e2a83bab | ||
|
|
0ef030bb89 | ||
|
|
3e9e6baf32 | ||
|
|
c03d45b3fc | ||
|
|
0a9b583c4b | ||
|
|
54ac0c84a7 | ||
|
|
4d59798d8d | ||
|
|
f95dab544d | ||
|
|
41e43dda96 | ||
|
|
cec60db78c | ||
|
|
7e741e4af9 | ||
|
|
24d47ae1c5 | ||
|
|
f556d59ad7 | ||
|
|
09c4662436 | ||
|
|
9bf6ba9cf0 | ||
|
|
7843ca9b1a | ||
|
|
c78b9866a3 | ||
|
|
c8701aba63 | ||
|
|
09c1a8ae35 | ||
|
|
0ef2814de3 | ||
|
|
8e105f0b36 | ||
|
|
ba4da3e35c | ||
|
|
1b8be56c15 | ||
|
|
f6e65f82e5 | ||
|
|
8b7bb099f3 | ||
|
|
2b26db78eb | ||
|
|
663d91b648 | ||
|
|
2a7686ec75 | ||
|
|
549cb56cdf | ||
|
|
146bb6c5c0 | ||
|
|
67b6da7c31 | ||
|
|
c2d96922c8 | ||
|
|
624894621b | ||
|
|
3fba215266 | ||
|
|
bbf291e8f3 | ||
|
|
70b4ec7948 | ||
|
|
341fc09c22 | ||
|
|
a3ec364034 | ||
|
|
fb30529808 | ||
|
|
e1728b275b | ||
|
|
46999145fc | ||
|
|
9d1f810af2 | ||
|
|
10d55df461 | ||
|
|
b9693aae95 | ||
|
|
02f5f1985c | ||
|
|
37edceee84 | ||
|
|
76a855990d | ||
|
|
517eca0900 | ||
|
|
ba9b248b1f | ||
|
|
0cd1e3ae98 | ||
|
|
1fd63012b0 | ||
|
|
cf084fa168 | ||
|
|
96af289640 | ||
|
|
f8df351de6 | ||
|
|
b8c096f4ff | ||
|
|
93ad23b615 | ||
|
|
0a918535bb | ||
|
|
4863b16b5f | ||
|
|
a27f5e2153 | ||
|
|
3ffea50072 | ||
|
|
34af7d3880 | ||
|
|
4432721c27 | ||
|
|
65ad4d9426 | ||
|
|
18924b4f08 | ||
|
|
c524f5f0e0 | ||
|
|
2fbac78eec | ||
|
|
4734971d48 | ||
|
|
fc1055c644 | ||
|
|
24798390b5 | ||
|
|
a992dead04 | ||
|
|
f039cd8d0d | ||
|
|
494e691230 | ||
|
|
4cc7366290 | ||
|
|
fd9d660a61 | ||
|
|
0b5bd4f718 | ||
|
|
7511b42bd4 | ||
|
|
26f94c4d5b | ||
|
|
0810798d30 | ||
|
|
aa55f4840b | ||
|
|
2810428d19 | ||
|
|
83da64f96b | ||
|
|
1f8d027f97 | ||
|
|
2f8ba20a5b | ||
|
|
b525f920e0 | ||
|
|
91fe6745fe | ||
|
|
b9073fe3f5 | ||
|
|
787366b231 | ||
|
|
2c95b49ae1 | ||
|
|
ace0039429 | ||
|
|
f13a70a22f | ||
|
|
fa29bd609f | ||
|
|
38681158c1 | ||
|
|
25b289b65d | ||
|
|
66671385d0 | ||
|
|
5adb54f5cb | ||
|
|
9bde06e110 | ||
|
|
30449ca113 | ||
|
|
0405d19f98 | ||
|
|
2c5310403b | ||
|
|
e2ef4f1caf | ||
|
|
b32a2d4d86 | ||
|
|
7c92484ae0 | ||
|
|
b069b6bc4c | ||
|
|
fbb473941c | ||
|
|
4d2d9500ff | ||
|
|
67cada5d8e | ||
|
|
47fe1959b1 | ||
|
|
e8ee2a9416 | ||
|
|
4bf68b637f | ||
|
|
b40c433865 | ||
|
|
55ed499ab5 | ||
|
|
353bc3bc05 | ||
|
|
df5c96345c | ||
|
|
2c7c187c45 | ||
|
|
9efaa55235 | ||
|
|
3a5a78d60a | ||
|
|
6dd2871c07 | ||
|
|
ef36ab9da0 | ||
|
|
a917f6bcdf | ||
|
|
c5d6457146 | ||
|
|
f3831e934f |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -121,3 +121,12 @@ logs/oAuthClient-log.log
|
||||
/*.env.*
|
||||
.idea/*
|
||||
.idea
|
||||
|
||||
# Vitest
|
||||
vitest-report*/
|
||||
vitest-coverage/
|
||||
*.vitest.log
|
||||
test-output.txt
|
||||
server/job/test/fixtures
|
||||
|
||||
.github
|
||||
|
||||
@@ -56,4 +56,5 @@ COPY . .
|
||||
EXPOSE 4000 9229
|
||||
|
||||
# Start the application
|
||||
CMD ["nodemon", "--legacy-watch", "--inspect=0.0.0.0:9229", "server.js"]
|
||||
RUN echo "Starting the application..."
|
||||
CMD ["nodemon", "--ignore", "./server/job/test/fixtures", "--legacy-watch", "--inspect=0.0.0.0:9229", "server.js"]
|
||||
|
||||
764
_reference/localEmailViewer/package-lock.json
generated
764
_reference/localEmailViewer/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,8 @@
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"express": "^4.21.1",
|
||||
"mailparser": "^3.7.1",
|
||||
"express": "^5.1.0",
|
||||
"mailparser": "^3.7.2",
|
||||
"node-fetch": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,3 +12,5 @@ VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||
VITE_APP_REPORTS_SERVER_URL=https://reports.test.imex.online
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_INSTANCE=IMEX
|
||||
TEST_USERNAME="test@imex.dev"
|
||||
TEST_PASSWORD="test123"
|
||||
|
||||
@@ -14,3 +14,5 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.test.romeonline.io
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_COUNTRY=USA
|
||||
VITE_APP_INSTANCE=ROME
|
||||
TEST_USERNAME="test@imex.dev"
|
||||
TEST_PASSWORD="test123"
|
||||
|
||||
11
client/.gitignore
vendored
11
client/.gitignore
vendored
@@ -1,3 +1,14 @@
|
||||
# Vitest
|
||||
vitest-report*/
|
||||
vitest-coverage/
|
||||
*.vitest.log
|
||||
test-output.txt
|
||||
|
||||
# Playwright
|
||||
playwright-report/
|
||||
test-results/
|
||||
playwright/.cache/
|
||||
*.playwright.log
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
|
||||
@@ -74,50 +74,8 @@
|
||||
})();
|
||||
</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 || []);
|
||||
<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 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>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
4080
client/package-lock.json
generated
4080
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,24 +8,23 @@
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@ant-design/pro-layout": "^7.22.3",
|
||||
"@apollo/client": "^3.13.5",
|
||||
"@ant-design/pro-layout": "^7.22.4",
|
||||
"@apollo/client": "^3.13.6",
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||
"@firebase/analytics": "^0.10.12",
|
||||
"@firebase/app": "^0.11.3",
|
||||
"@firebase/auth": "^1.9.1",
|
||||
"@firebase/firestore": "^4.7.10",
|
||||
"@firebase/messaging": "^0.12.17",
|
||||
"@firebase/analytics": "^0.10.13",
|
||||
"@firebase/app": "^0.12.1",
|
||||
"@firebase/auth": "^1.10.2",
|
||||
"@firebase/firestore": "^4.7.12",
|
||||
"@firebase/messaging": "^0.12.18",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.6.1",
|
||||
"@sentry/cli": "^2.42.4",
|
||||
"@sentry/react": "^9.9.0",
|
||||
"@sentry/vite-plugin": "^3.2.2",
|
||||
"@splitsoftware/splitio-react": "^2.0.1",
|
||||
"@reduxjs/toolkit": "^2.8.1",
|
||||
"@sentry/cli": "^2.45.0",
|
||||
"@sentry/react": "^9.17.0",
|
||||
"@sentry/vite-plugin": "^3.4.0",
|
||||
"@splitsoftware/splitio-react": "^2.1.1",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"antd": "^5.24.5",
|
||||
"antd": "^5.25.0",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^4.2.0",
|
||||
"autosize": "^6.0.1",
|
||||
@@ -38,18 +37,18 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"graphql": "^16.10.0",
|
||||
"graphql": "^16.11.0",
|
||||
"i18next": "^24.2.3",
|
||||
"i18next-browser-languagedetector": "^8.0.4",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.12.6",
|
||||
"libphonenumber-js": "^1.12.8",
|
||||
"logrocket": "^9.0.2",
|
||||
"markerjs2": "^2.32.4",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.1",
|
||||
"object-hash": "^3.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.1.1",
|
||||
"query-string": "^9.1.2",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.18.0",
|
||||
@@ -70,17 +69,17 @@
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.30.0",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-virtuoso": "^4.12.5",
|
||||
"recharts": "^2.15.0",
|
||||
"react-virtuoso": "^4.12.7",
|
||||
"recharts": "^2.15.2",
|
||||
"redux": "^5.0.1",
|
||||
"redux-actions": "^3.0.3",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.86.0",
|
||||
"sass": "^1.86.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"styled-components": "^6.1.16",
|
||||
"styled-components": "^6.1.18",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"use-memo-one": "^1.1.3",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
@@ -100,7 +99,14 @@
|
||||
"build:production:imex": "env-cmd -f .env.production.imex npm run build",
|
||||
"build:production:rome": "env-cmd -f .env.production.rome npm run build",
|
||||
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
||||
"eulaize": "node src/utils/eulaize.js"
|
||||
"eulaize": "node src/utils/eulaize.js",
|
||||
"test:unit": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e:imex": "playwright test --config playwright.config.js",
|
||||
"test:e2e:rome": "playwright test --config playwright.rome.config.js",
|
||||
"test:e2e:imex:headed": "playwright test --config playwright.config.js --headed",
|
||||
"test:e2e:rome:headed": "playwright test --config playwright.rome.config.js --headed",
|
||||
"test:e2e:report": "playwright show-report"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -123,30 +129,38 @@
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@dotenvx/dotenvx": "^1.39.0",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@dotenvx/dotenvx": "^1.43.0",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@eslint/js": "^9.23.0",
|
||||
"@sentry/webpack-plugin": "^3.2.2",
|
||||
"browserslist": "^4.24.4",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@sentry/webpack-plugin": "^3.4.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"browserslist": "^4.24.5",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"chalk": "^5.4.1",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"memfs": "^4.17.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"memfs": "^4.17.1",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.51.1",
|
||||
"react-error-overlay": "^6.1.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^6.2.3",
|
||||
"vite-plugin-babel": "^1.3.0",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-babel": "^1.3.1",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-pwa": "^0.21.2",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
"vitest": "^3.1.3",
|
||||
"workbox-window": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
25
client/playwright.config.js
Normal file
25
client/playwright.config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config({
|
||||
path: "./.env.development.imex",
|
||||
prefix: "TEST_"
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
testMatch: "*.e2e.js",
|
||||
timeout: 60 * 1000,
|
||||
reporter: [["list"], ["html"]],
|
||||
use: {
|
||||
baseURL: "https://localhost:3000",
|
||||
browser: "chromium",
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
webServer: {
|
||||
command: "npm run start:imex",
|
||||
ignoreHTTPSErrors: true,
|
||||
url: "https://localhost:3000/health", // Health check endpoint will tell us when the server is ready
|
||||
reuseExistingServer: !process.env.CI // Reuse server locally, not in CI
|
||||
}
|
||||
});
|
||||
25
client/playwright.rome.config.js
Normal file
25
client/playwright.rome.config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config({
|
||||
path: "./.env.development.rome",
|
||||
prefix: "TEST_"
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
testMatch: "*.e2e.js",
|
||||
timeout: 60 * 1000,
|
||||
reporter: [["list"], ["html"]],
|
||||
use: {
|
||||
baseURL: "https://localhost:3000",
|
||||
browser: "chromium",
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
webServer: {
|
||||
command: "npm run start:rome",
|
||||
ignoreHTTPSErrors: true,
|
||||
url: "https://localhost:3000/health", // Health check endpoint will tell us when the server is ready
|
||||
reuseExistingServer: !process.env.CI // Reuse server locally, not in CI
|
||||
}
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import client from "../utils/GraphQLClient";
|
||||
import App from "./App";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import themeProvider from "./themeProvider";
|
||||
import { CookiesProvider } from "react-cookie";
|
||||
|
||||
// Base Split configuration
|
||||
const config = {
|
||||
@@ -38,26 +39,28 @@ function AppContainer() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<ConfigProvider
|
||||
input={{ autoComplete: "new-password" }}
|
||||
locale={enLocale}
|
||||
theme={themeProvider}
|
||||
form={{
|
||||
validateMessages: {
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
required: t("general.validation.required", { label: "${label}" })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<GlobalLoadingBar />
|
||||
<SplitFactoryProvider config={config}>
|
||||
<SplitClientProvider>
|
||||
<App />
|
||||
</SplitClientProvider>
|
||||
</SplitFactoryProvider>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
<CookiesProvider>
|
||||
<ApolloProvider client={client}>
|
||||
<ConfigProvider
|
||||
input={{ autoComplete: "new-password" }}
|
||||
locale={enLocale}
|
||||
theme={themeProvider}
|
||||
form={{
|
||||
validateMessages: {
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
required: t("general.validation.required", { label: "${label}" })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<GlobalLoadingBar />
|
||||
<SplitFactoryProvider config={config}>
|
||||
<SplitClientProvider>
|
||||
<App />
|
||||
</SplitClientProvider>
|
||||
</SplitFactoryProvider>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
</CookiesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ import "./App.styles.scss";
|
||||
import Eula from "../components/eula/eula.component";
|
||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||
import { SocketProvider } from "../contexts/SocketIO/useSocket.jsx";
|
||||
import { NotificationProvider } from "../contexts/Notifications/notificationContext.jsx";
|
||||
import SocketProvider from "../contexts/SocketIO/socketProvider.jsx";
|
||||
|
||||
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||
@@ -142,11 +142,10 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
>
|
||||
<ProductFruitsWrapper
|
||||
currentUser={currentUser}
|
||||
workspaceCode={InstanceRenderMgr({
|
||||
imex: null,
|
||||
rome: "9BkbEseqNqxw8jUH"
|
||||
})}
|
||||
bodyshop={bodyshop}
|
||||
workspaceCode={bodyshop?.tours_enabled ? "9BkbEseqNqxw8jUH" : ""}
|
||||
/>
|
||||
|
||||
<NotificationProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import React from "react";
|
||||
import { ProductFruits } from "react-product-fruits";
|
||||
import PropTypes from "prop-types";
|
||||
import { ProductFruits } from "react-product-fruits";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const ProductFruitsWrapper = React.memo(({ currentUser, bodyshop, workspaceCode }) => {
|
||||
const featureProps = bodyshop?.features
|
||||
? Object.entries(bodyshop.features).reduce((acc, [key, value]) => {
|
||||
acc[key] = value === true || (typeof value === "string" && dayjs(value).isAfter(dayjs()));
|
||||
return acc;
|
||||
}, {})
|
||||
: {};
|
||||
|
||||
const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
|
||||
return (
|
||||
workspaceCode &&
|
||||
currentUser?.authorized === true &&
|
||||
@@ -14,7 +22,8 @@ const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
|
||||
language="en"
|
||||
user={{
|
||||
email: currentUser.email,
|
||||
username: currentUser.email
|
||||
username: currentUser.email,
|
||||
props: featureProps
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -28,5 +37,6 @@ ProductFruitsWrapper.propTypes = {
|
||||
authorized: PropTypes.bool,
|
||||
email: PropTypes.string
|
||||
}),
|
||||
workspaceCode: PropTypes.string
|
||||
workspaceCode: PropTypes.string,
|
||||
bodyshop: PropTypes.object
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Alert component should render Alert component 1`] = `ShallowWrapper {}`;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Alert from "./alert.component";
|
||||
|
||||
describe("Alert component", () => {
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
const mockProps = {
|
||||
type: "error",
|
||||
message: "Test error message."
|
||||
};
|
||||
|
||||
wrapper = shallow(<Alert {...mockProps} />);
|
||||
});
|
||||
|
||||
it("should render Alert component", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
31
client/src/components/alert/alert.component.test.jsx
Normal file
31
client/src/components/alert/alert.component.test.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import AlertComponent from "./alert.component";
|
||||
|
||||
describe("AlertComponent", () => {
|
||||
it("renders with default props", () => {
|
||||
render(<AlertComponent message="Default Alert" />);
|
||||
expect(screen.getByText("Default Alert")).toBeInTheDocument();
|
||||
expect(screen.getByRole("alert")).toHaveClass("ant-alert");
|
||||
});
|
||||
|
||||
it("applies type prop correctly", () => {
|
||||
render(<AlertComponent message="Success Alert" type="success" />);
|
||||
const alert = screen.getByRole("alert");
|
||||
expect(screen.getByText("Success Alert")).toBeInTheDocument();
|
||||
expect(alert).toHaveClass("ant-alert-success");
|
||||
});
|
||||
|
||||
it("displays description when provided", () => {
|
||||
render(<AlertComponent message="Error Alert" description="Something went wrong" type="error" />);
|
||||
expect(screen.getByText("Error Alert")).toBeInTheDocument();
|
||||
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
|
||||
expect(screen.getByRole("alert")).toHaveClass("ant-alert-error");
|
||||
});
|
||||
|
||||
it("is closable and shows icon when props are set", () => {
|
||||
render(<AlertComponent message="Warning Alert" type="warning" showIcon closable />);
|
||||
expect(screen.getByText("Warning Alert")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /close/i })).toBeInTheDocument(); // Close button
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AllocationsAssignmentComponent component should create an allocation on save 1`] = `ReactWrapper {}`;
|
||||
|
||||
exports[`AllocationsAssignmentComponent component should render AllocationsAssignmentComponent component 1`] = `ReactWrapper {}`;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import { MockBodyshop } from "../../utils/TestingHelpers";
|
||||
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
|
||||
|
||||
describe("AllocationsAssignmentComponent component", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockProps = {
|
||||
bodyshop: MockBodyshop,
|
||||
handleAssignment: jest.fn(),
|
||||
assignment: {},
|
||||
setAssignment: jest.fn(),
|
||||
visibilityState: [false, jest.fn()],
|
||||
maxHours: 4
|
||||
};
|
||||
|
||||
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
|
||||
});
|
||||
|
||||
it("should render AllocationsAssignmentComponent component", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render a list of employees", () => {
|
||||
const empList = wrapper.find("#employeeSelector");
|
||||
expect(empList.children()).to.have.lengthOf(2);
|
||||
});
|
||||
|
||||
it("should create an allocation on save", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -3,11 +3,11 @@ import { getToken } from "@firebase/messaging";
|
||||
import axios from "axios";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
||||
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
||||
import "./chat-affix.styles.scss";
|
||||
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Button } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -4,10 +4,10 @@ import { Link } from "react-router-dom";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -3,11 +3,11 @@ import axios from "axios";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ChatConversationComponent from "./chat-conversation.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Input, Spin, Tag, Tooltip } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { PictureFilled } from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Badge, Popover } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -9,6 +10,7 @@ import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-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 LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
|
||||
@@ -23,6 +25,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
||||
export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const {
|
||||
treatments: { Imgproxy }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Imgproxy"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||
fetchPolicy: "network-only",
|
||||
@@ -42,6 +51,10 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
||||
setSelectedMedia([]);
|
||||
}, [setSelectedMedia, conversation]);
|
||||
|
||||
//Knowingly taking on the technical debt of poor implementation below. Done this way to avoid an edge case where no component may be displayed.
|
||||
//Cloudinary will be removed once the migration is completed.
|
||||
//If Imageproxy is on, rely only on the LMS selector
|
||||
//If not on, use the old methods.
|
||||
const content = (
|
||||
<div>
|
||||
{loading && <LoadingSpinner />}
|
||||
@@ -49,17 +62,37 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
||||
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
||||
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
||||
) : null}
|
||||
{!bodyshop.uselocalmediaserver && data && (
|
||||
<JobDocumentsGalleryExternal
|
||||
data={data ? data.documents : []}
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
/>
|
||||
)}
|
||||
{bodyshop.uselocalmediaserver && open && (
|
||||
<JobDocumentsLocalGalleryExternal
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
||||
/>
|
||||
|
||||
{Imgproxy.treatment === "on" ? (
|
||||
<>
|
||||
{!bodyshop.uselocalmediaserver && (
|
||||
<JobsDocumentImgproxyGalleryExternal
|
||||
jobId={conversation.job_conversations[0].jobid}
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
/>
|
||||
)}
|
||||
{bodyshop.uselocalmediaserver && open && (
|
||||
<JobDocumentsLocalGalleryExternal
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!bodyshop.uselocalmediaserver && data && (
|
||||
<JobDocumentsGalleryExternal
|
||||
data={data ? data.documents : []}
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
/>
|
||||
)}
|
||||
{bodyshop.uselocalmediaserver && open && (
|
||||
<JobDocumentsLocalGalleryExternal
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
|
||||
@@ -7,7 +7,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
|
||||
@@ -12,9 +12,9 @@ import ChatConversationListComponent from "../chat-conversation-list/chat-conver
|
||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
|
||||
import "./chat-popup.styles.scss";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
|
||||
@@ -8,10 +8,10 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
||||
import ChatTagRo from "./chat-tag-ro.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { UploadOutlined } from "@ant-design/icons";
|
||||
import { Progress, Result, Space, Upload } from "antd";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import formatBytes from "../../utils/formatbytes";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||
import { handleUpload } from "./documents-upload-imgproxy.utility.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export function DocumentsUploadImgproxyComponent({
|
||||
children,
|
||||
currentUser,
|
||||
bodyshop,
|
||||
jobId,
|
||||
tagsArray,
|
||||
billId,
|
||||
callbackAfterUpload,
|
||||
totalSize,
|
||||
ignoreSizeLimit = false
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [fileList, setFileList] = useState([]);
|
||||
const notification = useNotification();
|
||||
|
||||
const pct = useMemo(() => {
|
||||
return parseInt((totalSize / ((bodyshop && bodyshop.jobsizelimit) || 1)) * 100);
|
||||
}, [bodyshop, totalSize]);
|
||||
|
||||
if (pct > 100 && !ignoreSizeLimit)
|
||||
return (
|
||||
<Result
|
||||
status="error"
|
||||
title={t("documents.labels.storageexceeded_title")}
|
||||
subTitle={t("documents.labels.storageexceeded")}
|
||||
/>
|
||||
);
|
||||
|
||||
const handleDone = (uid) => {
|
||||
setTimeout(() => {
|
||||
setFileList((fileList) => fileList.filter((x) => x.uid !== uid));
|
||||
}, 2000);
|
||||
};
|
||||
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
|
||||
|
||||
return (
|
||||
<Upload.Dragger
|
||||
multiple={true}
|
||||
fileList={fileList}
|
||||
disabled={!hasMediaAccess}
|
||||
onChange={(f) => {
|
||||
if (f.event && f.event.percent === 100) handleDone(f.file.uid);
|
||||
setFileList(f.fileList);
|
||||
}}
|
||||
beforeUpload={(file, fileList) => {
|
||||
if (ignoreSizeLimit) return true;
|
||||
const newFiles = fileList.reduce((acc, val) => acc + val.size, 0);
|
||||
const shouldStopUpload = (totalSize + newFiles) / ((bodyshop && bodyshop.jobsizelimit) || 1) >= 1;
|
||||
|
||||
//Check to see if old files plus newly uploaded ones will be too much.
|
||||
if (shouldStopUpload) {
|
||||
notification.error({
|
||||
key: "cannotuploaddocuments",
|
||||
message: t("documents.labels.upload_limitexceeded_title"),
|
||||
description: t("documents.labels.upload_limitexceeded")
|
||||
});
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
customRequest={(ev) =>
|
||||
handleUpload(
|
||||
ev,
|
||||
{
|
||||
bodyshop: bodyshop,
|
||||
uploaded_by: currentUser.email,
|
||||
jobId: jobId,
|
||||
billId: billId,
|
||||
tagsArray: tagsArray,
|
||||
callback: callbackAfterUpload
|
||||
},
|
||||
notification
|
||||
)
|
||||
}
|
||||
accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx"
|
||||
// showUploadList={false}
|
||||
>
|
||||
{children || (
|
||||
<>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<UploadOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">
|
||||
<LockWrapperComponent featureName="media">{t("documents.labels.dragtoupload")}</LockWrapperComponent>
|
||||
</p>
|
||||
{!ignoreSizeLimit && (
|
||||
<Space wrap className="ant-upload-text">
|
||||
<Progress type="dashboard" percent={pct} size="small" />
|
||||
<span>
|
||||
{t("documents.labels.usage", {
|
||||
percent: pct,
|
||||
used: formatBytes(totalSize),
|
||||
total: formatBytes(bodyshop && bodyshop.jobsizelimit)
|
||||
})}
|
||||
</span>
|
||||
</Space>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Upload.Dragger>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(DocumentsUploadImgproxyComponent);
|
||||
@@ -0,0 +1,172 @@
|
||||
import axios from "axios";
|
||||
import exifr from "exifr";
|
||||
import i18n from "i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
|
||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||
|
||||
//Required to prevent headers from getting set and rejected from Cloudinary.
|
||||
var cleanAxios = axios.create();
|
||||
cleanAxios.interceptors.request.eject(axiosAuthInterceptorId);
|
||||
|
||||
export const handleUpload = (ev, context, notification) => {
|
||||
logImEXEvent("document_upload", { filetype: ev.file?.type });
|
||||
|
||||
const { onError, onSuccess, onProgress } = ev;
|
||||
const { bodyshop, jobId } = context;
|
||||
|
||||
const fileName = ev.file?.name || ev.filename;
|
||||
|
||||
let extension = fileName.split(".").pop();
|
||||
let key = `${bodyshop.id}/${jobId}/${replaceAccents(fileName).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}`;
|
||||
|
||||
uploadToS3(key, extension, ev.file.type, ev.file, onError, onSuccess, onProgress, context, notification).catch(
|
||||
(error) => {
|
||||
console.error("Error uploading file to S3", error);
|
||||
notification.error({
|
||||
message: i18n.t("documents.errors.insert", {
|
||||
message: error.message
|
||||
})
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
//Handles only 1 file at a time.
|
||||
export const uploadToS3 = async (
|
||||
key,
|
||||
extension,
|
||||
fileType,
|
||||
file,
|
||||
onError,
|
||||
onSuccess,
|
||||
onProgress,
|
||||
context,
|
||||
notification
|
||||
) => {
|
||||
const { bodyshop, jobId, billId, uploaded_by, callback } = context;
|
||||
|
||||
//Get the signed url allowing us to PUT to S3.
|
||||
const signedURLResponse = await axios.post("/media/imgproxy/sign", {
|
||||
filenames: [key],
|
||||
bodyshopid: bodyshop.id,
|
||||
jobid: jobId
|
||||
});
|
||||
|
||||
if (signedURLResponse.status !== 200) {
|
||||
if (onError) onError(signedURLResponse.statusText);
|
||||
notification.error({
|
||||
message: i18n.t("documents.errors.getpresignurl", {
|
||||
message: signedURLResponse.statusText
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Key should be same as we provided to maintain backwards compatibility.
|
||||
const { presignedUrl: preSignedUploadUrlToS3, key: s3Key } = signedURLResponse.data.signedUrls[0];
|
||||
|
||||
const options = {
|
||||
onUploadProgress: (e) => {
|
||||
if (onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const s3UploadResponse = await cleanAxios.put(preSignedUploadUrlToS3, file, options);
|
||||
//Insert the document with the matching key.
|
||||
let takenat;
|
||||
if (fileType.includes("image")) {
|
||||
try {
|
||||
const exif = await exifr.parse(file);
|
||||
takenat = exif && exif.DateTimeOriginal;
|
||||
} catch (error) {
|
||||
console.log("Unable to parse image file for EXIF Data", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const documentInsert = await client.mutate({
|
||||
mutation: INSERT_NEW_DOCUMENT,
|
||||
variables: {
|
||||
docInput: [
|
||||
{
|
||||
...(jobId ? { jobid: jobId } : {}),
|
||||
...(billId ? { billid: billId } : {}),
|
||||
uploaded_by: uploaded_by,
|
||||
key: s3Key,
|
||||
type: fileType,
|
||||
extension: s3UploadResponse.data.format || extension,
|
||||
bodyshopid: bodyshop.id,
|
||||
size: s3UploadResponse.data.bytes || file.size, //Leftover from Cloudinary. We don't do any optimization on upload, so it will always be file.size.
|
||||
takenat
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
if (!documentInsert.errors) {
|
||||
if (onSuccess)
|
||||
onSuccess({
|
||||
uid: documentInsert.data.insert_documents.returning[0].id,
|
||||
name: documentInsert.data.insert_documents.returning[0].name,
|
||||
status: "done",
|
||||
key: documentInsert.data.insert_documents.returning[0].key
|
||||
});
|
||||
notification.success({
|
||||
key: "docuploadsuccess",
|
||||
message: i18n.t("documents.successes.insert")
|
||||
});
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
if (onError) onError(JSON.stringify(documentInsert.errors));
|
||||
notification.error({
|
||||
message: i18n.t("documents.errors.insert", {
|
||||
message: JSON.stringify(documentInsert.errors)
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error uploading file to S3", error.message, error.stack);
|
||||
notification.error({
|
||||
message: i18n.t("documents.errors.insert", {
|
||||
message: error.message
|
||||
})
|
||||
});
|
||||
if (onError) onError(JSON.stringify(error.message));
|
||||
}
|
||||
};
|
||||
|
||||
function replaceAccents(str) {
|
||||
// Verifies if the String has accents and replace them
|
||||
if (str.search(/[\xC0-\xFF]/g) > -1) {
|
||||
str = str
|
||||
.replace(/[\xC0-\xC5]/g, "A")
|
||||
.replace(/[\xC6]/g, "AE")
|
||||
.replace(/[\xC7]/g, "C")
|
||||
.replace(/[\xC8-\xCB]/g, "E")
|
||||
.replace(/[\xCC-\xCF]/g, "I")
|
||||
.replace(/[\xD0]/g, "D")
|
||||
.replace(/[\xD1]/g, "N")
|
||||
.replace(/[\xD2-\xD6\xD8]/g, "O")
|
||||
.replace(/[\xD9-\xDC]/g, "U")
|
||||
.replace(/[\xDD]/g, "Y")
|
||||
.replace(/[\xDE]/g, "P")
|
||||
.replace(/[\xE0-\xE5]/g, "a")
|
||||
.replace(/[\xE6]/g, "ae")
|
||||
.replace(/[\xE7]/g, "c")
|
||||
.replace(/[\xE8-\xEB]/g, "e")
|
||||
.replace(/[\xEC-\xEF]/g, "i")
|
||||
.replace(/[\xF1]/g, "n")
|
||||
.replace(/[\xF2-\xF6\xF8]/g, "o")
|
||||
.replace(/[\xF9-\xFC]/g, "u")
|
||||
.replace(/[\xFE]/g, "p")
|
||||
.replace(/[\xFD\xFF]/g, "y");
|
||||
}
|
||||
return str;
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import AlertComponent from "../alert/alert.component";
|
||||
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
||||
import JobsDocumentsLocalGalleryExternalComponent from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -23,6 +25,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(EmailDocumentsCompon
|
||||
|
||||
export function EmailDocumentsComponent({ emailConfig, form, selectedMediaState, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
treatments: { Imgproxy }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Imgproxy"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const [selectedMedia, setSelectedMedia] = selectedMediaState;
|
||||
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||
@@ -46,17 +55,37 @@ export function EmailDocumentsComponent({ emailConfig, form, selectedMediaState,
|
||||
10485760 - new Blob([form.getFieldValue("html")]).size ? (
|
||||
<div style={{ color: "red" }}>{t("general.errors.sizelimit")}</div>
|
||||
) : null}
|
||||
{!bodyshop.uselocalmediaserver && data && (
|
||||
<JobDocumentsGalleryExternal
|
||||
data={data ? data.documents : []}
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
/>
|
||||
)}
|
||||
{bodyshop.uselocalmediaserver && (
|
||||
<JobsDocumentsLocalGalleryExternalComponent
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
jobId={emailConfig.jobid}
|
||||
/>
|
||||
|
||||
{Imgproxy.treatment === "on" ? (
|
||||
<>
|
||||
{!bodyshop.uselocalmediaserver && data && (
|
||||
<JobsDocumentImgproxyGalleryExternal
|
||||
jobId={emailConfig.jobid}
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
/>
|
||||
)}
|
||||
{bodyshop.uselocalmediaserver && (
|
||||
<JobsDocumentsLocalGalleryExternalComponent
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
jobId={emailConfig.jobid}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!bodyshop.uselocalmediaserver && data && (
|
||||
<JobDocumentsGalleryExternal
|
||||
data={data ? data.documents : []}
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
/>
|
||||
)}
|
||||
{bodyshop.uselocalmediaserver && (
|
||||
<JobsDocumentsLocalGalleryExternalComponent
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
jobId={emailConfig.jobid}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
const { Option } = Select;
|
||||
//To be used as a form element only.
|
||||
|
||||
const EmployeeSearchSelect = ({ options, ...props }) => {
|
||||
const EmployeeSearchSelect = ({ options, showEmail, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -21,12 +21,16 @@ const EmployeeSearchSelect = ({ options, ...props }) => {
|
||||
{options
|
||||
? options.map((o) => (
|
||||
<Option key={o.id} value={o.id} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
|
||||
<Space>
|
||||
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
||||
|
||||
<Tag color="green">
|
||||
<Space size="small">
|
||||
{`${o.employee_number ?? ""} ${o.first_name} ${o.last_name}`}
|
||||
<Tag color="green" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
||||
</Tag>
|
||||
{showEmail && o.user_email ? (
|
||||
<Tag color="blue" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||
{o.user_email}
|
||||
</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
</Option>
|
||||
))
|
||||
|
||||
@@ -20,6 +20,7 @@ function FeatureWrapper({
|
||||
children,
|
||||
upsellComponent,
|
||||
bypass,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
...restProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -78,7 +79,11 @@ export function HasFeatureAccess({ featureName, bodyshop, bypass, debug = false
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return bodyshop?.features?.allAccess || dayjs(bodyshop?.features[featureName]).isAfter(dayjs());
|
||||
return (
|
||||
bodyshop?.features?.allAccess ||
|
||||
bodyshop?.features?.[featureName] ||
|
||||
dayjs(bodyshop?.features[featureName]).isAfter(dayjs())
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(FeatureWrapper);
|
||||
|
||||
@@ -40,7 +40,6 @@ import { RiSurveyLine } from "react-icons/ri";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
||||
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
@@ -51,6 +50,7 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
||||
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
// Redux mappings
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AlertFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Divider, Dropdown, Form, Input, Popover, Select, Space } from "antd";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import queryString from "query-string";
|
||||
@@ -8,24 +8,30 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||
import { GET_JOB_BY_PK_QUICK_INTAKE, JOB_PRODUCTION_TOGGLE } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
|
||||
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
||||
import ScheduleAtChange from "./job-at-change.component";
|
||||
import ScheduleEventColor from "./schedule-event.color.component";
|
||||
import ScheduleEventNote from "./schedule-event.note.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -33,7 +39,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
setMessage: (text) => dispatch(setMessage(text))
|
||||
setMessage: (text) => dispatch(setMessage(text)),
|
||||
insertAuditTrail: ({ jobid, operation }) => dispatch(insertAuditTrail({ jobid, operation }))
|
||||
});
|
||||
|
||||
export function ScheduleEventComponent({
|
||||
@@ -43,16 +50,41 @@ export function ScheduleEventComponent({
|
||||
event,
|
||||
refetch,
|
||||
handleCancel,
|
||||
setScheduleContext
|
||||
setScheduleContext,
|
||||
insertAuditTrail
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const history = useNavigate();
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||
const [mutationUpdateJob] = useMutation(JOB_PRODUCTION_TOGGLE);
|
||||
const [title, setTitle] = useState(event.title);
|
||||
const { socket } = useSocket();
|
||||
const notification = useNotification();
|
||||
const [form] = Form.useForm();
|
||||
const [popOverVisible, setPopOverVisible] = useState(false);
|
||||
|
||||
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
|
||||
variables: { id: event.job.id },
|
||||
onCompleted: (data) => {
|
||||
if (data?.jobs_by_pk) {
|
||||
const totalHours =
|
||||
(data.jobs_by_pk.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) +
|
||||
(data.jobs_by_pk.larhrs?.aggregate?.sum?.mod_lb_hrs || 0);
|
||||
form.setFieldsValue({
|
||||
actual_in: data.jobs_by_pk.actual_in ? data.jobs_by_pk.actual_in : dayjs(),
|
||||
scheduled_completion: data.jobs_by_pk.scheduled_completion
|
||||
? data.jobs_by_pk.scheduled_completion
|
||||
: totalHours && bodyshop.ss_configuration.nobusinessdays
|
||||
? dayjs().businessDaysAdd(totalHours / (bodyshop.target_touchtime || 1), "day")
|
||||
: dayjs().add(totalHours / (bodyshop.target_touchtime || 1), "day"),
|
||||
scheduled_delivery: data.jobs_by_pk.scheduled_delivery
|
||||
});
|
||||
}
|
||||
},
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
const blockContent = (
|
||||
<Space direction="vertical" wrap>
|
||||
@@ -89,6 +121,74 @@ export function ScheduleEventComponent({
|
||||
</Space>
|
||||
);
|
||||
|
||||
const handleConvert = async (values) => {
|
||||
const res = await mutationUpdateJob({
|
||||
variables: {
|
||||
jobId: event.job.id,
|
||||
job: {
|
||||
...values,
|
||||
status: bodyshop.md_ro_statuses.default_arrived,
|
||||
inproduction: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.errors) {
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.converted")
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: event.job.id,
|
||||
operation: AuditTrailMapping.jobintake(
|
||||
res.data.update_jobs.returning[0].status,
|
||||
DateTimeFormatterFunction(values.scheduled_completion)
|
||||
)
|
||||
});
|
||||
setPopOverVisible(false);
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
const popMenu = (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Form layout="vertical" form={form} onFinish={handleConvert}>
|
||||
<Form.Item
|
||||
name={["actual_in"]}
|
||||
label={t("jobs.fields.actual_in")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FormDateTimePickerComponent disabled={event.ro_number} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["scheduled_completion"]}
|
||||
label={t("jobs.fields.scheduled_completion")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FormDateTimePickerComponent disabled={event.ro_number} />
|
||||
</Form.Item>
|
||||
<Form.Item name={["scheduled_delivery"]} label={t("jobs.fields.scheduled_delivery")}>
|
||||
<FormDateTimePickerComponent disabled={event.ro_number} />
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap>
|
||||
<Button type="primary" onClick={() => form.submit()}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
const popoverContent = (
|
||||
<div style={{ maxWidth: "40vw" }}>
|
||||
{!event.isintake ? (
|
||||
@@ -294,7 +394,7 @@ export function ScheduleEventComponent({
|
||||
) : (
|
||||
<ScheduleManualEvent event={event} />
|
||||
)}
|
||||
{event.isintake ? (
|
||||
{event.isintake && HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
||||
@@ -303,7 +403,21 @@ export function ScheduleEventComponent({
|
||||
>
|
||||
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
||||
</Link>
|
||||
) : null}
|
||||
) : (
|
||||
<Popover //open={open}
|
||||
content={popMenu}
|
||||
open={popOverVisible}
|
||||
onOpenChange={setPopOverVisible}
|
||||
onClick={(e) => {
|
||||
getJobDetails();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
trigger="click"
|
||||
>
|
||||
<Button disabled={event.arrived}>{t("jobs.actions.intake_quick")}</Button>
|
||||
</Popover>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Col, Row } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import JobReconciliationBillsTable from "../job-reconciliation-bills-table/job-reconciliation-bills-table.component";
|
||||
import JobReconciliationPartsTable from "../job-reconciliation-parts-table/job-reconciliation-parts-table.component";
|
||||
import JobReconciliationTotals from "../job-reconciliation-totals/job-reconciliation-totals.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
export default function JobReconciliationModalComponent({ job, bills }) {
|
||||
const jobLineState = useState([]);
|
||||
@@ -20,7 +20,7 @@ export default function JobReconciliationModalComponent({ job, bills }) {
|
||||
|
||||
const filterFunction = InstanceRenderManager({
|
||||
imex: (j) =>
|
||||
(j.part_type !== null && j.part_type !== "PAE") ||
|
||||
(j.part_type !== null && j.part_type !== "PAE" && j.act_price !== 0 && j.part_qty !== 0) ||
|
||||
(j.line_desc && j.line_desc.toLowerCase().includes("towing") && j.lbr_op === "OP13") ||
|
||||
j.db_ref === "936004", //ADD SHIPPING LINE.
|
||||
rome: (j) =>
|
||||
|
||||
@@ -104,6 +104,7 @@ export default function JobWatcherToggleComponent({
|
||||
}
|
||||
placeholder={t("notifications.labels.employee-search")}
|
||||
value={selectedWatcher}
|
||||
showEmail={true}
|
||||
onChange={(value) => {
|
||||
setSelectedWatcher(value);
|
||||
handleWatcherSelect(value);
|
||||
|
||||
@@ -106,7 +106,12 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
|
||||
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.estimate_approved")} name="estimate_approved">
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.date_scheduled")} name="date_scheduled">
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import FormRow from "../layout-form-row/layout-form-row.component";
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -40,6 +41,20 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
||||
<Form.Item label={t("jobs.fields.date_rentalresp")} name="date_rentalresp">
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
|
||||
<DateTimePicker
|
||||
disabled={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 header={t("jobs.forms.scheddates")}>
|
||||
@@ -76,21 +91,15 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.actual_completion")}
|
||||
name="actual_completion"
|
||||
rules={[
|
||||
{
|
||||
required: jobInPostProduction
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
{() => (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.actual_completion")}
|
||||
name="actual_completion"
|
||||
rules={[{ required: jobInPostProduction }]}
|
||||
>
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.scheduled_delivery")} name="scheduled_delivery">
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
@@ -103,15 +112,12 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
||||
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
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">
|
||||
<Switch disabled={jobRO} />
|
||||
</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>
|
||||
</Col>
|
||||
<Col {...lossColDamage}>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
||||
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
||||
@@ -133,6 +133,16 @@ export function JobsDetailHeaderActions({
|
||||
const { socket } = useSocket();
|
||||
const notification = useNotification();
|
||||
|
||||
const isDevEnv = import.meta.env.DEV;
|
||||
const isProdEnv = import.meta.env.PROD;
|
||||
const userEmail = currentUser?.email || "";
|
||||
|
||||
const devEmails = ["imex.dev", "rome.dev"];
|
||||
const prodEmails = ["imex.prod", "rome.prod", "imex.test", "rome.test"];
|
||||
|
||||
const hasValidEmail = (emails) => emails.some((email) => userEmail.endsWith(email));
|
||||
const canSubmitForTesting = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails));
|
||||
|
||||
const {
|
||||
treatments: { ImEXPay }
|
||||
} = useSplitTreatments({
|
||||
@@ -171,7 +181,7 @@ export function JobsDetailHeaderActions({
|
||||
{ defaultOpenStatus: bodyshop.md_ro_statuses.default_imported },
|
||||
(newJobId) => {
|
||||
history(`/manage/jobs/${newJobId}`);
|
||||
notification["success"]({
|
||||
notification.success({
|
||||
message: t("jobs.successes.duplicated")
|
||||
});
|
||||
},
|
||||
@@ -181,7 +191,7 @@ export function JobsDetailHeaderActions({
|
||||
const handleDuplicateConfirm = () =>
|
||||
DuplicateJob(client, job.id, { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported }, (newJobId) => {
|
||||
history(`/manage/jobs/${newJobId}`);
|
||||
notification["success"]({
|
||||
notification.success({
|
||||
message: t("jobs.successes.duplicated")
|
||||
});
|
||||
});
|
||||
@@ -217,13 +227,13 @@ export function JobsDetailHeaderActions({
|
||||
const result = await deleteJob({ variables: { id: job.id } });
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
notification.success({
|
||||
message: t("jobs.successes.delete")
|
||||
});
|
||||
//go back to jobs list.
|
||||
history(`/manage/`);
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("jobs.errors.deleted", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
@@ -275,9 +285,9 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({ message: t("csi.successes.created") });
|
||||
notification.success({ message: t("csi.successes.created") });
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("csi.errors.creating", {
|
||||
message: JSON.stringify(result.errors)
|
||||
})
|
||||
@@ -316,7 +326,7 @@ export function JobsDetailHeaderActions({
|
||||
`${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}`
|
||||
);
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("messaging.error.invalidphone")
|
||||
});
|
||||
}
|
||||
@@ -328,7 +338,7 @@ export function JobsDetailHeaderActions({
|
||||
);
|
||||
}
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("csi.errors.notconfigured")
|
||||
});
|
||||
}
|
||||
@@ -358,7 +368,7 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
setMessage(`${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`);
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("messaging.error.invalidphone")
|
||||
});
|
||||
}
|
||||
@@ -398,7 +408,7 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
notification.success({
|
||||
message: t("jobs.successes.voided")
|
||||
});
|
||||
insertAuditTrail({
|
||||
@@ -409,7 +419,7 @@ export function JobsDetailHeaderActions({
|
||||
//go back to jobs list.
|
||||
history(`/manage/`);
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("jobs.errors.voiding", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
@@ -442,7 +452,7 @@ export function JobsDetailHeaderActions({
|
||||
console.log("handle -> XML", QbXmlResponse);
|
||||
} catch (error) {
|
||||
console.log("Error getting QBXML from Server.", error);
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("jobs.errors.exporting", {
|
||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message)
|
||||
})
|
||||
@@ -460,7 +470,7 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error connecting to quickbooks or partner.", error);
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("jobs.errors.exporting-partner")
|
||||
});
|
||||
|
||||
@@ -556,7 +566,7 @@ export function JobsDetailHeaderActions({
|
||||
}
|
||||
});
|
||||
if (!jobUpdate.errors) {
|
||||
notification["success"]({
|
||||
notification.success({
|
||||
message: t("appointments.successes.canceled")
|
||||
});
|
||||
insertAuditTrail({
|
||||
@@ -931,11 +941,11 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
notification.success({
|
||||
message: t("jobs.successes.partsqueue")
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("jobs.errors.saving", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
@@ -1111,6 +1121,27 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
}
|
||||
|
||||
if (canSubmitForTesting) {
|
||||
menuItems.push({
|
||||
key: "submitfortesting",
|
||||
id: "job-actions-submitfortesting",
|
||||
label: t("menus.jobsactions.submit-for-testing"),
|
||||
onClick: async () => {
|
||||
try {
|
||||
await axios.post("/job/totals-recorder", { id: job.id });
|
||||
notification.success({
|
||||
message: t("general.messages.submit-for-testing")
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Error submitting job for testing: ${err?.message}`);
|
||||
notification.error({
|
||||
message: t("general.errors.submit-for-testing-error")
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const menu = {
|
||||
items: menuItems,
|
||||
key: "popovermenu"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { GET_JOB_BY_PK_QUICK_INTAKE, JOB_PRODUCTION_TOGGLE } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
@@ -12,7 +13,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -44,9 +44,16 @@ export function JobsDetailHeaderActionsToggleProduction({
|
||||
variables: { id: job.id },
|
||||
onCompleted: (data) => {
|
||||
if (data?.jobs_by_pk) {
|
||||
const totalHours =
|
||||
(data.jobs_by_pk.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) +
|
||||
(data.jobs_by_pk.larhrs?.aggregate?.sum?.mod_lb_hrs || 0);
|
||||
form.setFieldsValue({
|
||||
actual_in: data.jobs_by_pk.actual_in ? data.jobs_by_pk.actual_in : dayjs(),
|
||||
scheduled_completion: data.jobs_by_pk.scheduled_completion,
|
||||
scheduled_completion: data.jobs_by_pk.scheduled_completion
|
||||
? data.jobs_by_pk.scheduled_completion
|
||||
: totalHours && bodyshop.ss_configuration.nobusinessdays
|
||||
? dayjs().businessDaysAdd(totalHours / (bodyshop.target_touchtime || 1), "day")
|
||||
: dayjs().add(totalHours / (bodyshop.target_touchtime || 1), "day"),
|
||||
actual_completion: data.jobs_by_pk.actual_completion,
|
||||
scheduled_delivery: data.jobs_by_pk.scheduled_delivery,
|
||||
actual_delivery: data.jobs_by_pk.actual_delivery
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled } from "@ant-design/icons";
|
||||
import { Card, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { Card, Checkbox, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
@@ -21,7 +24,7 @@ import ProductionListColumnComment from "../production-list-columns/production-l
|
||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
import "./jobs-detail-header.styles.scss";
|
||||
import dayjs from "../../utils/day";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -29,41 +32,55 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" }))
|
||||
setPrintCenterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "printCenter"
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
const colSpan = {
|
||||
xs: {
|
||||
span: 24
|
||||
},
|
||||
sm: {
|
||||
span: 24
|
||||
},
|
||||
md: {
|
||||
span: 12
|
||||
},
|
||||
lg: {
|
||||
span: 6
|
||||
},
|
||||
xl: {
|
||||
span: 6
|
||||
}
|
||||
xs: { span: 24 },
|
||||
sm: { span: 24 },
|
||||
md: { span: 12 },
|
||||
lg: { span: 6 },
|
||||
xl: { span: 6 }
|
||||
};
|
||||
|
||||
export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
const { t } = useTranslation();
|
||||
const { notification } = useNotification();
|
||||
const [notesClamped, setNotesClamped] = useState(true);
|
||||
const vehicleTitle = `${job.v_model_yr || ""} ${job.v_color || ""}
|
||||
${job.v_make_desc || ""}
|
||||
${job.v_model_desc || ""}`.trim();
|
||||
|
||||
const [updateJob] = useMutation(UPDATE_JOB);
|
||||
const vehicleTitle =
|
||||
`${job.v_model_yr || ""} ${job.v_color || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim();
|
||||
const bodyHrs = job.joblines.filter((j) => j.mod_lbr_ty !== "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
||||
const refinishHrs = job.joblines
|
||||
.filter((line) => line.mod_lbr_ty === "LAR")
|
||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
||||
|
||||
const ownerTitle = OwnerNameDisplayFunction(job).trim();
|
||||
|
||||
// Handle checkbox changes
|
||||
const handleCheckboxChange = async (field, checked) => {
|
||||
const value = checked ? dayjs().toISOString() : null;
|
||||
try {
|
||||
await updateJob({
|
||||
variables: {
|
||||
jobId: job.id,
|
||||
job: { [field]: value }
|
||||
},
|
||||
refetchQueries: ["GET_JOB_BY_PK"],
|
||||
awaitRefetchQueries: true
|
||||
});
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: t("jobs.errors.saving", { error: error.message })
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
|
||||
<Col {...colSpan}>
|
||||
@@ -72,11 +89,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
<DataLabel label={t("jobs.fields.status")}>
|
||||
<Space wrap>
|
||||
{job.status}
|
||||
{job.inproduction && (
|
||||
<Tag color="#f50" key="production">
|
||||
{t("jobs.labels.inproduction")}
|
||||
</Tag>
|
||||
)}
|
||||
{job.inproduction && <Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>}
|
||||
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||
{job.iouparent && (
|
||||
<Link to={`/manage/jobs/${job.iouparent}`}>
|
||||
@@ -110,7 +123,6 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
<span style={{ margin: "0rem .5rem" }}>/</span>
|
||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||
</DataLabel>
|
||||
|
||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||
{job.alt_transport}
|
||||
<JobAltTransportChange job={job} />
|
||||
@@ -127,11 +139,39 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
))}
|
||||
</DataLabel>
|
||||
)}
|
||||
|
||||
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
||||
<ProductionListColumnProductionNote record={job} />
|
||||
</DataLabel>
|
||||
|
||||
<DataLabel label={t("jobs.fields.estimate_sent_approval")}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={!!job.estimate_sent_approval}
|
||||
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{job.estimate_sent_approval && (
|
||||
<span style={{ color: "#888" }}>
|
||||
<DateTimeFormatter>{job.estimate_sent_approval}</DateTimeFormatter>
|
||||
</span>
|
||||
)}
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.estimate_approved")}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={!!job.estimate_approved}
|
||||
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{job.estimate_approved && (
|
||||
<span style={{ color: "#888" }}>
|
||||
<DateTimeFormatter>{job.estimate_approved}</DateTimeFormatter>
|
||||
</span>
|
||||
)}
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</DataLabel>
|
||||
<Space wrap>
|
||||
{job.special_coverage_policy && (
|
||||
<Tag color="tomato">
|
||||
@@ -149,6 +189,14 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
</Space>
|
||||
</Tag>
|
||||
)}
|
||||
{job.hit_and_run && (
|
||||
<Tag color="green">
|
||||
<Space>
|
||||
<WarningFilled />
|
||||
<span>{t("jobs.fields.hit_and_run")}</span>
|
||||
</Space>
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -19,7 +19,13 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsDownloadButton);
|
||||
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||
################################################################################################
|
||||
*/
|
||||
export function JobsDocumentsDownloadButton({ bodyshop, galleryImages, identifier }) {
|
||||
const { t } = useTranslation();
|
||||
const [download, setDownload] = useState(null);
|
||||
|
||||
@@ -17,7 +17,13 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsGalleryReassign);
|
||||
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||
################################################################################################
|
||||
*/
|
||||
export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages, callback }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Col, Row, Space } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Gallery } from "react-grid-gallery";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Lightbox from "react-image-lightbox";
|
||||
import "react-image-lightbox/style.css";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import DocumentsUploadComponent from "../documents-upload/documents-upload.component";
|
||||
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
|
||||
import JobsDocumentsDownloadButton from "./jobs-document-gallery.download.component";
|
||||
import JobsDocumentsGalleryReassign from "./jobs-document-gallery.reassign.component";
|
||||
import JobsDocumentsDeleteButton from "./jobs-documents-gallery.delete.component";
|
||||
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-gallery.selectall.component";
|
||||
|
||||
import Lightbox from "react-image-lightbox";
|
||||
import "react-image-lightbox/style.css";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||
################################################################################################
|
||||
*/
|
||||
function JobsDocumentsComponent({
|
||||
bodyshop,
|
||||
data,
|
||||
@@ -114,6 +119,7 @@ function JobsDocumentsComponent({
|
||||
);
|
||||
setgalleryImages(documents);
|
||||
}, [data, setgalleryImages, t]);
|
||||
|
||||
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
|
||||
const hasMobileAccess = HasFeatureAccess({ bodyshop, featureName: "mobile" });
|
||||
return (
|
||||
@@ -137,7 +143,6 @@ function JobsDocumentsComponent({
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
<DocumentsUploadComponent
|
||||
|
||||
@@ -2,29 +2,77 @@ import { useQuery } from "@apollo/client";
|
||||
import React from "react";
|
||||
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobDocumentsImgProxy from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import JobDocuments from "./jobs-documents-gallery.component";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsContainer);
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||
################################################################################################
|
||||
*/
|
||||
export function JobsDocumentsContainer({
|
||||
jobId,
|
||||
billId,
|
||||
documentsList,
|
||||
billsCallback,
|
||||
refetchOverride,
|
||||
ignoreSizeLimit,
|
||||
bodyshop
|
||||
}) {
|
||||
const {
|
||||
treatments: { Imgproxy }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Imgproxy"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
export default function JobsDocumentsContainer({ jobId, billId, documentsList, billsCallback }) {
|
||||
const { loading, error, data, refetch } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||
variables: { jobId: jobId },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
skip: !!billId
|
||||
skip: Imgproxy.treatment === "on" || !!billId
|
||||
});
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent type="error" message={error.message} />;
|
||||
|
||||
return (
|
||||
<JobDocuments
|
||||
data={(data && data.documents) || documentsList || []}
|
||||
downloadIdentifier={data && data.jobs_by_pk.ro_number}
|
||||
totalSize={data && data.documents_aggregate.aggregate.sum.size}
|
||||
billId={billId}
|
||||
jobId={jobId}
|
||||
refetch={refetch}
|
||||
billsCallback={billsCallback}
|
||||
/>
|
||||
);
|
||||
if (Imgproxy.treatment === "on") {
|
||||
return (
|
||||
<JobDocumentsImgProxy
|
||||
data={(data && data.documents) || documentsList || []}
|
||||
downloadIdentifier={data && data.jobs_by_pk.ro_number}
|
||||
totalSize={data && data.documents_aggregate.aggregate.sum.size}
|
||||
billId={billId}
|
||||
jobId={jobId}
|
||||
refetch={refetchOverride || refetch}
|
||||
billsCallback={billsCallback}
|
||||
ignoreSizeLimit={ignoreSizeLimit}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<JobDocuments
|
||||
data={(data && data.documents) || documentsList || []}
|
||||
downloadIdentifier={data && data.jobs_by_pk.ro_number}
|
||||
totalSize={data && data.documents_aggregate.aggregate.sum.size}
|
||||
billId={billId}
|
||||
jobId={jobId}
|
||||
refetch={refetchOverride || refetch}
|
||||
billsCallback={billsCallback}
|
||||
ignoreSizeLimit={ignoreSizeLimit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,13 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||
################################################################################################
|
||||
*/
|
||||
export default function JobsDocumentsDeleteButton({ galleryImages, deletionCallback }) {
|
||||
const { t } = useTranslation();
|
||||
const notification = useNotification();
|
||||
|
||||
@@ -3,6 +3,13 @@ import { Gallery } from "react-grid-gallery";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
|
||||
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||
################################################################################################
|
||||
*/
|
||||
function JobsDocumentGalleryExternal({
|
||||
data,
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@ import { Button, Space } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||
################################################################################################
|
||||
*/
|
||||
|
||||
export default function JobsDocumentsGallerySelectAllComponent({ galleryImages, setGalleryImages }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Button, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import cleanAxios from "../../utils/CleanAxios";
|
||||
import formatBytes from "../../utils/formatbytes";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||
################################################################################################
|
||||
*/
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
|
||||
|
||||
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier, jobId }) {
|
||||
const { t } = useTranslation();
|
||||
const [download, setDownload] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const imagesToDownload = [
|
||||
...galleryImages.images.filter((image) => image.isSelected),
|
||||
...galleryImages.other.filter((image) => image.isSelected)
|
||||
];
|
||||
|
||||
function downloadProgress(progressEvent) {
|
||||
setDownload((currentDownloadState) => {
|
||||
return {
|
||||
downloaded: progressEvent.loaded || 0,
|
||||
speed: (progressEvent.loaded || 0) - ((currentDownloadState && currentDownloadState.downloaded) || 0)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function standardMediaDownload(bufferData) {
|
||||
const a = document.createElement("a");
|
||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||
a.href = url;
|
||||
a.download = `${identifier || "documents"}.zip`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
const handleDownload = async () => {
|
||||
logImEXEvent("jobs_documents_download");
|
||||
setLoading(true);
|
||||
const zipUrl = await axios({
|
||||
url: "/media/imgproxy/download",
|
||||
method: "POST",
|
||||
data: { jobId, documentids: imagesToDownload.map((_) => _.id) }
|
||||
});
|
||||
|
||||
const theDownloadedZip = await cleanAxios({
|
||||
url: zipUrl.data.url,
|
||||
method: "GET",
|
||||
responseType: "arraybuffer",
|
||||
onDownloadProgress: downloadProgress
|
||||
});
|
||||
setLoading(false);
|
||||
setDownload(null);
|
||||
|
||||
standardMediaDownload(theDownloadedZip.data);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button loading={download || loading} disabled={imagesToDownload.length < 1} onClick={handleDownload}>
|
||||
<Space>
|
||||
<span>{t("documents.actions.download")}</span>
|
||||
{download && <span>{`(${formatBytes(download.downloaded)} @ ${formatBytes(download.speed)} / second)`}</span>}
|
||||
</Space>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { Button, Form, Popover, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { GET_DOC_SIZE_BY_JOB } from "../../graphql/documents.queries.js";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import JobSearchSelect from "../job-search-select/job-search-select.component.jsx";
|
||||
import { isFunction } from "lodash";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyGalleryReassign);
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||
################################################################################################
|
||||
*/
|
||||
export function JobsDocumentsImgproxyGalleryReassign({ bodyshop, galleryImages, callback }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const notification = useNotification();
|
||||
|
||||
const selectedImages = useMemo(() => {
|
||||
return [
|
||||
...galleryImages.images.filter((image) => image.isSelected),
|
||||
...galleryImages.other.filter((image) => image.isSelected)
|
||||
];
|
||||
}, [galleryImages]);
|
||||
const client = useApolloClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleFinish = async ({ jobid }) => {
|
||||
setLoading(true);
|
||||
|
||||
//Check to see if the space remaining on the new job is sufficient. If it isn't cancel this.
|
||||
const newJobData = await client.query({
|
||||
query: GET_DOC_SIZE_BY_JOB,
|
||||
variables: { jobId: jobid }
|
||||
});
|
||||
|
||||
const transferedDocSizeTotal = selectedImages.reduce((acc, val) => acc + val.size, 0);
|
||||
|
||||
const shouldPreventTransfer =
|
||||
bodyshop.jobsizelimit - newJobData.data.documents_aggregate.aggregate.sum.size < transferedDocSizeTotal;
|
||||
|
||||
if (shouldPreventTransfer) {
|
||||
notification.error({
|
||||
key: "cannotuploaddocuments",
|
||||
message: t("documents.labels.reassign_limitexceeded_title"),
|
||||
description: t("documents.labels.reassign_limitexceeded")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await axios.post("/media/imgproxy/rename", {
|
||||
tojobid: jobid,
|
||||
documents: selectedImages.map((i) => {
|
||||
//Need to check if the current key folder is null, or another job.
|
||||
const currentKeys = i.key.split("/");
|
||||
currentKeys[1] = jobid;
|
||||
currentKeys.join("/");
|
||||
return {
|
||||
id: i.id,
|
||||
from: i.key,
|
||||
to: currentKeys.join("/"),
|
||||
extension: i.extension,
|
||||
type: i.type
|
||||
};
|
||||
})
|
||||
});
|
||||
//Add in confirmation & errors.
|
||||
if (isFunction(callback)) callback();
|
||||
|
||||
if (res.errors) {
|
||||
notification.error({
|
||||
message: t("documents.errors.updating", {
|
||||
message: JSON.stringify(res.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
if (!res.mutationResult?.errors) {
|
||||
notification.success({
|
||||
message: t("documents.successes.updated")
|
||||
});
|
||||
}
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const popContent = (
|
||||
<div>
|
||||
<Form onFinish={handleFinish} layout="vertical" form={form}>
|
||||
<Form.Item
|
||||
label={t("documents.labels.newjobid")}
|
||||
style={{ width: "20rem" }}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={"jobid"}
|
||||
>
|
||||
<JobSearchSelect notExported={false} notInvoiced={false} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Space>
|
||||
<Button type="primary" onClick={() => form.submit()}>
|
||||
{t("general.actions.submit")}
|
||||
</Button>
|
||||
<Button onClick={() => setOpen(false)}>{t("general.actions.cancel")}</Button>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={popContent} open={open}>
|
||||
<Button disabled={selectedImages.length < 1} onClick={() => setOpen(true)} loading={loading}>
|
||||
{t("documents.actions.reassign")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Col, Row, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import i18n from "i18next";
|
||||
import { isFunction } from "lodash";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Gallery } from "react-grid-gallery";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Lightbox from "react-image-lightbox";
|
||||
import "react-image-lightbox/style.css";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import DocumentsUploadImgproxyComponent from "../documents-upload-imgproxy/documents-upload-imgproxy.component";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||
import JobsDocumentsDownloadButton from "./jobs-document-imgproxy-gallery.download.component";
|
||||
import JobsDocumentsGalleryReassign from "./jobs-document-imgproxy-gallery.reassign.component";
|
||||
import JobsDocumentsDeleteButton from "./jobs-documents-imgproxy-gallery.delete.component";
|
||||
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-imgproxy-gallery.selectall.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||
################################################################################################
|
||||
*/
|
||||
function JobsDocumentsImgproxyComponent({
|
||||
bodyshop,
|
||||
data,
|
||||
jobId,
|
||||
refetch,
|
||||
billId,
|
||||
billsCallback,
|
||||
totalSize,
|
||||
downloadIdentifier,
|
||||
ignoreSizeLimit
|
||||
}) {
|
||||
const [galleryImages, setGalleryImages] = useState({ images: [], other: [] });
|
||||
const { t } = useTranslation();
|
||||
const [modalState, setModalState] = useState({ open: false, index: 0 });
|
||||
|
||||
const fetchThumbnails = useCallback(() => {
|
||||
fetchImgproxyThumbnails({ setStateCallback: setGalleryImages, jobId });
|
||||
}, [jobId, setGalleryImages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
fetchThumbnails();
|
||||
}
|
||||
}, [data, fetchThumbnails]);
|
||||
|
||||
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
|
||||
const hasMobileAccess = HasFeatureAccess({ bodyshop, featureName: "mobile" });
|
||||
return (
|
||||
<div>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Space wrap>
|
||||
<Button
|
||||
onClick={() => {
|
||||
//Handle any doc refresh.
|
||||
|
||||
isFunction(refetch) && refetch();
|
||||
|
||||
//Do the imgproxy refresh too
|
||||
fetchThumbnails();
|
||||
}}
|
||||
>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setGalleryImages} />
|
||||
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} jobId={jobId} />
|
||||
<JobsDocumentsDeleteButton
|
||||
galleryImages={galleryImages}
|
||||
deletionCallback={billsCallback || fetchThumbnails || refetch}
|
||||
/>
|
||||
{!billId && (
|
||||
<JobsDocumentsGalleryReassign galleryImages={galleryImages} callback={fetchThumbnails || refetch} />
|
||||
)}
|
||||
</Space>
|
||||
</Col>
|
||||
{!hasMediaAccess && (
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
<UpsellComponent disableMask upsell={upsellEnum().media.general} />
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
<DocumentsUploadImgproxyComponent
|
||||
jobId={jobId}
|
||||
totalSize={totalSize}
|
||||
billId={billId}
|
||||
callbackAfterUpload={billsCallback || fetchThumbnails || refetch}
|
||||
ignoreSizeLimit={ignoreSizeLimit}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
{hasMediaAccess && !hasMobileAccess && (
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
<UpsellComponent upsell={upsellEnum().media.mobile} />
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
<Col span={24}>
|
||||
<Card title={t("jobs.labels.documents-images")}>
|
||||
<Gallery
|
||||
images={galleryImages.images}
|
||||
onClick={(index, item) => {
|
||||
setModalState({ open: true, index: index });
|
||||
// window.open(
|
||||
// item.fullsize,
|
||||
// "_blank",
|
||||
// "toolbar=0,location=0,menubar=0"
|
||||
// );
|
||||
}}
|
||||
onSelect={(index, image) => {
|
||||
setGalleryImages({
|
||||
...galleryImages,
|
||||
images: galleryImages.images.map((g, idx) =>
|
||||
index === idx ? { ...g, isSelected: !g.isSelected } : g
|
||||
)
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card title={t("jobs.labels.documents-other")}>
|
||||
<Gallery
|
||||
images={galleryImages.other}
|
||||
thumbnailStyle={() => {
|
||||
return {
|
||||
backgroundImage: <FileExcelFilled />,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
cursor: "pointer"
|
||||
};
|
||||
}}
|
||||
onClick={(index) => {
|
||||
window.open(galleryImages.other[index].source, "_blank", "toolbar=0,location=0,menubar=0");
|
||||
}}
|
||||
onSelect={(index) => {
|
||||
setGalleryImages({
|
||||
...galleryImages,
|
||||
other: galleryImages.other.map((g, idx) => (index === idx ? { ...g, isSelected: !g.isSelected } : g))
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
{modalState.open && (
|
||||
<Lightbox
|
||||
toolbarButtons={[
|
||||
<EditFilled
|
||||
key="edit"
|
||||
onClick={() => {
|
||||
const newWindow = window.open(
|
||||
`${window.location.protocol}//${window.location.host}/edit?documentId=${
|
||||
galleryImages.images[modalState.index].id
|
||||
}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
if (newWindow) newWindow.opener = null;
|
||||
}}
|
||||
/>
|
||||
]}
|
||||
mainSrc={galleryImages.images[modalState.index].fullsize}
|
||||
nextSrc={galleryImages.images[(modalState.index + 1) % galleryImages.images.length].fullsize}
|
||||
prevSrc={
|
||||
galleryImages.images[(modalState.index + galleryImages.images.length - 1) % galleryImages.images.length]
|
||||
.fullsize
|
||||
}
|
||||
onCloseRequest={() => setModalState({ open: false, index: 0 })}
|
||||
onMovePrevRequest={() =>
|
||||
setModalState({
|
||||
...modalState,
|
||||
index: (modalState.index + galleryImages.images.length - 1) % galleryImages.images.length
|
||||
})
|
||||
}
|
||||
onMoveNextRequest={() =>
|
||||
setModalState({
|
||||
...modalState,
|
||||
index: (modalState.index + 1) % galleryImages.images.length
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyComponent);
|
||||
|
||||
export const fetchImgproxyThumbnails = async ({ setStateCallback, jobId, imagesOnly }) => {
|
||||
const result = await axios.post("/media/imgproxy/thumbnails", { jobid: jobId });
|
||||
const documents = result.data.reduce(
|
||||
(acc, value) => {
|
||||
if (value.type.startsWith("image")) {
|
||||
acc.images.push({
|
||||
src: value.thumbnailUrl,
|
||||
fullsize: value.originalUrl,
|
||||
height: 225,
|
||||
width: 225,
|
||||
isSelected: false,
|
||||
key: value.key,
|
||||
extension: value.extension,
|
||||
id: value.id,
|
||||
type: value.type,
|
||||
size: value.size,
|
||||
tags: [{ value: value.type, title: value.type }]
|
||||
});
|
||||
} else {
|
||||
const fileName = value.key.split("/").pop();
|
||||
acc.other.push({
|
||||
source: value.originalUrlViaProxyPath,
|
||||
src: value.thumbnailUrl,
|
||||
fullsize: value.presignedGetUrl,
|
||||
tags: [
|
||||
{
|
||||
value: fileName,
|
||||
title: fileName
|
||||
},
|
||||
|
||||
{ value: value.type, title: value.type },
|
||||
...(value.bill
|
||||
? [
|
||||
{
|
||||
value: value.bill.vendor.name,
|
||||
title: i18n.t("vendors.fields.name")
|
||||
},
|
||||
{ value: value.bill.date, title: i18n.t("bills.fields.date") },
|
||||
{
|
||||
value: value.bill.invoice_number,
|
||||
title: i18n.t("bills.fields.invoice_number")
|
||||
}
|
||||
]
|
||||
: [])
|
||||
],
|
||||
height: 225,
|
||||
width: 225,
|
||||
isSelected: false,
|
||||
extension: value.extension,
|
||||
key: value.key,
|
||||
id: value.id,
|
||||
type: value.type,
|
||||
size: value.size
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ images: [], other: [] }
|
||||
);
|
||||
|
||||
setStateCallback(imagesOnly ? documents.images : documents);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import JobDocuments from "./jobs-documents-imgproxy-gallery.component";
|
||||
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||
################################################################################################
|
||||
*/
|
||||
|
||||
export default function JobsDocumentsImgproxyContainer({ jobId, billId, documentsList, billsCallback }) {
|
||||
const { loading, error, data, refetch } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||
variables: { jobId: jobId },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
skip: !!billId
|
||||
});
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent type="error" message={error.message} />;
|
||||
|
||||
return (
|
||||
<JobDocuments
|
||||
data={(data && data.documents) || documentsList || []}
|
||||
downloadIdentifier={data && data.jobs_by_pk.ro_number}
|
||||
totalSize={data && data.documents_aggregate.aggregate.sum.size}
|
||||
billId={billId}
|
||||
jobId={jobId}
|
||||
refetch={refetch}
|
||||
billsCallback={billsCallback}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { QuestionCircleOutlined } from "@ant-design/icons";
|
||||
import { Button, Popconfirm } from "antd";
|
||||
import axios from "axios";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { isFunction } from "lodash";
|
||||
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||
################################################################################################
|
||||
*/
|
||||
|
||||
export default function JobsDocumentsImgproxyDeleteButton({ galleryImages, deletionCallback }) {
|
||||
const { t } = useTranslation();
|
||||
const notification = useNotification();
|
||||
|
||||
const imagesToDelete = [
|
||||
...galleryImages.images.filter((image) => image.isSelected),
|
||||
...galleryImages.other.filter((image) => image.isSelected)
|
||||
];
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
logImEXEvent("job_documents_delete", { count: imagesToDelete.length });
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await axios.post("/media/imgproxy/delete", {
|
||||
ids: imagesToDelete.map((d) => d.id)
|
||||
});
|
||||
|
||||
if (res.data.error) {
|
||||
notification.error({
|
||||
message: t("documents.errors.deleting", {
|
||||
error: JSON.stringify(res.data.error.response.errors)
|
||||
})
|
||||
});
|
||||
} else {
|
||||
notification.success({
|
||||
key: "docdeletedsuccesfully",
|
||||
message: t("documents.successes.delete")
|
||||
});
|
||||
|
||||
if (isFunction(deletionCallback)) deletionCallback();
|
||||
}
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: t("documents.errors.deleting", {
|
||||
error: error.message
|
||||
})
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popconfirm
|
||||
disabled={imagesToDelete.length < 1}
|
||||
icon={<QuestionCircleOutlined style={{ color: "red" }} />}
|
||||
onConfirm={handleDelete}
|
||||
title={t("documents.labels.confirmdelete")}
|
||||
okText={t("general.actions.delete")}
|
||||
okButtonProps={{ danger: true }}
|
||||
cancelText={t("general.actions.cancel")}
|
||||
>
|
||||
<Button disabled={imagesToDelete.length < 1} loading={loading}>
|
||||
{t("documents.actions.delete")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useEffect } from "react";
|
||||
import { Gallery } from "react-grid-gallery";
|
||||
import { fetchImgproxyThumbnails } from "./jobs-documents-imgproxy-gallery.component";
|
||||
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||
################################################################################################
|
||||
*/
|
||||
|
||||
function JobsDocumentImgproxyGalleryExternal({ jobId, externalMediaState }) {
|
||||
const [galleryImages, setgalleryImages] = externalMediaState;
|
||||
|
||||
useEffect(() => {
|
||||
if (jobId) fetchImgproxyThumbnails({ setStateCallback: setgalleryImages, jobId, imagesOnly: true });
|
||||
}, [jobId, setgalleryImages]);
|
||||
|
||||
return (
|
||||
<div className="clearfix">
|
||||
<Gallery
|
||||
images={galleryImages}
|
||||
backdropClosesModal={true}
|
||||
onSelect={(index, image) => {
|
||||
setgalleryImages(galleryImages.map((g, idx) => (index === idx ? { ...g, isSelected: !g.isSelected } : g)));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobsDocumentImgproxyGalleryExternal;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Button, Space } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/*
|
||||
################################################################################################
|
||||
Developer Note:
|
||||
Known Technical Debt Item
|
||||
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||
################################################################################################
|
||||
*/
|
||||
|
||||
export default function JobsDocumentsImgproxyGallerySelectAllComponent({ galleryImages, setGalleryImages }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSelectAll = () => {
|
||||
setGalleryImages({
|
||||
...galleryImages,
|
||||
other: galleryImages.other.map((i) => {
|
||||
return { ...i, isSelected: true };
|
||||
}),
|
||||
images: galleryImages.images.map((i) => {
|
||||
return { ...i, isSelected: true };
|
||||
})
|
||||
});
|
||||
};
|
||||
const handleSelectAllImages = () => {
|
||||
setGalleryImages({
|
||||
...galleryImages,
|
||||
|
||||
images: galleryImages.images.map((i) => {
|
||||
return { ...i, isSelected: true };
|
||||
})
|
||||
});
|
||||
};
|
||||
const handleSelectAllDocuments = () => {
|
||||
setGalleryImages({
|
||||
...galleryImages,
|
||||
other: galleryImages.other.map((i) => {
|
||||
return { ...i, isSelected: true };
|
||||
})
|
||||
});
|
||||
};
|
||||
const handleDeselectAll = () => {
|
||||
setGalleryImages({
|
||||
...galleryImages,
|
||||
other: galleryImages.other.map((i) => {
|
||||
return { ...i, isSelected: false };
|
||||
}),
|
||||
images: galleryImages.images.map((i) => {
|
||||
return { ...i, isSelected: false };
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
<Button onClick={handleSelectAll}>{t("general.actions.selectall")}</Button>
|
||||
<Button onClick={handleSelectAllImages}>{t("documents.actions.selectallimages")}</Button>
|
||||
<Button onClick={handleSelectAllDocuments}>{t("documents.actions.selectallotherdocuments")}</Button>
|
||||
<Button onClick={handleDeselectAll}>{t("general.actions.deselectall")}</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/* you can make up upload button and sample style by using stylesheets */
|
||||
.ant-upload-select-picture-card i {
|
||||
font-size: 32px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.ant-upload-select-picture-card .ant-upload-text {
|
||||
margin-top: 8px;
|
||||
color: #666;
|
||||
}
|
||||
@@ -3,10 +3,10 @@ import { useQuery } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import NotificationCenterComponent from "./notification-center.component";
|
||||
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
||||
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import day from "../../utils/day.js";
|
||||
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
// This will be used to poll for notifications when the socket is disconnected
|
||||
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Card, Checkbox, Form, Space, Table } from "antd";
|
||||
import { Button, Card, Checkbox, Divider, Form, Space, Switch, Table, Typography } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import 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 LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
@@ -24,9 +28,11 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [initialValues, setInitialValues] = useState({});
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const [autoAddEnabled, setAutoAddEnabled] = useState(false);
|
||||
const [initialAutoAdd, setInitialAutoAdd] = useState(false);
|
||||
const notification = useNotification();
|
||||
|
||||
// Fetch notification settings.
|
||||
// Fetch notification settings and notifications_autoadd
|
||||
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
@@ -34,13 +40,16 @@ const NotificationSettingsForm = ({ 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(() => {
|
||||
if (data?.associations?.length > 0) {
|
||||
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) => {
|
||||
acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false };
|
||||
return acc;
|
||||
@@ -48,32 +57,66 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
||||
|
||||
setInitialValues(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]);
|
||||
|
||||
// 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) => {
|
||||
if (data?.associations?.length > 0) {
|
||||
const userId = data.associations[0].id;
|
||||
// Save the updated notification settings.
|
||||
const result = await updateNotificationSettings({ variables: { id: userId, ns: values } });
|
||||
if (!result?.errors) {
|
||||
notification.success({ message: t("notifications.labels.notification-settings-success") });
|
||||
setInitialValues(values);
|
||||
setIsDirty(false);
|
||||
} else {
|
||||
try {
|
||||
const result = await updateNotificationSettings({ variables: { id: userId, ns: values } });
|
||||
if (!result?.errors) {
|
||||
notification.success({ message: t("notifications.labels.notification-settings-success") });
|
||||
setInitialValues(values);
|
||||
setIsDirty(false);
|
||||
} else {
|
||||
throw new Error("Failed to update notification settings");
|
||||
}
|
||||
} catch (err) {
|
||||
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 = () => {
|
||||
setIsDirty(true);
|
||||
};
|
||||
|
||||
// Check if auto-add has changed
|
||||
const isAutoAddDirty = autoAddEnabled !== initialAutoAdd;
|
||||
|
||||
// Handle reset of form and auto-add
|
||||
const handleReset = () => {
|
||||
form.setFieldsValue(initialValues);
|
||||
setAutoAddEnabled(initialAutoAdd);
|
||||
setIsDirty(false);
|
||||
};
|
||||
|
||||
@@ -139,17 +182,25 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
||||
title={t("notifications.labels.notificationscenarios")}
|
||||
extra={
|
||||
<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")}
|
||||
</Button>
|
||||
|
||||
<Button type="primary" htmlType="submit" disabled={!isDirty} loading={saving}>
|
||||
<Button type="primary" htmlType="submit" disabled={!isDirty} loading={savingSettings}>
|
||||
{t("notifications.labels.save")}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
|
||||
<Divider />
|
||||
</Card>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -32,7 +33,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
const handleClick = ({ item }) => {
|
||||
const handleClick = ({ item, key, keyPath }) => {
|
||||
form.setFieldsValue({ comments: item.props.value });
|
||||
};
|
||||
|
||||
@@ -97,18 +98,17 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
)}
|
||||
{!isReturn && (
|
||||
<Form.Item name="order_type" initialValue="parts_order" label={t("parts_orders.labels.order_type")}>
|
||||
<Radio.Group disabled={sendType === "oec"}>
|
||||
<Radio value={"parts_order"}>{t("parts_orders.labels.parts_order")}</Radio>
|
||||
<Radio value={"sublet"}>{t("parts_orders.labels.sublet_order")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item name="order_type" initialValue="parts_order" label={t("parts_orders.labels.order_type")}>
|
||||
<Radio.Group disabled={sendType === "oec"}>
|
||||
<Radio value={"parts_order"}>{t("parts_orders.labels.parts_order")}</Radio>
|
||||
<Radio value={"sublet"}>{t("parts_orders.labels.sublet_order")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Divider orientation="left">{t("parts_orders.labels.inthisorder")}</Divider>
|
||||
<Form.List name={["parts_order_lines", "data"]}>
|
||||
{(fields, { remove, move }) => {
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import { Card, Form, Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const FilterSettings = ({
|
||||
selectedMdInsCos,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Card, Checkbox, Col, Form, Row } from "antd";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const InformationSettings = ({ t }) => (
|
||||
<Card title={t("production.settings.information")}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Card title={t("production.settings.information")} style={{ maxWidth: "100%", overflowX: "auto" }}>
|
||||
<Row gutter={[16, 16]} wrap>
|
||||
{[
|
||||
"model_info",
|
||||
"ownr_nm",
|
||||
@@ -21,7 +20,7 @@ const InformationSettings = ({ t }) => (
|
||||
"subtotal",
|
||||
"tasks"
|
||||
].map((item) => (
|
||||
<Col span={4} key={item}>
|
||||
<Col xs={24} sm={12} md={8} lg={6} key={item}>
|
||||
<Form.Item name={item} valuePropName="checked">
|
||||
<Checkbox>{t(`production.labels.${item}`)}</Checkbox>
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Card, Col, Form, Radio, Row } from "antd";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const LayoutSettings = ({ t }) => (
|
||||
<Card title={t("production.settings.layout")}>
|
||||
<Card title={t("production.settings.layout")} style={{ maxWidth: "100%", overflowX: "auto" }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{[
|
||||
{
|
||||
@@ -48,9 +47,9 @@ const LayoutSettings = ({ t }) => (
|
||||
]
|
||||
}
|
||||
].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}>
|
||||
<Radio.Group>
|
||||
<Radio.Group style={{ display: "flex", flexWrap: "nowrap" }}>
|
||||
{options.map((option) => (
|
||||
<Radio.Button key={option.value.toString()} value={option.value}>
|
||||
{option.label}
|
||||
|
||||
@@ -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 { 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 onDragEnd = (result) => {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { SettingOutlined } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
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 { useTranslation } from "react-i18next";
|
||||
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
|
||||
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.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 PropTypes from "prop-types";
|
||||
import { isFunction } from "lodash";
|
||||
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
|
||||
import { SettingOutlined } from "@ant-design/icons";
|
||||
import InformationSettings from "./InformationSettings.jsx";
|
||||
import LayoutSettings from "./LayoutSettings.jsx";
|
||||
import StatisticsSettings from "./StatisticsSettings.jsx";
|
||||
|
||||
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
|
||||
const [form] = Form.useForm();
|
||||
@@ -87,7 +87,7 @@ function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bod
|
||||
};
|
||||
|
||||
const overlay = (
|
||||
<Card style={{ minWidth: "80vw" }}>
|
||||
<Card style={{ maxWidth: "80vw", width: "100%"}}>
|
||||
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
|
||||
@@ -100,26 +100,28 @@ const BoardContainer = ({
|
||||
const onLaneDrag = useCallback(
|
||||
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
|
||||
setIsDragging(false);
|
||||
setDragTime(source.droppableId);
|
||||
if (!type || type !== "lane" || !source || !destination || isEqual(source, destination)) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
// Only update drag time if it's a valid drop with a different destination
|
||||
if (type === "lane" && source && destination && !isEqual(source, destination)) {
|
||||
setDragTime(source.droppableId);
|
||||
setIsProcessing(true);
|
||||
|
||||
dispatch(
|
||||
actions.moveCardAcrossLanes({
|
||||
fromLaneId: source.droppableId,
|
||||
toLaneId: destination.droppableId,
|
||||
cardId: draggableId,
|
||||
index: destination.index
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
actions.moveCardAcrossLanes({
|
||||
fromLaneId: source.droppableId,
|
||||
toLaneId: destination.droppableId,
|
||||
cardId: draggableId,
|
||||
index: destination.index
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||
} catch (err) {
|
||||
console.error("Error in onLaneDrag", err);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
try {
|
||||
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||
} catch (err) {
|
||||
console.error("Error in onLaneDrag", err);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[dispatch, onDragEnd, setDragTime]
|
||||
|
||||
@@ -120,15 +120,14 @@ const Lane = ({
|
||||
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
|
||||
const FinalComponent = collapsed ? "div" : Component;
|
||||
const commonProps = {
|
||||
useWindowScroll: true,
|
||||
data: renderedCards
|
||||
data: renderedCards,
|
||||
customScrollParent: laneRef.current
|
||||
};
|
||||
|
||||
const verticalProps = {
|
||||
...commonProps,
|
||||
listClassName: "grid-container",
|
||||
itemClassName: "grid-item",
|
||||
customScrollParent: laneRef.current,
|
||||
components: {
|
||||
List: ListComponent,
|
||||
Item: ItemComponent
|
||||
@@ -142,7 +141,6 @@ const Lane = ({
|
||||
components: { Item: HeightPreservingItem },
|
||||
overscan: { main: 3, reverse: 3 },
|
||||
itemContent: (index, item) => renderDraggable(index, item),
|
||||
scrollerRef: provided.innerRef,
|
||||
style: {
|
||||
minWidth: maxCardWidth,
|
||||
minHeight: maxLaneHeight
|
||||
@@ -180,13 +178,14 @@ const Lane = ({
|
||||
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
|
||||
>
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
ref={laneRef} // Ensure laneRef is set here
|
||||
style={{ height: "100%", width: "100%" }} // Make it scrollable
|
||||
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
|
||||
style={{ ...provided.droppableProps.style }}
|
||||
>
|
||||
<FinalComponent {...finalComponentProps} />
|
||||
{shouldRenderPlaceholder && provided.placeholder}
|
||||
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...provided.droppableProps.style }}>
|
||||
<FinalComponent {...finalComponentProps} />
|
||||
{shouldRenderPlaceholder && provided.placeholder}
|
||||
</div>
|
||||
</div>
|
||||
</HeightMemoryWrapper>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
import { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import ProductionListTable from "./production-list-table.component";
|
||||
import _ from "lodash";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
|
||||
const client = useApolloClient();
|
||||
|
||||
@@ -8,7 +8,7 @@ import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { logImEXEvent, updateCurrentPassword } from "../../firebase/firebase.utils";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import NotificationSettingsForm from "../notification-settings/notification-settings-form.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
|
||||
@@ -54,6 +54,9 @@ export function ProfileShopsContainer({ bodyshop, currentUser }) {
|
||||
|
||||
//Force window refresh.
|
||||
|
||||
//Ping the new partner to refresh.
|
||||
axios.post("http://localhost:1337/refresh");
|
||||
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -8,16 +8,16 @@ import { calculateScheduleLoad } from "../../redux/application/application.actio
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import BlurWrapper from "../feature-wrapper/blur-wrapper.component";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import EmailInput from "../form-items-formatted/email-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||
import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container";
|
||||
import ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component";
|
||||
import "./schedule-job-modal.scss";
|
||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||
import BlurWrapper from "../feature-wrapper/blur-wrapper.component";
|
||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||
import "./schedule-job-modal.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -60,10 +60,12 @@ export function ScheduleJobModalComponent({
|
||||
const totalHours =
|
||||
lbrHrsData.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs + lbrHrsData.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
|
||||
if (values.start && !values.scheduled_completion)
|
||||
form.setFieldsValue({
|
||||
scheduled_completion: dayjs(values.start).businessDaysAdd(totalHours / bodyshop.target_touchtime, "day")
|
||||
});
|
||||
if (values.start && !values.scheduled_completion) {
|
||||
const addDays = bodyshop.ss_configuration.nobusinessdays
|
||||
? dayjs(values.start).add(totalHours / (bodyshop.target_touchtime || 1), "day")
|
||||
: dayjs(values.start).businessDaysAdd(totalHours / (bodyshop.target_touchtime || 1), "day");
|
||||
form.setFieldsValue({ scheduled_completion: addDays });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Card, Tabs } from "antd";
|
||||
import React from "react";
|
||||
@@ -24,6 +23,8 @@ import ShopInfoRoGuard from "./shop-info.roguard.component";
|
||||
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import ShopInfoNotificationsAutoadd from "./shop-info.notifications-autoadd.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -41,6 +42,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
||||
names: ["CriticalPartsScanning", "Enhanced_Payroll"],
|
||||
splitKey: bodyshop.imexshopid
|
||||
});
|
||||
const { scenarioNotificationsOn } = useSocket();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const history = useNavigate();
|
||||
@@ -137,9 +139,21 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
||||
|
||||
{
|
||||
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} />
|
||||
}
|
||||
},
|
||||
...(scenarioNotificationsOn
|
||||
? [
|
||||
{
|
||||
key: "notifications_autoadd",
|
||||
label: t("bodyshop.labels.notifications.followers"),
|
||||
children: <ShopInfoNotificationsAutoadd form={form} bodyshop={bodyshop} />
|
||||
}
|
||||
]
|
||||
: [])
|
||||
];
|
||||
return (
|
||||
<Card
|
||||
|
||||
@@ -906,6 +906,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
id="insurancecos-add-button"
|
||||
>
|
||||
{t("general.actions.add")}
|
||||
</Button>
|
||||
|
||||
@@ -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.id && typeof e.id === "string") || [];
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch, TimePicker } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component";
|
||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import { ColorPicker } from "./shop-info.rostatus.component";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
@@ -78,6 +77,13 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["ss_configuration", "nobusinessdays"]}
|
||||
label={t("bodyshop.fields.ss_configuration.nobusinessdays")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_lost_sale_reasons"]}
|
||||
label={t("bodyshop.fields.md_lost_sale_reasons")}
|
||||
|
||||
@@ -25,23 +25,6 @@ export function ShopTemplateTestRender({ bodyshop, query, emailEditorRef, style
|
||||
|
||||
emailEditorRef.current.exportHtml(async (data) => {
|
||||
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);
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Form, Input, InputNumber, Select, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { Card, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -19,6 +18,7 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
||||
import JobEmployeeAssignmentsContainer from "./../job-employee-assignments/job-employee-assignments.container";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -319,10 +319,15 @@ export function TimeTicketModalComponent({
|
||||
}
|
||||
|
||||
export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideTimeTickets = false }) {
|
||||
const { t } = useTranslation();
|
||||
if (loading) return <LoadingSkeleton />;
|
||||
if (!lineTicketData) return null;
|
||||
if (!jobid) return null;
|
||||
return (
|
||||
<div>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Card style={{ height: "100%" }} title={t("jobs.labels.employeeassignments")}>
|
||||
<JobEmployeeAssignmentsContainer job={lineTicketData.jobs_by_pk} />
|
||||
</Card>
|
||||
<LaborAllocationsTable
|
||||
jobId={jobid}
|
||||
joblines={lineTicketData.joblines}
|
||||
@@ -332,6 +337,6 @@ export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideT
|
||||
{!hideTimeTickets && (
|
||||
<TimeTicketList loading={loading} timetickets={jobid ? lineTicketData.timetickets : []} techConsole />
|
||||
)}
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Form, Modal, Space } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries";
|
||||
import { INSERT_NEW_TIME_TICKET, UPDATE_TIME_TICKET } from "../../graphql/timetickets.queries";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
@@ -14,7 +15,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import dayjs from "../../utils/day";
|
||||
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
|
||||
import TimeTicketModalComponent from "./time-ticket-modal.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
timeTicketModal: selectTimeTicket,
|
||||
@@ -81,7 +81,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
||||
}
|
||||
};
|
||||
|
||||
const handleMutationSuccess = (response) => {
|
||||
const handleMutationSuccess = () => {
|
||||
notification["success"]({
|
||||
message: t("timetickets.successes.created")
|
||||
});
|
||||
@@ -123,7 +123,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
||||
if (timeTicketModal.open) form.resetFields();
|
||||
}, [timeTicketModal.open, form]);
|
||||
|
||||
const handleFieldsChange = (changedFields, allFields) => {
|
||||
const handleFieldsChange = (changedFields) => {
|
||||
if (!!changedFields.employeeid && !!EmployeeAutoCompleteData) {
|
||||
const emps = EmployeeAutoCompleteData.employees.filter((e) => e.id === changedFields.employeeid);
|
||||
form.setFieldsValue({
|
||||
@@ -182,6 +182,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
||||
</Space>
|
||||
}
|
||||
destroyOnClose
|
||||
id="time-ticket-modal"
|
||||
>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
|
||||
@@ -113,7 +113,7 @@ export function UpdateAlert({ updateAvailable }) {
|
||||
</Col>
|
||||
<Col sm={24} md={8} lg={6}>
|
||||
<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")}
|
||||
</Button>
|
||||
<Button loading={loading} type="primary" onClick={() => ReloadNewVersion()}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
// SocketProvider.js
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import SocketIO from "socket.io-client";
|
||||
import { auth } from "../../firebase/firebase.utils";
|
||||
import { store } from "../../redux/store";
|
||||
@@ -15,10 +16,7 @@ import {
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const SocketContext = createContext(null);
|
||||
|
||||
const INITIAL_NOTIFICATIONS = 10;
|
||||
import { SocketContext, INITIAL_NOTIFICATIONS } from "./useSocket.js";
|
||||
|
||||
/**
|
||||
* Socket Provider - Scenario Notifications / Web Socket related items
|
||||
@@ -216,7 +214,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
};
|
||||
|
||||
const handleNotification = (data) => {
|
||||
// Scenario Notifications have been disabled, bail.
|
||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||
return;
|
||||
}
|
||||
@@ -336,7 +333,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
};
|
||||
|
||||
const handleSyncNotificationRead = ({ notificationId, timestamp }) => {
|
||||
// Scenario Notifications have been disabled, bail.
|
||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||
return;
|
||||
}
|
||||
@@ -378,7 +374,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
};
|
||||
|
||||
const handleSyncAllNotificationsRead = ({ timestamp }) => {
|
||||
// Scenario Notifications have been disabled, bail.
|
||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||
return;
|
||||
}
|
||||
@@ -490,11 +485,4 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const useSocket = () => {
|
||||
const context = useContext(SocketContext);
|
||||
// NOTE: Not sure if we absolutely require this, does cause slipups on dev env
|
||||
if (!context) throw new Error("useSocket must be used within a SocketProvider");
|
||||
return context;
|
||||
};
|
||||
|
||||
export { SocketContext, SocketProvider, INITIAL_NOTIFICATIONS, useSocket };
|
||||
export default SocketProvider;
|
||||
13
client/src/contexts/SocketIO/useSocket.js
Normal file
13
client/src/contexts/SocketIO/useSocket.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
const SocketContext = createContext(null);
|
||||
|
||||
const INITIAL_NOTIFICATIONS = 10;
|
||||
|
||||
const useSocket = () => {
|
||||
const context = useContext(SocketContext);
|
||||
if (!context) throw new Error("useSocket must be used within a SocketProvider");
|
||||
return context;
|
||||
};
|
||||
|
||||
export { SocketContext, INITIAL_NOTIFICATIONS, useSocket };
|
||||
@@ -57,6 +57,7 @@ export const QUERY_BODYSHOP = gql`
|
||||
logo_img_path
|
||||
md_ro_statuses
|
||||
md_order_statuses
|
||||
tours_enabled
|
||||
md_functionality_toggles
|
||||
shopname
|
||||
state
|
||||
@@ -140,6 +141,7 @@ export const QUERY_BODYSHOP = gql`
|
||||
use_paint_scale_data
|
||||
intellipay_config
|
||||
md_ro_guard
|
||||
notification_followers
|
||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||
id
|
||||
name
|
||||
@@ -186,6 +188,7 @@ export const UPDATE_SHOP = gql`
|
||||
phone
|
||||
federal_tax_id
|
||||
id
|
||||
tours_enabled
|
||||
insurance_vendor_id
|
||||
logo_img_path
|
||||
md_ro_statuses
|
||||
@@ -269,6 +272,7 @@ export const UPDATE_SHOP = gql`
|
||||
md_tasks_presets
|
||||
intellipay_config
|
||||
md_ro_guard
|
||||
notification_followers
|
||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -35,6 +35,30 @@ export const GET_LINE_TICKET_BY_PK = gql`
|
||||
lbr_adjustments
|
||||
converted
|
||||
status
|
||||
employee_body
|
||||
employee_body_rel {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
employee_csr
|
||||
employee_csr_rel {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
employee_prep
|
||||
employee_prep_rel {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
employee_refinish
|
||||
employee_refinish_rel {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
}
|
||||
joblines(where: { jobid: { _eq: $id }, removed: { _eq: false } }) {
|
||||
id
|
||||
|
||||
@@ -423,6 +423,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
actual_completion
|
||||
actual_delivery
|
||||
actual_in
|
||||
acv_amount
|
||||
adjustment_bottom_line
|
||||
alt_transport
|
||||
area_of_damage
|
||||
@@ -511,6 +512,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
est_ph1
|
||||
flat_rate_ats
|
||||
federal_tax_rate
|
||||
hit_and_run
|
||||
id
|
||||
inproduction
|
||||
ins_addr1
|
||||
@@ -683,6 +685,8 @@ export const GET_JOB_BY_PK = gql`
|
||||
scheduled_delivery
|
||||
scheduled_in
|
||||
selling_dealer
|
||||
estimate_approved
|
||||
estimate_sent_approval
|
||||
selling_dealer_contact
|
||||
servicing_dealer
|
||||
servicing_dealer_contact
|
||||
@@ -927,6 +931,8 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
||||
date_exported
|
||||
date_repairstarted
|
||||
date_scheduled
|
||||
estimate_sent_approval
|
||||
estimate_approved
|
||||
date_estimated
|
||||
employee_body_rel {
|
||||
id
|
||||
@@ -1075,6 +1081,8 @@ export const UPDATE_JOB = gql`
|
||||
date_repairstarted
|
||||
date_void
|
||||
date_lost_sale
|
||||
estimate_sent_approval
|
||||
estimate_approved
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2429,6 +2437,8 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
||||
plate_st
|
||||
po_number
|
||||
production_vars
|
||||
estimate_sent_approval
|
||||
estimate_approved
|
||||
ro_number
|
||||
scheduled_completion
|
||||
scheduled_delivery
|
||||
@@ -2570,6 +2580,20 @@ export const GET_JOB_BY_PK_QUICK_INTAKE = gql`
|
||||
actual_completion
|
||||
scheduled_delivery
|
||||
actual_delivery
|
||||
labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _eq: "LAR" } }, { removed: { _eq: false } }] }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -78,6 +78,9 @@ export const QUERY_PARTS_ORDER_OEC = gql`
|
||||
}
|
||||
ro_number
|
||||
clm_no
|
||||
cieca_stl
|
||||
cieca_ttl
|
||||
cieca_pfl
|
||||
asgn_no
|
||||
asgn_date
|
||||
state_tax_rate
|
||||
@@ -164,6 +167,7 @@ export const QUERY_PARTS_ORDER_OEC = gql`
|
||||
loss_desc
|
||||
loss_of_use
|
||||
loss_type
|
||||
materials
|
||||
ownr_addr1
|
||||
ownr_addr2
|
||||
ownr_city
|
||||
|
||||
@@ -91,6 +91,7 @@ export const QUERY_NOTIFICATION_SETTINGS = gql`
|
||||
associations(where: { _and: { useremail: { _eq: $email }, active: { _eq: true } } }) {
|
||||
id
|
||||
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);
|
||||
@@ -56,7 +56,7 @@ import { DateTimeFormat } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import JobWatcherToggleContainer from "../../components/job-watcher-toggle/job-watcher-toggle.container.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
|
||||
@@ -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 React, { lazy, Suspense, useEffect, useState } from "react";
|
||||
@@ -19,8 +20,6 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
|
||||
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
|
||||
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
||||
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
||||
import { requestForToken } from "../../firebase/firebase.utils";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
||||
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||
@@ -29,6 +28,7 @@ import WssStatusDisplayComponent from "../../components/wss-status-display/wss-s
|
||||
import { selectAlerts } from "../../redux/application/application.selectors.js";
|
||||
import { addAlerts } from "../../redux/application/application.actions.js";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
||||
|
||||
@@ -56,6 +56,7 @@ const ContractCreatePage = lazy(() => import("../contract-create/contract-create
|
||||
const ContractDetailPage = lazy(() => import("../contract-detail/contract-detail.page.container"));
|
||||
const ContractsList = lazy(() => import("../contracts/contracts.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 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]);
|
||||
|
||||
useEffect(() => {
|
||||
const widgetId = InstanceRenderManager({
|
||||
imex: "IABVNO4scRKY11XBQkNr",
|
||||
rome: "mQdqARMzkZRUVugJ6TdS"
|
||||
});
|
||||
window.noticeable.render("widget", widgetId);
|
||||
requestForToken().catch((error) => {
|
||||
console.error(`Unable to request for token.`, error);
|
||||
window.Canny("initChangelog", {
|
||||
appID: "680bd2c7ee501290377f6686",
|
||||
position: "top",
|
||||
align: "left",
|
||||
theme: "light" // options: light [default], dark, auto
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -480,6 +478,8 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
||||
// element={<ShopTemplates />}
|
||||
// />
|
||||
}
|
||||
<Route path="/feature-request/*" index element={<FeatureRequestPage />} />
|
||||
|
||||
<Route
|
||||
path="/shop/vendors"
|
||||
element={
|
||||
@@ -669,7 +669,12 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
||||
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 />
|
||||
<div onClick={broadcastMessage}>
|
||||
{`${InstanceRenderManager({
|
||||
@@ -677,8 +682,10 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
||||
rome: t("titles.romeonline")
|
||||
})} - ${import.meta.env.VITE_APP_GIT_SHA_DATE}`}
|
||||
</div>
|
||||
<div id="noticeable-widget" style={{ marginLeft: "1rem" }} />
|
||||
</div>
|
||||
<Button icon={<AlertOutlined />} data-canny-changelog type="text">
|
||||
{t("general.labels.changelog")}
|
||||
</Button>
|
||||
</Space>
|
||||
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
||||
Disclaimer & Notices
|
||||
</Link>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import React from "react";
|
||||
import AlertComponent from "../../components/alert/alert.component";
|
||||
import JobsDocumentsComponent from "../../components/jobs-documents-gallery/jobs-documents-gallery.component";
|
||||
import JobsDocumentsContainer from "../../components/jobs-documents-gallery/jobs-documents-gallery.container";
|
||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||
import { QUERY_TEMPORARY_DOCS } from "../../graphql/documents.queries";
|
||||
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -19,10 +20,18 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TemporaryDocsComponent);
|
||||
|
||||
export function TemporaryDocsComponent({ bodyshop }) {
|
||||
const {
|
||||
treatments: { Imgproxy }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Imgproxy"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(QUERY_TEMPORARY_DOCS, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
skip: bodyshop.uselocalmediaserver
|
||||
skip: Imgproxy.treatment === "on"
|
||||
});
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
@@ -32,12 +41,14 @@ export function TemporaryDocsComponent({ bodyshop }) {
|
||||
return <JobsDocumentsLocalGallery job={{ id: "temporary" }} />;
|
||||
}
|
||||
return (
|
||||
<JobsDocumentsComponent
|
||||
data={data ? data.documents : []}
|
||||
jobId={null}
|
||||
billId={null}
|
||||
refetch={refetch}
|
||||
ignoreSizeLimit
|
||||
/>
|
||||
<>
|
||||
<JobsDocumentsContainer
|
||||
documentsList={data ? data.documents : []}
|
||||
jobId={null}
|
||||
billId={null}
|
||||
refetchOverride={refetch}
|
||||
ignoreSizeLimit
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import FingerprintJS from "@fingerprintjs/fingerprintjs";
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { notification } from "antd";
|
||||
import axios from "axios";
|
||||
import { setUserId, setUserProperties } from "@firebase/analytics";
|
||||
import {
|
||||
checkActionCode,
|
||||
@@ -12,6 +9,9 @@ import {
|
||||
} from "@firebase/auth";
|
||||
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
||||
import { getToken } from "@firebase/messaging";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { notification } from "antd";
|
||||
import axios from "axios";
|
||||
import i18next from "i18next";
|
||||
import LogRocket from "logrocket";
|
||||
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
||||
@@ -340,6 +340,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
args: [],
|
||||
imex: () => {
|
||||
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]]);
|
||||
}
|
||||
@@ -350,7 +351,14 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
});
|
||||
payload.features?.allAccess === true
|
||||
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
||||
: window.$crisp.push(["set", "session:segments", [["basic"]]]);
|
||||
: (() => {
|
||||
const featureKeys = Object.keys(payload.features).filter(
|
||||
(key) =>
|
||||
payload.features[key] === true ||
|
||||
(typeof payload.features[key] === "string" && !isNaN(Date.parse(payload.features[key])))
|
||||
);
|
||||
window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]);
|
||||
})();
|
||||
} catch (error) {
|
||||
console.warn("Couldnt find $crisp.", error.message);
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user