Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2776-cdk-fortellis
# Conflicts: # client/src/components/dms-post-form/dms-post-form.component.jsx # package-lock.json # package.json # server/web-sockets/redisSocketEvents.js
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -128,3 +128,7 @@ vitest-coverage/
|
|||||||
*.vitest.log
|
*.vitest.log
|
||||||
test-output.txt
|
test-output.txt
|
||||||
server/job/test/fixtures
|
server/job/test/fixtures
|
||||||
|
|
||||||
|
.github
|
||||||
|
_reference/ragmate/.ragmate.env
|
||||||
|
docker_data
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
client_max_body_size 50M;
|
client_max_body_size 50M;
|
||||||
client_body_buffer_size 5M;
|
client_body_buffer_size 5M;
|
||||||
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",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.21.1",
|
"express": "^5.1.0",
|
||||||
"mailparser": "^3.7.1",
|
"mailparser": "^3.7.2",
|
||||||
"node-fetch": "^3.3.2"
|
"node-fetch": "^3.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -74,50 +74,8 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
<script>
|
<script>!function(w,d,i,s){function l(){if(!d.getElementById(i)){var f=d.getElementsByTagName(s)[0],e=d.createElement(s);e.type="text/javascript",e.async=!0,e.src="https://canny.io/sdk.js",f.parentNode.insertBefore(e,f)}}if("function"!=typeof w.Canny){var c=function(){c.q.push(arguments)};c.q=[],w.Canny=c,"complete"===d.readyState?l():w.attachEvent?w.attachEvent("onload",l):w.addEventListener("load",l,!1)}}(window,document,"canny-jssdk","script");</script>
|
||||||
!(function () {
|
|
||||||
"use strict";
|
|
||||||
var e = [
|
|
||||||
"debug",
|
|
||||||
"destroy",
|
|
||||||
"do",
|
|
||||||
"help",
|
|
||||||
"identify",
|
|
||||||
"is",
|
|
||||||
"off",
|
|
||||||
"on",
|
|
||||||
"ready",
|
|
||||||
"render",
|
|
||||||
"reset",
|
|
||||||
"safe",
|
|
||||||
"set"
|
|
||||||
];
|
|
||||||
if (window.noticeable) console.warn("Noticeable SDK code snippet loaded more than once");
|
|
||||||
else {
|
|
||||||
var n = (window.noticeable = window.noticeable || []);
|
|
||||||
|
|
||||||
function t(e) {
|
|
||||||
return function () {
|
|
||||||
var t = Array.prototype.slice.call(arguments);
|
|
||||||
return t.unshift(e), n.push(t), n;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
!(function () {
|
|
||||||
for (var o = 0; o < e.length; o++) {
|
|
||||||
var r = e[o];
|
|
||||||
n[r] = t(r);
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
(function () {
|
|
||||||
var e = document.createElement("script");
|
|
||||||
(e.async = !0), (e.src = "https://sdk.noticeable.io/l.js");
|
|
||||||
var n = document.head;
|
|
||||||
n.insertBefore(e, n.firstChild);
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
3778
client/package-lock.json
generated
3778
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,21 +12,21 @@
|
|||||||
"@apollo/client": "^3.13.6",
|
"@apollo/client": "^3.13.6",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||||
"@firebase/analytics": "^0.10.12",
|
"@firebase/analytics": "^0.10.16",
|
||||||
"@firebase/app": "^0.11.4",
|
"@firebase/app": "^0.13.1",
|
||||||
"@firebase/auth": "^1.10.0",
|
"@firebase/auth": "^1.10.6",
|
||||||
"@firebase/firestore": "^4.7.10",
|
"@firebase/firestore": "^4.7.17",
|
||||||
"@firebase/messaging": "^0.12.17",
|
"@firebase/messaging": "^0.12.21",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.6.1",
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"@sentry/cli": "^2.43.0",
|
"@sentry/cli": "^2.47.1",
|
||||||
"@sentry/react": "^9.11.0",
|
"@sentry/react": "^9.38.0",
|
||||||
"@sentry/vite-plugin": "^3.3.1",
|
"@sentry/vite-plugin": "^3.5.0",
|
||||||
"@splitsoftware/splitio-react": "^2.1.1",
|
"@splitsoftware/splitio-react": "^2.3.1",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"antd": "^5.24.6",
|
"antd": "^5.25.4",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^4.2.0",
|
"apollo-link-sentry": "^4.3.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -37,28 +37,29 @@
|
|||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.11.0",
|
||||||
"i18next": "^24.2.3",
|
"i18next": "^24.2.3",
|
||||||
"i18next-browser-languagedetector": "^8.0.4",
|
"i18next-browser-languagedetector": "^8.1.0",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.12.6",
|
"libphonenumber-js": "^1.12.10",
|
||||||
"logrocket": "^9.0.2",
|
"logrocket": "^9.0.2",
|
||||||
"markerjs2": "^2.32.4",
|
"markerjs2": "^2.32.4",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-url": "^8.0.1",
|
"normalize-url": "^8.0.2",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
|
"phone": "^3.1.59",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.1.1",
|
"query-string": "^9.2.0",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.18.0",
|
"react-big-calendar": "^1.19.2",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^8.0.1",
|
"react-cookie": "^8.0.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-drag-listview": "^2.0.0",
|
"react-drag-listview": "^2.0.0",
|
||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "^1.3.4",
|
"react-grid-layout": "1.3.4",
|
||||||
"react-i18next": "^15.4.1",
|
"react-i18next": "^15.5.2",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^6.30.0",
|
"react-router-dom": "^6.30.0",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtuoso": "^4.12.5",
|
"react-virtuoso": "^4.12.8",
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
@@ -77,9 +78,9 @@
|
|||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.86.3",
|
"sass": "^1.89.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.17",
|
"styled-components": "^6.1.18",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"use-memo-one": "^1.1.3",
|
"use-memo-one": "^1.1.3",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
@@ -129,18 +130,18 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^6.0.0",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.27.1",
|
||||||
"@dotenvx/dotenvx": "^1.39.1",
|
"@dotenvx/dotenvx": "^1.47.5",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.24.0",
|
"@eslint/js": "^9.31.0",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.54.1",
|
||||||
"@sentry/webpack-plugin": "^3.3.1",
|
"@sentry/webpack-plugin": "^3.5.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.25.0",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
@@ -148,19 +149,19 @@
|
|||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"memfs": "^4.17.0",
|
"memfs": "^4.17.2",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"playwright": "^1.51.1",
|
"playwright": "^1.54.1",
|
||||||
"react-error-overlay": "^6.1.0",
|
"react-error-overlay": "^6.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^6.2.5",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-babel": "^1.3.0",
|
"vite-plugin-babel": "^1.3.1",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
"vite-plugin-pwa": "^1.0.0",
|
"vite-plugin-pwa": "^1.0.0",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"vitest": "^3.1.1",
|
"vitest": "^3.2.3",
|
||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,19 @@ import { ApolloProvider } from "@apollo/client";
|
|||||||
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
||||||
import { ConfigProvider } from "antd";
|
import { ConfigProvider } from "antd";
|
||||||
import enLocale from "antd/es/locale/en_US";
|
import enLocale from "antd/es/locale/en_US";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSelector } from "react-redux";
|
import { connect, useSelector } from "react-redux";
|
||||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||||
import client from "../utils/GraphQLClient";
|
import client from "../utils/GraphQLClient";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import themeProvider from "./themeProvider";
|
import getTheme from "./themeProvider";
|
||||||
import { CookiesProvider } from "react-cookie";
|
import { CookiesProvider } from "react-cookie";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectCurrentUser } from "../redux/user/user.selectors.js";
|
||||||
|
import { selectDarkMode } from "../redux/application/application.selectors";
|
||||||
|
import { setDarkMode } from "../redux/application/application.actions";
|
||||||
|
|
||||||
// Base Split configuration
|
// Base Split configuration
|
||||||
const config = {
|
const config = {
|
||||||
@@ -24,19 +28,54 @@ const config = {
|
|||||||
function SplitClientProvider({ children }) {
|
function SplitClientProvider({ children }) {
|
||||||
const imexshopid = useSelector((state) => state.user.imexshopid); // Access imexshopid from Redux store
|
const imexshopid = useSelector((state) => state.user.imexshopid); // Access imexshopid from Redux store
|
||||||
const splitClient = useSplitClient({ key: imexshopid || "anon" }); // Use imexshopid or fallback to "anon"
|
const splitClient = useSplitClient({ key: imexshopid || "anon" }); // Use imexshopid or fallback to "anon"
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (splitClient && imexshopid) {
|
if (splitClient && imexshopid) {
|
||||||
// Log readiness for debugging; no need for ready() since isReady is available
|
|
||||||
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
||||||
}
|
}
|
||||||
}, [splitClient, imexshopid]);
|
}, [splitClient, imexshopid]);
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppContainer() {
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setDarkMode: (isDarkMode) => dispatch(setDarkMode(isDarkMode))
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
currentUser: selectCurrentUser
|
||||||
|
});
|
||||||
|
|
||||||
|
function AppContainer({ currentUser, setDarkMode }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const isDarkMode = useSelector(selectDarkMode);
|
||||||
|
const theme = useMemo(() => getTheme(isDarkMode), [isDarkMode]);
|
||||||
|
|
||||||
|
// Update data-theme attribute when dark mode changes
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.setAttribute("data-theme", isDarkMode ? "dark" : "light");
|
||||||
|
return () => document.documentElement.removeAttribute("data-theme");
|
||||||
|
}, [isDarkMode]);
|
||||||
|
|
||||||
|
// Sync Redux darkMode with localStorage on user change
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser?.uid) {
|
||||||
|
const savedMode = localStorage.getItem(`dark-mode-${currentUser.uid}`);
|
||||||
|
if (savedMode !== null) {
|
||||||
|
setDarkMode(JSON.parse(savedMode));
|
||||||
|
} else {
|
||||||
|
setDarkMode(false); // default to light mode
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setDarkMode(false);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [currentUser?.uid]);
|
||||||
|
|
||||||
|
// Persist darkMode to localStorage when it or user changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser?.uid) {
|
||||||
|
localStorage.setItem(`dark-mode-${currentUser.uid}`, JSON.stringify(isDarkMode));
|
||||||
|
}
|
||||||
|
}, [isDarkMode, currentUser?.uid]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CookiesProvider>
|
<CookiesProvider>
|
||||||
@@ -44,11 +83,10 @@ function AppContainer() {
|
|||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
input={{ autoComplete: "new-password" }}
|
input={{ autoComplete: "new-password" }}
|
||||||
locale={enLocale}
|
locale={enLocale}
|
||||||
theme={themeProvider}
|
theme={theme}
|
||||||
form={{
|
form={{
|
||||||
validateMessages: {
|
validateMessages: {
|
||||||
// eslint-disable-next-line no-template-curly-in-string
|
required: t("general.validation.required", { label: "{{label}}" })
|
||||||
required: t("general.validation.required", { label: "${label}" })
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -64,4 +102,4 @@ function AppContainer() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Sentry.withProfiler(AppContainer);
|
export default Sentry.withProfiler(connect(mapStateToProps, mapDispatchToProps)(AppContainer));
|
||||||
|
|||||||
@@ -1,15 +1,226 @@
|
|||||||
//Global Styles.
|
:root {
|
||||||
|
--table-stripe-bg: #f4f4f4; /* Light mode table stripe */
|
||||||
|
--menu-divider-color: #74695c; /* Light mode menu divider */
|
||||||
|
--menu-submenu-text: rgba(255, 255, 255, 0.65); /* Light mode submenu text */
|
||||||
|
--kanban-column-bg: #ddd; /* Light mode kanban column */
|
||||||
|
--alert-color: blue; /* Light mode alert */
|
||||||
|
--completion-soon-color: rgba(255, 140, 0, 0.8); /* Light mode completion soon */
|
||||||
|
--completion-past-color: rgba(255, 0, 0, 0.8); /* Light mode completion past */
|
||||||
|
--job-line-manual-color: tomato; /* Light mode job line manual */
|
||||||
|
--muted-button-color: lightgray; /* Light mode muted button */
|
||||||
|
--muted-button-hover-color: darkgrey; /* Light mode muted button hover */
|
||||||
|
--table-border-color: #ddd; /* Light mode table border */
|
||||||
|
--table-hover-bg: #f5f5f5; /* Light mode table hover */
|
||||||
|
--popover-bg: #fff; /* Light mode popover background */
|
||||||
|
--error-text: red; /* Light mode error message */
|
||||||
|
--no-jobs-text: #888; /* Light mode no jobs message */
|
||||||
|
--message-yours-bg: #eee; /* Light mode yours message background */
|
||||||
|
--message-mine-bg-start: #00d0ea; /* Light mode mine message gradient start */
|
||||||
|
--message-mine-bg-end: #0085d1; /* Light mode mine message gradient end */
|
||||||
|
--message-mine-text: white; /* Light mode mine message text */
|
||||||
|
--message-mine-tail-bg: white; /* Light mode mine/yours message tail */
|
||||||
|
--system-message-bg: #f5f5f5; /* Light mode system message background */
|
||||||
|
--system-message-text: #555; /* Light mode system message text */
|
||||||
|
--system-label-text: #888; /* Light mode system label/date text */
|
||||||
|
--message-icon-color: whitesmoke; /* Light mode message icon */
|
||||||
|
--eula-card-bg: lightgray; /* Light mode eula card background */
|
||||||
|
--notification-bg: #fff; /* Light mode notification background */
|
||||||
|
--notification-text: rgba(0, 0, 0, 0.85); /* Light mode notification text */
|
||||||
|
--notification-border: #d9d9d9; /* Light mode notification border */
|
||||||
|
--notification-header-bg: #fafafa; /* Light mode notification header background */
|
||||||
|
--notification-header-border: #f0f0f0; /* Light mode notification header border */
|
||||||
|
--notification-header-text: rgba(0, 0, 0, 0.85); /* Light mode notification header text */
|
||||||
|
--notification-toggle-icon: #1677ff; /* Light mode notification toggle icon */
|
||||||
|
--notification-switch-bg: #1677ff; /* Light mode notification switch background */
|
||||||
|
--notification-btn-link: #1677ff; /* Light mode notification link button */
|
||||||
|
--notification-btn-link-hover: #69b1ff; /* Light mode notification link button hover */
|
||||||
|
--notification-btn-link-disabled: rgba(0, 0, 0, 0.25); /* Light mode notification link button disabled */
|
||||||
|
--notification-btn-link-active: #0958d9; /* Light mode notification link button active */
|
||||||
|
--notification-read-bg: #fff; /* Light mode notification read background */
|
||||||
|
--notification-read-text: rgba(0, 0, 0, 0.65); /* Light mode notification read text */
|
||||||
|
--notification-unread-bg: #f5f5f5; /* Light mode notification unread background */
|
||||||
|
--notification-unread-text: rgba(0, 0, 0, 0.85); /* Light mode notification unread text */
|
||||||
|
--notification-item-hover-bg: #fafafa; /* Light mode notification item hover background */
|
||||||
|
--notification-ro-number: #1677ff; /* Light mode notification RO number */
|
||||||
|
--notification-relative-time: rgba(0, 0, 0, 0.45); /* Light mode notification relative time */
|
||||||
|
--alert-bg: #fff1f0; /* Light mode alert background */
|
||||||
|
--alert-text: rgba(0, 0, 0, 0.85); /* Light mode alert text */
|
||||||
|
--alert-border: #ffa39e; /* Light mode alert border */
|
||||||
|
--alert-message: #ff4d4f; /* Light mode alert message */
|
||||||
|
--share-badge-bg: #cccccc; /* Light mode share badge background */
|
||||||
|
--column-header-bg: #d0d0d0; /* Light mode column header background */
|
||||||
|
--footer-bg: #d0d0d0; /* Light mode footer background */
|
||||||
|
--tech-icon-color: orangered; /* Light mode tech icon color */
|
||||||
|
--clone-border-color: #1890ff; /* Light mode clone border color */
|
||||||
|
--event-arrived-bg: rgba(4, 141, 4, 0.4); /* Light mode arrived event background */
|
||||||
|
--event-block-bg: tomato; /* Light mode block event background */
|
||||||
|
--event-selected-bg: slategrey; /* Light mode selected event background */
|
||||||
|
--task-bg: #fff; /* Light mode task center background */
|
||||||
|
--task-text: rgba(0, 0, 0, 0.85); /* Light mode task text */
|
||||||
|
--task-border: #d9d9d9; /* Light mode task border */
|
||||||
|
--task-header-bg: #fafafa; /* Light mode task header background */
|
||||||
|
--task-header-border: #f0f0f0; /* Light mode task header border */
|
||||||
|
--task-section-bg: #f5f5f5; /* Light mode task section background */
|
||||||
|
--task-section-border: #e8e8e8; /* Light mode task section border */
|
||||||
|
--task-row-hover-bg: #f5f5f5; /* Light mode task row hover background */
|
||||||
|
--task-row-border: #f0f0f0; /* Light mode task row border */
|
||||||
|
--task-ro-number: #1677ff; /* Light mode task RO number */
|
||||||
|
--task-due-text: rgba(0, 0, 0, 0.45); /* Light mode task due text */
|
||||||
|
--task-button-bg: #1677ff; /* Light mode task button background */
|
||||||
|
--task-button-hover-bg: #4096ff; /* Light mode task button hover background */
|
||||||
|
--task-button-disabled-bg: #d9d9d9; /* Light mode task button disabled background */
|
||||||
|
--task-button-text: white; /* Light mode task button text */
|
||||||
|
--task-message-text: rgba(0, 0, 0, 0.45); /* Light mode task message text */
|
||||||
|
--mask-bg: rgba(0, 0, 0, 0.05); /* Light mode mask background */
|
||||||
|
--board-text-color: #393939; /* Light mode board text color */
|
||||||
|
--section-bg: #e3e3e3; /* Light mode section background */
|
||||||
|
--detail-text-color: #4d4d4d; /* Light mode detail text color */
|
||||||
|
--card-selected-bg: rgba(128, 128, 128, 0.2); /* Light mode selected card background */
|
||||||
|
--card-stripe-even-bg: #f0f2f5; /* Light mode even card background */
|
||||||
|
--card-stripe-odd-bg: #ffffff; /* Light mode odd card background */
|
||||||
|
--bar-border-color: #f0f2f5; /* Light mode bar border and background */
|
||||||
|
--tag-wrapper-bg: #f0f2f5; /* Light mode tag wrapper background */
|
||||||
|
--tag-wrapper-text: #000; /* Light mode tag wrapper text */
|
||||||
|
--preview-bg: lightgray; /* Light mode preview background */
|
||||||
|
--preview-border-color: #2196F3; /* Light mode preview border color */
|
||||||
|
--event-bg-fallback: #c4c4c4; /* Light mode event background fallback */
|
||||||
|
--card-bg-fallback: #ffffff; /* Light mode card background fallback */
|
||||||
|
--card-text-fallback: black; /* Light mode card text fallback */
|
||||||
|
--table-row-even-bg: rgb(236, 236, 236); /* Light mode table row even background */
|
||||||
|
--status-row-bg-fallback: #ffffff; /* Light mode status row fallback background */
|
||||||
|
--reset-link-color: #0000ff; /* Light mode reset link color */
|
||||||
|
--error-header-text: tomato; /* Light mode error header text */
|
||||||
|
--tooltip-bg: white; /* Light mode tooltip background */
|
||||||
|
--tooltip-border: gray; /* Light mode tooltip border */
|
||||||
|
--tooltip-text-fallback: black; /* Light mode tooltip text fallback */
|
||||||
|
--teams-button-bg: #6264A7; /* Light mode Teams button background */
|
||||||
|
--teams-button-border: #6264A7; /* Light mode Teams button border */
|
||||||
|
--teams-button-text: #FFFFFF; /* Light mode Teams button text and icon */
|
||||||
|
--content-bg: #fff; /* Light mode content background */
|
||||||
|
--legend-bg-fallback: #ffffff; /* Light mode legend background fallback */
|
||||||
|
--tech-content-bg: #fff; /* Light mode tech content background */
|
||||||
|
--today-bg: #ffffff; /* Light mode today background */
|
||||||
|
--today-text: #000000; /* Light mode today text */
|
||||||
|
--off-range-bg: #f8f8f8; /* Light mode off-range background */
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--table-stripe-bg: #2a2a2a; /* Dark mode table stripe */
|
||||||
|
--menu-divider-color: #5c5c5c; /* Dark mode menu divider */
|
||||||
|
--menu-submenu-text: rgba(255, 255, 255, 0.85); /* Dark mode submenu text */
|
||||||
|
--kanban-column-bg: #333333; /* Dark mode kanban column */
|
||||||
|
--alert-color: #4da8ff; /* Dark mode alert */
|
||||||
|
--completion-soon-color: #ff8c1a; /* Dark mode completion soon */
|
||||||
|
--completion-past-color: #ff4d4f; /* Dark mode completion past */
|
||||||
|
--job-line-manual-color: #ff6347; /* Dark mode job line manual */
|
||||||
|
--muted-button-color: #666666; /* Dark mode muted button */
|
||||||
|
--muted-button-hover-color: #999999; /* Dark mode muted button hover */
|
||||||
|
--table-border-color: #5c5c5c; /* Dark mode table border */
|
||||||
|
--table-hover-bg: #2a2a2a; /* Dark mode table hover */
|
||||||
|
--popover-bg: #2a2a2a; /* Dark mode popover background */
|
||||||
|
--error-text: #ff4d4f; /* Dark mode error message */
|
||||||
|
--no-jobs-text: #999999; /* Dark mode no jobs message */
|
||||||
|
--message-yours-bg: #2a2a2a; /* Dark mode yours message background */
|
||||||
|
--message-mine-bg-start: #4da8ff; /* Dark mode mine message gradient start */
|
||||||
|
--message-mine-bg-end: #326ade; /* Dark mode mine message gradient end */
|
||||||
|
--message-mine-text: #ffffff; /* Dark mode mine message text */
|
||||||
|
--message-mine-tail-bg: #1f1f1f; /* Dark mode mine/yours message tail */
|
||||||
|
--system-message-bg: #333333; /* Dark mode system message background */
|
||||||
|
--system-message-text: #cccccc; /* Dark mode system message text */
|
||||||
|
--system-label-text: #999999; /* Dark mode system label/date text */
|
||||||
|
--message-icon-color: #cccccc; /* Dark mode message icon */
|
||||||
|
--eula-card-bg: #2a2a2a; /* Dark mode eula card background */
|
||||||
|
--notification-bg: #2a2a2a; /* Dark mode notification background */
|
||||||
|
--notification-text: rgba(255, 255, 255, 0.85); /* Dark mode notification text */
|
||||||
|
--notification-border: #5c5c5c; /* Dark mode notification border */
|
||||||
|
--notification-header-bg: #333333; /* Dark mode notification header background */
|
||||||
|
--notification-header-border: #444444; /* Dark mode notification header border */
|
||||||
|
--notification-header-text: rgba(255, 255, 255, 0.85); /* Dark mode notification header text */
|
||||||
|
--notification-toggle-icon: #4da8ff; /* Dark mode notification toggle icon */
|
||||||
|
--notification-switch-bg: #4da8ff; /* Dark mode notification switch background */
|
||||||
|
--notification-btn-link: #4da8ff; /* Dark mode notification link button */
|
||||||
|
--notification-btn-link-hover: #80c1ff; /* Dark mode notification link button hover */
|
||||||
|
--notification-btn-link-disabled: rgba(255, 255, 255, 0.25); /* Dark mode notification link button disabled */
|
||||||
|
--notification-btn-link-active: #2681ff; /* Dark mode notification link button active */
|
||||||
|
--notification-read-bg: #2a2a2a; /* Dark mode notification read background */
|
||||||
|
--notification-read-text: rgba(255, 255, 255, 0.65); /* Dark mode notification read text */
|
||||||
|
--notification-unread-bg: #333333; /* Dark mode notification unread background */
|
||||||
|
--notification-unread-text: rgba(255, 255, 255, 0.85); /* Dark mode notification unread text */
|
||||||
|
--notification-item-hover-bg: #3a3a3a; /* Dark mode notification item hover background */
|
||||||
|
--notification-ro-number: #4da8ff; /* Dark mode notification RO number */
|
||||||
|
--notification-relative-time: rgba(255, 255, 255, 0.45); /* Dark mode notification relative time */
|
||||||
|
--alert-bg: #3a1a1a; /* Dark mode alert background */
|
||||||
|
--alert-text: rgba(255, 255, 255, 0.85); /* Dark mode alert text */
|
||||||
|
--alert-border: #ff6666; /* Dark mode alert border */
|
||||||
|
--alert-message: #ff6666; /* Dark mode alert message */
|
||||||
|
--share-badge-bg: #666666; /* Dark mode share badge background */
|
||||||
|
--column-header-bg: #333333; /* Dark mode column header background */
|
||||||
|
--footer-bg: #333333; /* Dark mode footer background */
|
||||||
|
--tech-icon-color: #ff4500; /* Dark mode tech icon color */
|
||||||
|
--clone-border-color: #4da8ff; /* Dark mode clone border color */
|
||||||
|
--event-arrived-bg: rgba(4, 141, 4, 0.6); /* Dark mode arrived event background */
|
||||||
|
--event-block-bg: tomato; /* Dark mode block event background */
|
||||||
|
--event-selected-bg: #4a5e6e; /* Dark mode selected event background */
|
||||||
|
--task-bg: #2a2a2a; /* Dark mode task center background */
|
||||||
|
--task-text: rgba(255, 255, 255, 0.85); /* Dark mode task text */
|
||||||
|
--task-border: #5c5c5c; /* Dark mode task border */
|
||||||
|
--task-header-bg: #333333; /* Dark mode task header background */
|
||||||
|
--task-header-border: #444444; /* Dark mode task header border */
|
||||||
|
--task-section-bg: #333333; /* Dark mode task section background */
|
||||||
|
--task-section-border: #444444; /* Dark mode task section border */
|
||||||
|
--task-row-hover-bg: #3a3a3a; /* Dark mode task row hover background */
|
||||||
|
--task-row-border: #444444; /* Dark mode task row border */
|
||||||
|
--task-ro-number: #4da8ff; /* Dark mode task RO number */
|
||||||
|
--task-due-text: rgba(255, 255, 255, 0.45); /* Dark mode task due text */
|
||||||
|
--task-button-bg: #4da8ff; /* Dark mode task button background */
|
||||||
|
--task-button-hover-bg: #80c1ff; /* Dark mode task button hover background */
|
||||||
|
--task-button-disabled-bg: #666666; /* Dark mode task button disabled background */
|
||||||
|
--task-button-text: #ffffff; /* Dark mode task button text */
|
||||||
|
--task-message-text: rgba(255, 255, 255, 0.45); /* Dark mode task message text */
|
||||||
|
--mask-bg: rgba(255, 255, 255, 0.05); /* Dark mode mask background */
|
||||||
|
--board-text-color: #cccccc; /* Dark mode board text color */
|
||||||
|
--section-bg: #333333; /* Dark mode section background */
|
||||||
|
--detail-text-color: #bbbbbb; /* Dark mode detail text color */
|
||||||
|
--card-selected-bg: rgba(255, 255, 255, 0.1); /* Dark mode selected card background */
|
||||||
|
--card-stripe-even-bg: #2a2a2a; /* Dark mode even card background */
|
||||||
|
--card-stripe-odd-bg: #1f1f1f; /* Dark mode odd card background */
|
||||||
|
--bar-border-color: #2a2a2a; /* Dark mode bar border and background */
|
||||||
|
--tag-wrapper-bg: #2a2a2a; /* Dark mode tag wrapper background */
|
||||||
|
--tag-wrapper-text: #cccccc; /* Dark mode tag wrapper text */
|
||||||
|
--preview-bg: #2a2a2a; /* Dark mode preview background */
|
||||||
|
--preview-border-color: #4da8ff; /* Dark mode preview border color */
|
||||||
|
--event-bg-fallback: #262626; /* Dark mode event background fallback */
|
||||||
|
--card-bg-fallback: #2a2a2a; /* Dark mode card background fallback */
|
||||||
|
--card-text-fallback: #cccccc; /* Dark mode card text fallback */
|
||||||
|
--table-row-even-bg: #2a2a2a; /* Dark mode table row even background */
|
||||||
|
--status-row-bg-fallback: #1f1f1f; /* Dark mode status row fallback background */
|
||||||
|
--reset-link-color: #4da8ff; /* Dark mode reset link color */
|
||||||
|
--error-header-text: #ff6347; /* Dark mode error header text */
|
||||||
|
--tooltip-bg: #2a2a2a; /* Dark mode tooltip background */
|
||||||
|
--tooltip-border: #5c5c5c; /* Dark mode tooltip border */
|
||||||
|
--tooltip-text-fallback: #cccccc; /* Dark mode tooltip text fallback */
|
||||||
|
--teams-button-bg: #7b7dc4; /* Dark mode Teams button background */
|
||||||
|
--teams-button-border: #7b7dc4; /* Dark mode Teams button border */
|
||||||
|
--teams-button-text: #ffffff; /* Dark mode Teams button text and icon */
|
||||||
|
--content-bg: #2a2a2a; /* Dark mode content background */
|
||||||
|
--legend-bg-fallback: #2a2a2a; /* Dark mode legend background fallback */
|
||||||
|
--tech-content-bg: #2a2a2a; /* Dark mode tech content background */
|
||||||
|
--today-bg: #4a5e6e; /* Dark mode today background */
|
||||||
|
--today-text: #ffffff; /* Dark mode today text */
|
||||||
|
--off-range-bg: #333333; /* Dark mode off-range background */
|
||||||
|
--svg-background: #FFF; /* Dark mode SVG background */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global Styles
|
||||||
@import "react-big-calendar/lib/sass/styles";
|
@import "react-big-calendar/lib/sass/styles";
|
||||||
|
|
||||||
.ant-menu-item-divider {
|
.ant-menu-item-divider {
|
||||||
border-bottom: 1px solid #74695c !important;
|
border-bottom: 1px solid var(--menu-divider-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This was added because the newest release of ant was making the text color and the background color the same on a selected header
|
// Note: Monitor this in dark mode to ensure text visibility
|
||||||
// Tried all available tokens (https://ant.design/components/menu?locale=en-US) and even reverted all our custom styles, to no avail
|
|
||||||
// This should be kept an eye on, especially if implementing DARK MODE
|
|
||||||
.ant-menu-submenu-title {
|
.ant-menu-submenu-title {
|
||||||
color: rgba(255, 255, 255, 0.65) !important;
|
color: var(--menu-submenu-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.imex-table-header {
|
.imex-table-header {
|
||||||
@@ -46,7 +257,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ellipses {
|
.ellipses {
|
||||||
display: inline-block; /* for em, a, span, etc (inline by default) */
|
display: inline-block;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
width: calc(95%);
|
width: calc(95%);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -60,23 +271,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ::-webkit-scrollbar-track {
|
// Scrollbar styles (uncomment if needed, updated for dark mode)
|
||||||
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
::-webkit-scrollbar-track {
|
||||||
// border-radius: 0.2rem;
|
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
// background-color: #f5f5f5;
|
border-radius: 0.2rem;
|
||||||
// }
|
background-color: var(--table-stripe-bg);
|
||||||
|
}
|
||||||
|
|
||||||
// ::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
// width: 0.25rem;
|
width: 0.25rem;
|
||||||
// max-height: 0.25rem;
|
max-height: 0.25rem;
|
||||||
// background-color: #f5f5f5;
|
background-color: var(--table-stripe-bg);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// ::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
// border-radius: 0.2rem;
|
border-radius: 0.2rem;
|
||||||
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
// background-color: #188fff;
|
background-color: var(--alert-color);
|
||||||
// }
|
}
|
||||||
|
|
||||||
.ant-input-number-input,
|
.ant-input-number-input,
|
||||||
.ant-input-number,
|
.ant-input-number,
|
||||||
@@ -88,28 +300,27 @@
|
|||||||
|
|
||||||
.production-alert {
|
.production-alert {
|
||||||
animation: alertBlinker 1s linear infinite;
|
animation: alertBlinker 1s linear infinite;
|
||||||
color: blue;
|
color: var(--alert-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes alertBlinker {
|
@keyframes alertBlinker {
|
||||||
50% {
|
50% {
|
||||||
color: red;
|
color: var(--completion-past-color);
|
||||||
opacity: 100;
|
opacity: 100;
|
||||||
//opacity: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.blue {
|
.blue {
|
||||||
color: blue;
|
color: var(--alert-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.production-completion-soon {
|
.production-completion-soon {
|
||||||
color: rgba(255, 140, 0, 0.8);
|
color: var(--completion-soon-color);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.production-completion-past {
|
.production-completion-past {
|
||||||
color: rgba(255, 0, 0, 0.8);
|
color: var(--completion-past-color);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +350,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.react-kanban-column {
|
.react-kanban-column {
|
||||||
background-color: #ddd !important;
|
background-color: var(--kanban-column-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.production-list-table {
|
.production-list-table {
|
||||||
@@ -151,18 +362,18 @@
|
|||||||
.ReactGridGallery_tile-icon-bar {
|
.ReactGridGallery_tile-icon-bar {
|
||||||
div {
|
div {
|
||||||
svg {
|
svg {
|
||||||
fill: #1890ff;
|
fill: var(--alert-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-line-manual {
|
.job-line-manual {
|
||||||
color: tomato;
|
color: var(--job-line-manual-color);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
|
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
|
||||||
background-color: #f4f4f4;
|
background-color: var(--table-stripe-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rowWithColor > td {
|
.rowWithColor > td {
|
||||||
@@ -170,15 +381,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.muted-button {
|
.muted-button {
|
||||||
color: lightgray;
|
color: var(--muted-button-color);
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px; /* Adjust as needed */
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted-button:hover {
|
.muted-button:hover {
|
||||||
color: darkgrey;
|
color: var(--muted-button-hover-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-alert-unordered-list {
|
.notification-alert-unordered-list {
|
||||||
@@ -190,3 +401,27 @@
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override react-big-calendar styles for dark mode only
|
||||||
|
[data-theme="dark"] {
|
||||||
|
.car-svg {
|
||||||
|
background-color: var(--svg-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-today {
|
||||||
|
background-color: var(--today-bg);
|
||||||
|
color: var(--today-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-off-range {
|
||||||
|
background-color: var(--off-range-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-day-bg.rbc-today {
|
||||||
|
background-color: var(--today-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//.rbc-time-header-gutter {
|
||||||
|
// padding: 0;
|
||||||
|
//}
|
||||||
|
|||||||
@@ -4,36 +4,42 @@ import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
|||||||
|
|
||||||
const { defaultAlgorithm, darkAlgorithm } = theme;
|
const { defaultAlgorithm, darkAlgorithm } = theme;
|
||||||
|
|
||||||
let isDarkMode = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default theme
|
* Default theme
|
||||||
* @type {{components: {Menu: {itemDividerBorderColor: string}}}}
|
* @type {{components: {Menu: {itemDividerBorderColor: string}}}}
|
||||||
*/
|
*/
|
||||||
const defaultTheme = {
|
const defaultTheme = (isDarkMode) => ({
|
||||||
components: {
|
components: {
|
||||||
Table: {
|
Table: {
|
||||||
rowHoverBg: "#e7f3ff",
|
rowHoverBg: isDarkMode ? "#2a2a2a" : "#e7f3ff",
|
||||||
rowSelectedBg: "#e6f7ff",
|
rowSelectedBg: isDarkMode ? "#333333" : "#e6f7ff",
|
||||||
headerSortHoverBg: "transparent"
|
headerSortHoverBg: "transparent"
|
||||||
},
|
},
|
||||||
Menu: {
|
Menu: {
|
||||||
darkItemHoverBg: "#1890ff",
|
darkItemHoverBg: isDarkMode ? "#004a77" : "#1890ff",
|
||||||
itemHoverBg: "#1890ff",
|
itemHoverBg: isDarkMode ? "#004a77" : "#1890ff",
|
||||||
horizontalItemHoverBg: "#1890ff"
|
horizontalItemHoverBg: isDarkMode ? "#004a77" : "#1890ff"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: InstanceRenderMgr({
|
colorPrimary: InstanceRenderMgr(
|
||||||
imex: "#1890ff",
|
{
|
||||||
rome: "#326ade"
|
imex: isDarkMode ? "#4da8ff" : "#1890ff",
|
||||||
}),
|
rome: isDarkMode ? "#5b8ce6" : "#326ade"
|
||||||
colorInfo: InstanceRenderMgr({
|
},
|
||||||
imex: "#1890ff",
|
isDarkMode
|
||||||
rome: "#326ade"
|
),
|
||||||
})
|
colorInfo: InstanceRenderMgr(
|
||||||
|
{
|
||||||
|
imex: isDarkMode ? "#4da8ff" : "#1890ff",
|
||||||
|
rome: isDarkMode ? "#5b8ce6" : "#326ade"
|
||||||
|
},
|
||||||
|
isDarkMode
|
||||||
|
),
|
||||||
|
colorError: isDarkMode ? "#ff4d4f" : "#f5222d",
|
||||||
|
colorBgBase: isDarkMode ? "#1f1f1f" : "#ffffff" // Align with Ant Design dark mode
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Development theme
|
* Development theme
|
||||||
@@ -60,8 +66,9 @@ const prodTheme = {};
|
|||||||
|
|
||||||
const currentTheme = import.meta.env.DEV ? devTheme : prodTheme;
|
const currentTheme = import.meta.env.DEV ? devTheme : prodTheme;
|
||||||
|
|
||||||
const finaltheme = {
|
const getTheme = (isDarkMode) => ({
|
||||||
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
||||||
...defaultsDeep(currentTheme, defaultTheme)
|
...defaultsDeep(currentTheme, defaultTheme)
|
||||||
};
|
});
|
||||||
export default finaltheme;
|
|
||||||
|
export default getTheme;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Card, Checkbox, Input, Space, Table } from "antd";
|
import { Card, Checkbox, Input, Space, Table } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -16,12 +16,13 @@ import PayableExportAll from "../payable-export-all-button/payable-export-all-bu
|
|||||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||||
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
||||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
|
import useLocalStorage from "./../../utils/useLocalStorage";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedBills, setSelectedBills] = useState([]);
|
const [selectedBills, setSelectedBills] = useState([]);
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useLocalStorage("accounting-payables-table-state", {
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
search: ""
|
search: ""
|
||||||
});
|
});
|
||||||
@@ -181,7 +182,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
|
|||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
onSelectAll: (selected, selectedRows) => setSelectedBills(selectedRows.map((i) => i.id)),
|
onSelectAll: (selected, selectedRows) => setSelectedBills(selectedRows.map((i) => i.id)),
|
||||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
onSelect: (record, selected, selectedRows) => {
|
||||||
setSelectedBills(selectedRows.map((i) => i.id));
|
setSelectedBills(selectedRows.map((i) => i.id));
|
||||||
},
|
},
|
||||||
getCheckboxProps: (record) => ({
|
getCheckboxProps: (record) => ({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Card, Input, Space, Table } from "antd";
|
import { Card, Input, Space, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -10,6 +10,7 @@ import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
|||||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { exportPageLimit } from "../../utils/config";
|
import { exportPageLimit } from "../../utils/config";
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
||||||
@@ -21,7 +22,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedPayments, setSelectedPayments] = useState([]);
|
const [selectedPayments, setSelectedPayments] = useState([]);
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useLocalStorage("accounting-payments-table-state", {
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
search: ""
|
search: ""
|
||||||
});
|
});
|
||||||
@@ -194,7 +195,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
|
|||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
onSelectAll: (selected, selectedRows) => setSelectedPayments(selectedRows.map((i) => i.id)),
|
onSelectAll: (selected, selectedRows) => setSelectedPayments(selectedRows.map((i) => i.id)),
|
||||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
onSelect: (record, selected, selectedRows) => {
|
||||||
setSelectedPayments(selectedRows.map((i) => i.id));
|
setSelectedPayments(selectedRows.map((i) => i.id));
|
||||||
},
|
},
|
||||||
getCheckboxProps: (record) => ({
|
getCheckboxProps: (record) => ({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button, Card, Input, Space, Table } from "antd";
|
import { Button, Card, Input, Space, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -10,6 +10,7 @@ import { exportPageLimit } from "../../utils/config";
|
|||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||||
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||||
@@ -20,7 +21,7 @@ import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent);
|
||||||
@@ -30,7 +31,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
|||||||
const [selectedJobs, setSelectedJobs] = useState([]);
|
const [selectedJobs, setSelectedJobs] = useState([]);
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useLocalStorage("accounting-receivables-table-state", {
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
search: ""
|
search: ""
|
||||||
});
|
});
|
||||||
@@ -207,7 +208,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
|||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
onSelectAll: (selected, selectedRows) => setSelectedJobs(selectedRows.map((i) => i.id)),
|
onSelectAll: (selected, selectedRows) => setSelectedJobs(selectedRows.map((i) => i.id)),
|
||||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
onSelect: (record, selected, selectedRows) => {
|
||||||
setSelectedJobs(selectedRows.map((i) => i.id));
|
setSelectedJobs(selectedRows.map((i) => i.id));
|
||||||
},
|
},
|
||||||
getCheckboxProps: (record) => ({
|
getCheckboxProps: (record) => ({
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
td {
|
td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid var(--table-border-color);
|
||||||
|
|
||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
margin-bottom: 0px !important;
|
margin-bottom: 0px !important;
|
||||||
@@ -14,6 +14,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr:hover {
|
tr:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: var(--table-hover-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,21 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
setPartsOrderContext: (context) =>
|
||||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "partsOrder"
|
||||||
|
})
|
||||||
|
),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
|
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
|
||||||
@@ -69,7 +82,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, b
|
|||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
title={t("bills.actions.return")}
|
title={t("bills.actions.return")}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function BillDetailEditcontainer() {
|
|||||||
delete search.billid;
|
delete search.billid;
|
||||||
history({ search: queryString.stringify(search) });
|
history({ search: queryString.stringify(search) });
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
open={search.billid}
|
open={search.billid}
|
||||||
>
|
>
|
||||||
<BillDetailEditComponent />
|
<BillDetailEditComponent />
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { useApolloClient, useMutation } from "@apollo/client";
|
|||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Checkbox, Form, Modal, Space } from "antd";
|
import { Button, Checkbox, Form, Modal, Space } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
||||||
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
|
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
|
||||||
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||||
@@ -24,7 +25,7 @@ import BillFormContainer from "../bill-form/bill-form.container";
|
|||||||
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||||
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
||||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { handleUpload as handleUploadToImageProxy } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
billEnterModal: selectBillEnterModal,
|
billEnterModal: selectBillEnterModal,
|
||||||
@@ -53,10 +54,10 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treatments: { Enhanced_Payroll }
|
treatments: { Enhanced_Payroll, Imgproxy }
|
||||||
} = useSplitTreatments({
|
} = useSplitTreatments({
|
||||||
attributes: {},
|
attributes: {},
|
||||||
names: ["Enhanced_Payroll"],
|
names: ["Enhanced_Payroll", "Imgproxy"],
|
||||||
splitKey: bodyshop.imexshopid
|
splitKey: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,7 +197,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
job: { lbr_adjustments: newAdjustments }
|
job: { lbr_adjustments: newAdjustments }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!!jobUpdate.errors) {
|
if (jobUpdate.errors) {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("jobs.errors.saving", {
|
message: t("jobs.errors.saving", {
|
||||||
message: JSON.stringify(jobUpdate.errors)
|
message: JSON.stringify(jobUpdate.errors)
|
||||||
@@ -213,7 +214,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
variables: { partsLineIds: markPolReceived.map((p) => p.id) },
|
variables: { partsLineIds: markPolReceived.map((p) => p.id) },
|
||||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"]
|
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"]
|
||||||
});
|
});
|
||||||
if (!!r2.errors) {
|
if (r2.errors) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setEnterAgain(false);
|
setEnterAgain(false);
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
@@ -224,7 +225,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!r1.errors) {
|
if (r1.errors) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setEnterAgain(false);
|
setEnterAgain(false);
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
@@ -244,7 +245,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
consumedbybillid: billId
|
consumedbybillid: billId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!!r2.errors) {
|
if (r2.errors) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setEnterAgain(false);
|
setEnterAgain(false);
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
@@ -298,20 +299,39 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
upload.forEach((u) => {
|
//Check if using Imgproxy or cloudinary
|
||||||
handleUpload(
|
|
||||||
{ file: u.originFileObj },
|
if (Imgproxy.treatment === "on") {
|
||||||
{
|
upload.forEach((u) => {
|
||||||
bodyshop: bodyshop,
|
handleUploadToImageProxy(
|
||||||
uploaded_by: currentUser.email,
|
{ file: u.originFileObj },
|
||||||
jobId: values.jobid,
|
{
|
||||||
billId: billId,
|
bodyshop: bodyshop,
|
||||||
tagsArray: null,
|
uploaded_by: currentUser.email,
|
||||||
callback: null
|
jobId: values.jobid,
|
||||||
},
|
billId: billId,
|
||||||
notification
|
tagsArray: null,
|
||||||
);
|
callback: null
|
||||||
});
|
},
|
||||||
|
notification
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
upload.forEach((u) => {
|
||||||
|
handleUpload(
|
||||||
|
{ file: u.originFileObj },
|
||||||
|
{
|
||||||
|
bodyshop: bodyshop,
|
||||||
|
uploaded_by: currentUser.email,
|
||||||
|
jobId: values.jobid,
|
||||||
|
billId: billId,
|
||||||
|
tagsArray: null,
|
||||||
|
callback: null
|
||||||
|
},
|
||||||
|
notification
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
///////////////////////////
|
///////////////////////////
|
||||||
@@ -396,7 +416,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
{t("bills.labels.generatepartslabel")}
|
{t("bills.labels.generatepartslabel")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
||||||
<Button loading={loading} onClick={() => form.submit()}>
|
<Button loading={loading} onClick={() => form.submit()} id="save-bill-enter-modal">
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
{billEnterModal.context && billEnterModal.context.id ? null : (
|
{billEnterModal.context && billEnterModal.context.id ? null : (
|
||||||
@@ -406,13 +426,14 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEnterAgain(true);
|
setEnterAgain(true);
|
||||||
}}
|
}}
|
||||||
|
id="save-and-new-bill-enter-modal"
|
||||||
>
|
>
|
||||||
{t("general.actions.saveandnew")}
|
{t("general.actions.saveandnew")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
td {
|
td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid var(--table-border-color);
|
||||||
|
|
||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
margin-bottom: 0px !important;
|
margin-bottom: 0px !important;
|
||||||
@@ -14,6 +14,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr:hover {
|
tr:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: var(--table-hover-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Select } from "antd";
|
import { Select } from "antd";
|
||||||
import React, { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
|||||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
|
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
|
||||||
label: (
|
label: (
|
||||||
<div style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
|
<div style={{ whiteSpace: "normal", wordBreak: "break-word" }}>
|
||||||
<span>
|
<span>
|
||||||
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FaTasks } from "react-icons/fa";
|
import { FaTasks } from "react-icons/fa";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -209,6 +209,7 @@ export function BillsListTableComponent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
id="reconcile-bills-button"
|
||||||
>
|
>
|
||||||
<LockerWrapperComponent featureName="bills"> {t("jobs.actions.reconcile")}</LockerWrapperComponent>
|
<LockerWrapperComponent featureName="bills"> {t("jobs.actions.reconcile")}</LockerWrapperComponent>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisi
|
|||||||
title={t("payments.labels.findermodal")}
|
title={t("payments.labels.findermodal")}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
onOk={() => toggleModalVisible()}
|
onOk={() => toggleModalVisible()}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
forceRender
|
forceRender
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Button, Form, InputNumber, Popover, Space } from "antd";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
|
||||||
export default function CABCpvrtCalculator({ disabled, form }) {
|
export default function CABCpvrtCalculator({ disabled, form }) {
|
||||||
const [visibility, setVisibility] = useState(false);
|
const [visibility, setVisibility] = useState(false);
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
|
<Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
|
||||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||||
<CalculatorFilled />
|
<CalculatorFilled />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodys
|
|||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
width="80%"
|
width="80%"
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<CardPaymentModalComponent />
|
<CardPaymentModalComponent />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -34,16 +34,14 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
|||||||
|
|
||||||
SubscribeToTopicForFCMNotification();
|
SubscribeToTopicForFCMNotification();
|
||||||
|
|
||||||
//Register WS handlers
|
// Register WebSocket handlers
|
||||||
if (socket && socket.connected) {
|
if (socket && socket.connected) {
|
||||||
registerMessagingHandlers({ socket, client });
|
registerMessagingHandlers({ socket, client });
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (socket && socket.connected) {
|
|
||||||
unregisterMessagingHandlers({ socket });
|
unregisterMessagingHandlers({ socket });
|
||||||
}
|
};
|
||||||
};
|
}
|
||||||
}, [bodyshop, socket, t, client]);
|
}, [bodyshop, socket, t, client]);
|
||||||
|
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||||
|
|||||||
@@ -202,8 +202,6 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
text: message.text
|
text: message.text
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add cases for other known message types as needed
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Log a warning for unhandled message types
|
// Log a warning for unhandled message types
|
||||||
logLocal("handleMessageChanged - Unhandled message type", { type: message.type });
|
logLocal("handleMessageChanged - Unhandled message type", { type: message.type });
|
||||||
@@ -211,7 +209,7 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageRef; // Keep other messages unchanged
|
return messageRef;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,11 +243,8 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updatedList = existingList?.conversations
|
const updatedList = existingList?.conversations
|
||||||
? [
|
? [newConversation, ...existingList.conversations.filter((conv) => conv.id !== newConversation.id)]
|
||||||
newConversation,
|
: [newConversation]; // Prevent duplicates
|
||||||
...existingList.conversations.filter((conv) => conv.id !== newConversation.id) // Prevent duplicates
|
|
||||||
]
|
|
||||||
: [newConversation];
|
|
||||||
|
|
||||||
client.cache.writeQuery({
|
client.cache.writeQuery({
|
||||||
query: CONVERSATION_LIST_QUERY,
|
query: CONVERSATION_LIST_QUERY,
|
||||||
@@ -403,6 +398,7 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logLocal("handleConversationChanged - Unhandled type", { type });
|
logLocal("handleConversationChanged - Unhandled type", { type });
|
||||||
client.cache.modify({
|
client.cache.modify({
|
||||||
@@ -419,10 +415,95 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Existing handler for phone number opt-out
|
||||||
|
const handlePhoneNumberOptedOut = async (data) => {
|
||||||
|
const { bodyshopid, phone_number } = data;
|
||||||
|
logLocal("handlePhoneNumberOptedOut - Start", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.cache.modify({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fields: {
|
||||||
|
phone_number_opt_out(existing = [], { readField }) {
|
||||||
|
const phoneNumberExists = existing.some(
|
||||||
|
(ref) => readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid
|
||||||
|
);
|
||||||
|
|
||||||
|
if (phoneNumberExists) {
|
||||||
|
logLocal("handlePhoneNumberOptedOut - Phone number already in cache", { phone_number, bodyshopid });
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newOptOut = {
|
||||||
|
__typename: "phone_number_opt_out",
|
||||||
|
id: `temporary-${phone_number}-${Date.now()}`,
|
||||||
|
bodyshopid,
|
||||||
|
phone_number,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
return [...existing, newOptOut];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcast: true
|
||||||
|
});
|
||||||
|
|
||||||
|
client.cache.evict({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fieldName: "phone_number_opt_out",
|
||||||
|
args: { bodyshopid, search: phone_number }
|
||||||
|
});
|
||||||
|
client.cache.gc();
|
||||||
|
|
||||||
|
logLocal("handlePhoneNumberOptedOut - Cache updated successfully", data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating cache for phone number opt-out:", error);
|
||||||
|
logLocal("handlePhoneNumberOptedOut - Error", { error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// New handler for phone number opt-in
|
||||||
|
const handlePhoneNumberOptedIn = async (data) => {
|
||||||
|
const { bodyshopid, phone_number } = data;
|
||||||
|
logLocal("handlePhoneNumberOptedIn - Start", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update the Apollo cache for GET_PHONE_NUMBER_OPT_OUTS by removing the phone number
|
||||||
|
client.cache.modify({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fields: {
|
||||||
|
phone_number_opt_out(existing = [], { readField }) {
|
||||||
|
// Filter out the phone number from the opt-out list
|
||||||
|
return existing.filter(
|
||||||
|
(ref) => !(readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcast: true // Trigger UI updates
|
||||||
|
});
|
||||||
|
|
||||||
|
// Evict the cache entry to force a refetch on next query
|
||||||
|
client.cache.evict({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fieldName: "phone_number_opt_out",
|
||||||
|
args: { bodyshopid, search: phone_number }
|
||||||
|
});
|
||||||
|
client.cache.gc();
|
||||||
|
|
||||||
|
logLocal("handlePhoneNumberOptedIn - Cache updated successfully", data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating cache for phone number opt-in:", error);
|
||||||
|
logLocal("handlePhoneNumberOptedIn - Error", { error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
socket.on("new-message-summary", handleNewMessageSummary);
|
socket.on("new-message-summary", handleNewMessageSummary);
|
||||||
socket.on("new-message-detailed", handleNewMessageDetailed);
|
socket.on("new-message-detailed", handleNewMessageDetailed);
|
||||||
socket.on("message-changed", handleMessageChanged);
|
socket.on("message-changed", handleMessageChanged);
|
||||||
socket.on("conversation-changed", handleConversationChanged);
|
socket.on("conversation-changed", handleConversationChanged);
|
||||||
|
socket.on("phone-number-opted-out", handlePhoneNumberOptedOut);
|
||||||
|
socket.on("phone-number-opted-in", handlePhoneNumberOptedIn);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unregisterMessagingHandlers = ({ socket }) => {
|
export const unregisterMessagingHandlers = ({ socket }) => {
|
||||||
@@ -431,4 +512,6 @@ export const unregisterMessagingHandlers = ({ socket }) => {
|
|||||||
socket.off("new-message-detailed");
|
socket.off("new-message-detailed");
|
||||||
socket.off("message-changed");
|
socket.off("message-changed");
|
||||||
socket.off("conversation-changed");
|
socket.off("conversation-changed");
|
||||||
|
socket.off("phone-number-opted-out");
|
||||||
|
socket.off("phone-number-opted-in");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Badge, Card, List, Space, Tag } from "antd";
|
import { Badge, Card, List, Space, Tag, Tooltip } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -9,44 +9,65 @@ import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
|||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||||
import "./chat-conversation-list.styles.scss";
|
import "./chat-conversation-list.styles.scss";
|
||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries.js";
|
||||||
|
import { phone } from "phone";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation
|
selectedConversation: selectSelectedConversation,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
||||||
});
|
});
|
||||||
|
|
||||||
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) {
|
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) {
|
||||||
// That comma is there for a reason, do not remove it
|
const { t } = useTranslation();
|
||||||
const [, forceUpdate] = useState(false);
|
const [, forceUpdate] = useState(false);
|
||||||
|
const phoneNumbers = conversationList.map((item) => phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""));
|
||||||
|
const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
|
||||||
|
variables: {
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
phone_numbers: phoneNumbers
|
||||||
|
},
|
||||||
|
skip: !conversationList.length,
|
||||||
|
fetchPolicy: "cache-and-network"
|
||||||
|
});
|
||||||
|
|
||||||
|
const optOutMap = useMemo(() => {
|
||||||
|
const map = new Map();
|
||||||
|
optOutData?.phone_number_opt_out?.forEach((optOut) => {
|
||||||
|
map.set(optOut.phone_number, true);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [optOutData?.phone_number_opt_out]);
|
||||||
|
|
||||||
// Re-render every minute
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
forceUpdate((prev) => !prev); // Toggle state to trigger re-render
|
forceUpdate((prev) => !prev);
|
||||||
}, 60000); // 1 minute in milliseconds
|
}, 60000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
return () => clearInterval(interval); // Cleanup on unmount
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Memoize the sorted conversation list
|
const sortedConversationList = useMemo(() => {
|
||||||
const sortedConversationList = React.useMemo(() => {
|
|
||||||
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
|
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
|
||||||
}, [conversationList]);
|
}, [conversationList]);
|
||||||
|
|
||||||
const renderConversation = (index) => {
|
const renderConversation = (index, t) => {
|
||||||
const item = sortedConversationList[index];
|
const item = sortedConversationList[index];
|
||||||
|
const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
|
||||||
|
const hasOptOutEntry = optOutMap.has(normalizedPhone);
|
||||||
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||||
const cardContentLeft =
|
const cardContentLeft =
|
||||||
item.job_conversations.length > 0
|
item.job_conversations.length > 0
|
||||||
? item.job_conversations.map((j, idx) => <Tag key={idx}>{j.job.ro_number}</Tag>)
|
? item.job_conversations.map((j, idx) => <Tag key={idx}>{j.job.ro_number}</Tag>)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const names = <>{_.uniq(item.job_conversations.map((j, idx) => OwnerNameDisplayFunction(j.job)))}</>;
|
const names = <>{_.uniq(item.job_conversations.map((j, idx) => OwnerNameDisplayFunction(j.job)))}</>;
|
||||||
|
|
||||||
const cardTitle = (
|
const cardTitle = (
|
||||||
<>
|
<>
|
||||||
{item.label && <Tag color="blue">{item.label}</Tag>}
|
{item.label && <Tag color="blue">{item.label}</Tag>}
|
||||||
@@ -59,13 +80,22 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
const cardExtra = (
|
||||||
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count} />;
|
<>
|
||||||
|
<Badge count={item.messages_aggregate.aggregate.count} />
|
||||||
|
{hasOptOutEntry && (
|
||||||
|
<Tooltip title={t("consent.text_body")}>
|
||||||
|
<Tag color="red" icon={<ExclamationCircleOutlined />}>
|
||||||
|
{t("messaging.labels.no_consent")}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
const getCardStyle = () =>
|
const getCardStyle = () =>
|
||||||
item.id === selectedConversation
|
item.id === selectedConversation
|
||||||
? { backgroundColor: "rgba(128, 128, 128, 0.2)" }
|
? { backgroundColor: "var(--card-selected-bg)" }
|
||||||
: { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
|
: { backgroundColor: index % 2 === 0 ? "var(--card-stripe-even-bg)" : "var(--card-stripe-odd-bg)" };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
@@ -73,9 +103,25 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
onClick={() => setSelectedConversation(item.id)}
|
onClick={() => setSelectedConversation(item.id)}
|
||||||
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
||||||
>
|
>
|
||||||
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
<Card style={getCardStyle()} variant={true} size="small" extra={cardExtra} title={cardTitle}>
|
||||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
<div
|
||||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: "70%",
|
||||||
|
textAlign: "left"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cardContentLeft}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: "30%",
|
||||||
|
textAlign: "right"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cardContentRight}
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
);
|
);
|
||||||
@@ -85,7 +131,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
<div className="chat-list-container">
|
<div className="chat-list-container">
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
data={sortedConversationList}
|
data={sortedConversationList}
|
||||||
itemContent={(index) => renderConversation(index)}
|
itemContent={(index) => renderConversation(index, t)}
|
||||||
style={{ height: "100%", width: "100%" }}
|
style={{ height: "100%", width: "100%" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
/* Add spacing and better alignment for items */
|
/* Add spacing and better alignment for items */
|
||||||
.chat-list-item {
|
.chat-list-item {
|
||||||
padding: 0.5rem 0; /* Add spacing between list items */
|
padding: 0.2rem 0; /* Add spacing between list items */
|
||||||
|
|
||||||
.ant-card {
|
.ant-card {
|
||||||
border-radius: 8px; /* Slight rounding for card edges */
|
border-radius: 8px; /* Slight rounding for card edges */
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
|||||||
userid
|
userid
|
||||||
created_at
|
created_at
|
||||||
read
|
read
|
||||||
|
is_system
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
data: message
|
data: message
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-document
|
|||||||
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
|
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
|
||||||
import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
import "./chat-media-selector.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
});
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
||||||
|
|
||||||
export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
|
export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
|
||||||
@@ -37,9 +38,8 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
variables: {
|
variables: {
|
||||||
jobId: conversation.job_conversations[0] && conversation.job_conversations[0].jobid
|
jobId: conversation.job_conversations[0]?.jobid
|
||||||
},
|
},
|
||||||
|
|
||||||
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
|
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,25 +56,25 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
//If Imageproxy is on, rely only on the LMS selector
|
//If Imageproxy is on, rely only on the LMS selector
|
||||||
//If not on, use the old methods.
|
//If not on, use the old methods.
|
||||||
const content = (
|
const content = (
|
||||||
<div>
|
<div className="media-selector-content">
|
||||||
{loading && <LoadingSpinner />}
|
{loading && <LoadingSpinner />}
|
||||||
{error && <AlertComponent message={error.message} type="error" />}
|
{error && <AlertComponent message={error.message} type="error" />}
|
||||||
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
||||||
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
<div className="error-message">{t("messaging.labels.maxtenimages")}</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{Imgproxy.treatment === "on" ? (
|
{Imgproxy.treatment === "on" ? (
|
||||||
<>
|
<>
|
||||||
{!bodyshop.uselocalmediaserver && (
|
{!bodyshop.uselocalmediaserver && (
|
||||||
<JobsDocumentImgproxyGalleryExternal
|
<JobsDocumentImgproxyGalleryExternal
|
||||||
jobId={conversation.job_conversations[0].jobid}
|
jobId={conversation.job_conversations[0]?.jobid}
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{bodyshop.uselocalmediaserver && open && (
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
<JobDocumentsLocalGalleryExternal
|
<JobDocumentsLocalGalleryExternal
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
jobId={conversation.job_conversations[0]?.jobid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -89,7 +89,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
{bodyshop.uselocalmediaserver && open && (
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
<JobDocumentsLocalGalleryExternal
|
<JobDocumentsLocalGalleryExternal
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
jobId={conversation.job_conversations[0]?.jobid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -100,12 +100,17 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
content={
|
content={
|
||||||
conversation.job_conversations.length === 0 ? <div>{t("messaging.errors.noattachedjobs")}</div> : content
|
conversation.job_conversations.length === 0 ? (
|
||||||
|
<div className="no-jobs-message">{t("messaging.errors.noattachedjobs")}</div>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)
|
||||||
}
|
}
|
||||||
title={t("messaging.labels.selectmedia")}
|
title={t("messaging.labels.selectmedia")}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={handleVisibleChange}
|
onOpenChange={handleVisibleChange}
|
||||||
|
classNames={{ root: "media-selector-popover" }}
|
||||||
>
|
>
|
||||||
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
||||||
<PictureFilled style={{ margin: "0 .5rem" }} />
|
<PictureFilled style={{ margin: "0 .5rem" }} />
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
.media-selector-popover {
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
position: relative;
|
||||||
|
max-width: 640px;
|
||||||
|
max-height: 480px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: var(--popover-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-selector-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: var(--error-text);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-jobs-message {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--no-jobs-text);
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style images within gallery components */
|
||||||
|
.media-selector-content img {
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid layout for gallery components */
|
||||||
|
.media-selector-content .ant-image,
|
||||||
|
.media-selector-content .gallery-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
@@ -4,13 +4,16 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.archive-button {
|
.archive-button {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-title {
|
.chat-title {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages {
|
.messages {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -37,17 +40,18 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.chat-send-message-button{
|
|
||||||
|
.chat-send-message-button {
|
||||||
margin: 0.3rem;
|
margin: 0.3rem;
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-icon {
|
.message-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0.1rem;
|
bottom: 0.1rem;
|
||||||
right: 0.3rem;
|
right: 0.3rem;
|
||||||
margin: 0 0.1rem;
|
margin: 0 0.1rem;
|
||||||
color: whitesmoke;
|
color: var(--message-icon-color);
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +79,7 @@
|
|||||||
|
|
||||||
&:last-child:after {
|
&:last-child:after {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
background: white;
|
background: var(--message-mine-tail-bg);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,11 +91,11 @@
|
|||||||
|
|
||||||
.message {
|
.message {
|
||||||
margin-right: 20%;
|
margin-right: 20%;
|
||||||
background-color: #eee;
|
background-color: var(--message-yours-bg);
|
||||||
|
|
||||||
&:last-child:before {
|
&:last-child:before {
|
||||||
left: -7px;
|
left: -7px;
|
||||||
background: #eee;
|
background: var(--message-yours-bg);
|
||||||
border-bottom-right-radius: 15px;
|
border-bottom-right-radius: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,14 +111,14 @@
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
color: white;
|
color: var(--message-mine-text);
|
||||||
margin-left: 25%;
|
margin-left: 25%;
|
||||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
background: linear-gradient(to bottom, var(--message-mine-bg-start) 0%, var(--message-mine-bg-end) 100%);
|
||||||
padding-bottom: 0.6rem;
|
padding-bottom: 0.6rem;
|
||||||
|
|
||||||
&:last-child:before {
|
&:last-child:before {
|
||||||
right: -8px;
|
right: -8px;
|
||||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
background: linear-gradient(to bottom, var(--message-mine-bg-start) 0%, var(--message-mine-bg-end) 100%);
|
||||||
border-bottom-left-radius: 15px;
|
border-bottom-left-radius: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +129,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.system {
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.5rem 10%;
|
||||||
|
|
||||||
|
.message {
|
||||||
|
background-color: var(--system-message-bg);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--system-message-text);
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--system-label-text);
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--system-label-text);
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.virtuoso-container {
|
.virtuoso-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@@ -2,17 +2,29 @@ import Icon from "@ant-design/icons";
|
|||||||
import { Tooltip } from "antd";
|
import { Tooltip } from "antd";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import { MdDone, MdDoneAll } from "react-icons/md";
|
import { MdClose, MdDone, MdDoneAll } from "react-icons/md";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
export const renderMessage = (messages, index) => {
|
export const renderMessage = (messages, index) => {
|
||||||
const message = messages[index];
|
const message = messages[index];
|
||||||
|
const isSystem = message.is_system;
|
||||||
|
|
||||||
|
// Determine message class
|
||||||
|
const messageClass = isSystem ? "system messages" : message.isoutbound ? "mine messages" : "yours messages";
|
||||||
|
|
||||||
|
// Tooltip content based on message type
|
||||||
|
const tooltipTitle = isSystem ? (
|
||||||
|
i18n.t("consent.text_body")
|
||||||
|
) : (
|
||||||
|
<DateTimeFormatter>{message.created_at}</DateTimeFormatter>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className={`${message.isoutbound ? "mine messages" : "yours messages"}`}>
|
<div key={index} className={messageClass}>
|
||||||
<div className="message msgmargin">
|
<div className="message msgmargin">
|
||||||
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
<Tooltip title={tooltipTitle}>
|
||||||
<div>
|
<div>
|
||||||
|
{isSystem && <span className="system-label">System</span>}
|
||||||
{/* Render images if available */}
|
{/* Render images if available */}
|
||||||
{message.image && message.image_path?.length > 0 && (
|
{message.image && message.image_path?.length > 0 && (
|
||||||
<div className="message-images">
|
<div className="message-images">
|
||||||
@@ -26,20 +38,31 @@ export const renderMessage = (messages, index) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Render text if available */}
|
{/* Render text if available */}
|
||||||
{message.text && <div>{message.text}</div>}
|
{message.text && <div className="message-text">{message.text}</div>}
|
||||||
|
{/* Render date for system messages */}
|
||||||
|
{isSystem && (
|
||||||
|
<div className="system-date">
|
||||||
|
<DateTimeFormatter>{message.created_at}</DateTimeFormatter>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Message status icons */}
|
{/* Message status icons for non-system messages */}
|
||||||
{message.status && (message.status === "sent" || message.status === "delivered") && (
|
{!isSystem &&
|
||||||
<div className="message-status">
|
message.status &&
|
||||||
<Icon component={message.status === "sent" ? MdDone : MdDoneAll} className="message-icon" />
|
(message.status === "sent" || message.status === "delivered" || message.status === "failed") && (
|
||||||
</div>
|
<div className="message-status">
|
||||||
)}
|
<Icon
|
||||||
|
component={message.status === "sent" ? MdDone : message.status === "delivered" ? MdDoneAll : MdClose}
|
||||||
|
className="message-icon"
|
||||||
|
style={message.status === "failed" ? { color: "#ff0000" } : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Outbound message metadata for non-system messages */}
|
||||||
{/* Outbound message metadata */}
|
{!isSystem && message.isoutbound && (
|
||||||
{message.isoutbound && (
|
|
||||||
<div style={{ fontSize: 10 }}>
|
<div style={{ fontSize: 10 }}>
|
||||||
{i18n.t("messaging.labels.sentby", {
|
{i18n.t("messaging.labels.sentby", {
|
||||||
by: message.userid,
|
by: message.userid,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
import { ExclamationCircleOutlined, LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
||||||
import { Input, Spin } from "antd";
|
import { Alert, Input, Space, Spin, Tooltip } from "antd";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -10,6 +10,9 @@ import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
|
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
|
||||||
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { phone } from "phone";
|
||||||
|
import { GET_PHONE_NUMBER_OPT_OUT } from "../../graphql/phone-number-opt-out.queries";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -25,16 +28,24 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
||||||
const inputArea = useRef(null);
|
const inputArea = useRef(null);
|
||||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const normalizedPhone = phone(conversation.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
|
||||||
|
const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUT, {
|
||||||
|
variables: { bodyshopid: bodyshop.id, phone_number: normalizedPhone },
|
||||||
|
fetchPolicy: "cache-and-network"
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOptedOut = !!optOutData?.phone_number_opt_out?.[0];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputArea.current.focus();
|
inputArea.current.focus();
|
||||||
}, [isSending, setMessage]);
|
}, [isSending, setMessage]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleEnter = () => {
|
const handleEnter = () => {
|
||||||
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
||||||
if ((message === "" || !message) && selectedImages.length === 0) return;
|
if ((message === "" || !message) && selectedImages.length === 0) return;
|
||||||
|
if (isOptedOut) return; // Prevent sending if phone number is opted out
|
||||||
logImEXEvent("messaging_send_message");
|
logImEXEvent("messaging_send_message");
|
||||||
|
|
||||||
if (selectedImages.length < 11) {
|
if (selectedImages.length < 11) {
|
||||||
@@ -44,7 +55,8 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
messagingServiceSid: bodyshop.messagingservicesid,
|
messagingServiceSid: bodyshop.messagingservicesid,
|
||||||
conversationid: conversation.id,
|
conversationid: conversation.id,
|
||||||
selectedMedia: selectedImages,
|
selectedMedia: selectedImages,
|
||||||
imexshopid: bodyshop.imexshopid
|
imexshopid: bodyshop.imexshopid,
|
||||||
|
bodyshopid: bodyshop.id
|
||||||
};
|
};
|
||||||
sendMessage(newMessage);
|
sendMessage(newMessage);
|
||||||
setSelectedMedia(
|
setSelectedMedia(
|
||||||
@@ -56,47 +68,67 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="imex-flex-row" style={{ width: "100%" }}>
|
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
||||||
<ChatPresetsComponent className="imex-flex-row__margin" />
|
{isOptedOut && (
|
||||||
<ChatMediaSelector
|
<Tooltip title={t("consent.text_body")}>
|
||||||
conversation={conversation}
|
<Alert
|
||||||
selectedMedia={selectedMedia}
|
showIcon={true}
|
||||||
setSelectedMedia={setSelectedMedia}
|
icon={<ExclamationCircleOutlined />}
|
||||||
/>
|
message={t("messaging.errors.no_consent")}
|
||||||
<span style={{ flex: 1 }}>
|
type="error"
|
||||||
<Input.TextArea
|
|
||||||
className="imex-flex-row__margin imex-flex-row__grow"
|
|
||||||
allowClear
|
|
||||||
autoFocus
|
|
||||||
ref={inputArea}
|
|
||||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
|
||||||
value={message}
|
|
||||||
disabled={isSending}
|
|
||||||
placeholder={t("messaging.labels.typeamessage")}
|
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
onPressEnter={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (!!!event.shiftKey) handleEnter();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<SendOutlined
|
|
||||||
className="chat-send-message-button"
|
|
||||||
// disabled={message === "" || !message}
|
|
||||||
onClick={handleEnter}
|
|
||||||
/>
|
|
||||||
<Spin
|
|
||||||
style={{ display: `${isSending ? "" : "none"}` }}
|
|
||||||
indicator={
|
|
||||||
<LoadingOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: 24
|
|
||||||
}}
|
|
||||||
spin
|
|
||||||
/>
|
/>
|
||||||
}
|
</Tooltip>
|
||||||
/>
|
)}
|
||||||
</div>
|
<div className="imex-flex-row" style={{ width: "100%" }}>
|
||||||
|
{!isOptedOut && (
|
||||||
|
<>
|
||||||
|
<ChatPresetsComponent disabled={isSending} className="imex-flex-row__margin" />
|
||||||
|
<ChatMediaSelector
|
||||||
|
disabled={isSending}
|
||||||
|
conversation={conversation}
|
||||||
|
selectedMedia={selectedMedia}
|
||||||
|
setSelectedMedia={setSelectedMedia}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<span style={{ flex: 1 }}>
|
||||||
|
<Input.TextArea
|
||||||
|
className="imex-flex-row__margin imex-flex-row__grow"
|
||||||
|
allowClear
|
||||||
|
autoFocus
|
||||||
|
ref={inputArea}
|
||||||
|
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||||
|
value={message}
|
||||||
|
disabled={isSending || isOptedOut}
|
||||||
|
placeholder={t("messaging.labels.typeamessage")}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
onPressEnter={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!event.shiftKey && !isOptedOut) handleEnter();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{!isOptedOut && (
|
||||||
|
<SendOutlined
|
||||||
|
className="chat-send-message-button"
|
||||||
|
disabled={isSending || message === "" || !message}
|
||||||
|
onClick={handleEnter}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Spin
|
||||||
|
style={{ display: `${isSending ? "" : "none"}` }}
|
||||||
|
indicator={
|
||||||
|
<LoadingOutlined
|
||||||
|
style={{
|
||||||
|
fontSize: 24
|
||||||
|
}}
|
||||||
|
spin
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { forwardRef, useEffect, useState } from "react";
|
import { forwardRef, useEffect, useState } from "react";
|
||||||
import { Select } from "antd";
|
import { Select } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
const ContractStatusComponent = ({ value, onChange }, ref) => {
|
const ContractStatusComponent = ({ value, onChange }) => {
|
||||||
const [option, setOption] = useState(value);
|
const [option, setOption] = useState(value);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function ContractsFindModalContainer({
|
|||||||
title={t("contracts.labels.findermodal")}
|
title={t("contracts.labels.findermodal")}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
onOk={() => toggleModalVisible()}
|
onOk={() => toggleModalVisible()}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
forceRender
|
forceRender
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Slider } from "antd";
|
import { Slider } from "antd";
|
||||||
import React, { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const CourtesyCarFuelComponent = (props, ref) => {
|
const CourtesyCarFuelComponent = (props, ref) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Card, Table, Tag } from "antd";
|
import { Card, Table, Tag } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import dayjs from "../../../utils/day";
|
import dayjs from "../../../utils/day";
|
||||||
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
|
||||||
@@ -69,7 +69,6 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
|
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
|
||||||
|
|
||||||
const extra = `${t("job_lifecycle.content.calculated_based_on")} ${lifecycleData.jobs} ${t("job_lifecycle.content.jobs_in_since")} ${fortyFiveDaysAgo()}`;
|
const extra = `${t("job_lifecycle.content.calculated_based_on")} ${lifecycleData.jobs} ${t("job_lifecycle.content.jobs_in_since")} ${fortyFiveDaysAgo()}`;
|
||||||
@@ -88,7 +87,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
|||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
borderWidth: "5px",
|
borderWidth: "5px",
|
||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
borderColor: "#f0f2f5",
|
borderColor: "var(--bar-border-color)",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0
|
padding: 0
|
||||||
}}
|
}}
|
||||||
@@ -107,12 +106,10 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
borderTop: "1px solid var(--bar-border-color)",
|
||||||
borderTop: "1px solid #f0f2f5",
|
borderBottom: "1px solid var(--bar-border-color)",
|
||||||
borderBottom: "1px solid #f0f2f5",
|
borderLeft: isFirst ? "1px solid var(--bar-border-color)" : undefined,
|
||||||
borderLeft: isFirst ? "1px solid #f0f2f5" : undefined,
|
borderRight: isLast ? "1px solid var(--bar-border-color)" : undefined,
|
||||||
borderRight: isLast ? "1px solid #f0f2f5" : undefined,
|
|
||||||
|
|
||||||
backgroundColor: key.color,
|
backgroundColor: key.color,
|
||||||
width: `${key.percentage}%`
|
width: `${key.percentage}%`
|
||||||
}}
|
}}
|
||||||
@@ -124,7 +121,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
|||||||
<div>{key.roundedPercentage}</div>
|
<div>{key.roundedPercentage}</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#f0f2f5",
|
backgroundColor: "var(--tag-wrapper-bg)",
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
paddingRight: "2px",
|
paddingRight: "2px",
|
||||||
paddingLeft: "2px",
|
paddingLeft: "2px",
|
||||||
@@ -152,8 +149,8 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
|||||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#f0f2f5",
|
backgroundColor: "var(--tag-wrapper-bg)",
|
||||||
color: "#000",
|
color: "var(--tag-wrapper-text)",
|
||||||
padding: "4px",
|
padding: "4px",
|
||||||
textAlign: "center"
|
textAlign: "center"
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
||||||
|
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { TimeFormatter } from "../../../utils/DateFormatter";
|
||||||
|
import { onlyUnique } from "../../../utils/arrayHelper";
|
||||||
|
import dayjs from "../../../utils/day";
|
||||||
|
import { alphaSort, dateSort } from "../../../utils/sorters";
|
||||||
|
import useLocalStorage from "../../../utils/useLocalStorage";
|
||||||
|
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||||
|
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../../owner-name-display/owner-name-display.component";
|
||||||
|
import DashboardRefreshRequired from "../refresh-required.component";
|
||||||
|
|
||||||
|
export default function DashboardScheduledDeliveryToday({ data, ...cardProps }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: {}
|
||||||
|
});
|
||||||
|
const [isTvModeScheduledDelivery, setIsTvModeScheduledDelivery] = useLocalStorage("isTvModeScheduledDelivery", false);
|
||||||
|
if (!data) return null;
|
||||||
|
if (!data.scheduled_delivery_today) return <DashboardRefreshRequired {...cardProps} />;
|
||||||
|
|
||||||
|
const scheduledDeliveryToday = data.scheduled_delivery_today.map((item) => {
|
||||||
|
const joblines_body = item.joblines
|
||||||
|
? item.joblines.filter((l) => l.mod_lbr_ty !== "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0)
|
||||||
|
: 0;
|
||||||
|
const joblines_ref = item.joblines
|
||||||
|
? item.joblines.filter((l) => l.mod_lbr_ty === "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0)
|
||||||
|
: 0;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
joblines_body,
|
||||||
|
joblines_ref
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const tvFontSize = 18;
|
||||||
|
const tvFontWeight = "bold";
|
||||||
|
|
||||||
|
const tvColumns = [
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.scheduled_delivery"),
|
||||||
|
dataIndex: "scheduled_delivery",
|
||||||
|
key: "scheduled_delivery",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => dateSort(a.scheduled_delivery, b.scheduled_delivery),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "scheduled_delivery" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||||
|
<TimeFormatter>{record.scheduled_delivery}</TimeFormatter>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ro_number"),
|
||||||
|
dataIndex: "ro_number",
|
||||||
|
key: "ro_number",
|
||||||
|
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<Link to={"/manage/jobs/" + record.jobid} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Space>
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||||
|
{record.ro_number || t("general.labels.na")}
|
||||||
|
{record.production_vars && record.production_vars.alert ? (
|
||||||
|
<ExclamationCircleFilled className="production-alert" />
|
||||||
|
) : null}
|
||||||
|
{record.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||||
|
{record.iouparent && (
|
||||||
|
<Tooltip title={t("jobs.labels.iou")}>
|
||||||
|
<BranchesOutlined style={{ color: "orangered" }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.owner"),
|
||||||
|
dataIndex: "owner",
|
||||||
|
key: "owner",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.ownerid ? (
|
||||||
|
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||||
|
<OwnerNameDisplay ownerObject={record} />
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||||
|
<OwnerNameDisplay ownerObject={record} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.vehicle"),
|
||||||
|
dataIndex: "vehicle",
|
||||||
|
key: "vehicle",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) =>
|
||||||
|
alphaSort(
|
||||||
|
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`,
|
||||||
|
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||||
|
),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.vehicleid ? (
|
||||||
|
<Link to={"/manage/vehicles/" + record.vehicleid} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||||
|
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
|
||||||
|
record.v_model_yr || ""
|
||||||
|
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("appointments.fields.alt_transport"),
|
||||||
|
dataIndex: "alt_transport",
|
||||||
|
key: "alt_transport",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order,
|
||||||
|
filters:
|
||||||
|
(scheduledDeliveryToday &&
|
||||||
|
scheduledDeliveryToday
|
||||||
|
.map((j) => j.alt_transport)
|
||||||
|
.filter(onlyUnique)
|
||||||
|
.map((s) => {
|
||||||
|
return {
|
||||||
|
text: s || t("dashboard.errors.atp"),
|
||||||
|
value: [s]
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||||
|
[],
|
||||||
|
onFilter: (value, record) => value.includes(record.alt_transport),
|
||||||
|
render: (text, record) => (
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.alt_transport}</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.status"),
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
|
filters:
|
||||||
|
(scheduledDeliveryToday &&
|
||||||
|
scheduledDeliveryToday
|
||||||
|
.map((j) => j.status)
|
||||||
|
.filter(onlyUnique)
|
||||||
|
.map((s) => {
|
||||||
|
return {
|
||||||
|
text: s || t("dashboard.errors.status"),
|
||||||
|
value: [s]
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||||
|
[],
|
||||||
|
onFilter: (value, record) => value.includes(record.status),
|
||||||
|
render: (text, record) => <span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.status}</span>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.lab"),
|
||||||
|
dataIndex: "joblines_body",
|
||||||
|
key: "joblines_body",
|
||||||
|
sorter: (a, b) => a.joblines_body - b.joblines_body,
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "joblines_body" && state.sortedInfo.order,
|
||||||
|
align: "right",
|
||||||
|
render: (text, record) => (
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.joblines_body.toFixed(1)}</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.lar"),
|
||||||
|
dataIndex: "joblines_ref",
|
||||||
|
key: "joblines_ref",
|
||||||
|
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
|
||||||
|
align: "right",
|
||||||
|
render: (text, record) => (
|
||||||
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.joblines_ref.toFixed(1)}</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.scheduled_delivery"),
|
||||||
|
dataIndex: "scheduled_delivery",
|
||||||
|
key: "scheduled_delivery",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => dateSort(a.scheduled_delivery, b.scheduled_delivery),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "scheduled_delivery" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => <TimeFormatter>{record.scheduled_delivery}</TimeFormatter>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ro_number"),
|
||||||
|
dataIndex: "ro_number",
|
||||||
|
key: "ro_number",
|
||||||
|
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<Link to={"/manage/jobs/" + record.jobid} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Space>
|
||||||
|
{record.ro_number || t("general.labels.na")}
|
||||||
|
{record.production_vars && record.production_vars.alert ? (
|
||||||
|
<ExclamationCircleFilled className="production-alert" />
|
||||||
|
) : null}
|
||||||
|
{record.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||||
|
{record.iouparent && (
|
||||||
|
<Tooltip title={t("jobs.labels.iou")}>
|
||||||
|
<BranchesOutlined style={{ color: "orangered" }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.owner"),
|
||||||
|
dataIndex: "owner",
|
||||||
|
key: "owner",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.ownerid ? (
|
||||||
|
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<OwnerNameDisplay ownerObject={record} />
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
<OwnerNameDisplay ownerObject={record} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("dashboard.labels.phone"),
|
||||||
|
dataIndex: "ownr_ph",
|
||||||
|
key: "ownr_ph",
|
||||||
|
ellipsis: true,
|
||||||
|
responsive: ["md"],
|
||||||
|
render: (text, record) => (
|
||||||
|
<Space size="small" wrap>
|
||||||
|
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||||
|
|
||||||
|
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ownr_ea"),
|
||||||
|
dataIndex: "ownr_ea",
|
||||||
|
key: "ownr_ea",
|
||||||
|
ellipsis: true,
|
||||||
|
responsive: ["md"],
|
||||||
|
render: (text, record) => <a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.vehicle"),
|
||||||
|
dataIndex: "vehicle",
|
||||||
|
key: "vehicle",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) =>
|
||||||
|
alphaSort(
|
||||||
|
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`,
|
||||||
|
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||||
|
),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.vehicleid ? (
|
||||||
|
<Link to={"/manage/vehicles/" + record.vehicleid} onClick={(e) => e.stopPropagation()}>
|
||||||
|
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ins_co_nm"),
|
||||||
|
dataIndex: "ins_co_nm",
|
||||||
|
key: "ins_co_nm",
|
||||||
|
ellipsis: true,
|
||||||
|
responsive: ["md"],
|
||||||
|
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
|
||||||
|
filters:
|
||||||
|
(scheduledDeliveryToday &&
|
||||||
|
scheduledDeliveryToday
|
||||||
|
.map((j) => j.ins_co_nm)
|
||||||
|
.filter(onlyUnique)
|
||||||
|
.map((s) => {
|
||||||
|
return {
|
||||||
|
text: s || t("dashboard.errors.insco"),
|
||||||
|
value: [s]
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||||
|
[],
|
||||||
|
onFilter: (value, record) => value.includes(record.ins_co_nm)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("appointments.fields.alt_transport"),
|
||||||
|
dataIndex: "alt_transport",
|
||||||
|
key: "alt_transport",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order,
|
||||||
|
filters:
|
||||||
|
(scheduledDeliveryToday &&
|
||||||
|
scheduledDeliveryToday
|
||||||
|
.map((j) => j.alt_transport)
|
||||||
|
.filter(onlyUnique)
|
||||||
|
.map((s) => {
|
||||||
|
return {
|
||||||
|
text: s || t("dashboard.errors.atp"),
|
||||||
|
value: [s]
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||||
|
[],
|
||||||
|
onFilter: (value, record) => value.includes(record.alt_transport)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={t("dashboard.titles.scheduleddeliverydate", {
|
||||||
|
date: dayjs().startOf("day").format("MM/DD/YYYY")
|
||||||
|
})}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
onClick={() => setIsTvModeScheduledDelivery(!isTvModeScheduledDelivery)}
|
||||||
|
defaultChecked={isTvModeScheduledDelivery}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
{...cardProps}
|
||||||
|
>
|
||||||
|
<div style={{ height: "100%" }}>
|
||||||
|
<Table
|
||||||
|
onChange={handleTableChange}
|
||||||
|
pagination={false}
|
||||||
|
columns={isTvModeScheduledDelivery ? tvColumns : columns}
|
||||||
|
scroll={{ x: true, y: "calc(100% - 2em)" }}
|
||||||
|
rowKey="id"
|
||||||
|
style={{ height: "85%" }}
|
||||||
|
dataSource={scheduledDeliveryToday}
|
||||||
|
size={isTvModeScheduledDelivery ? "small" : "middle"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DashboardScheduledDeliveryTodayGql = `
|
||||||
|
scheduled_delivery_today: jobs(where: {
|
||||||
|
date_invoiced: {_is_null: true},
|
||||||
|
ro_number: {_is_null: false},
|
||||||
|
voided: {_eq: false},
|
||||||
|
scheduled_delivery: {_gte: "${dayjs().startOf("day").toISOString()}",
|
||||||
|
_lte: "${dayjs().endOf("day").toISOString()}"}}) {
|
||||||
|
alt_transport
|
||||||
|
clm_no
|
||||||
|
jobid: id
|
||||||
|
joblines(where: {removed: {_eq: false}}) {
|
||||||
|
mod_lb_hrs
|
||||||
|
mod_lbr_ty
|
||||||
|
}
|
||||||
|
ins_co_nm
|
||||||
|
iouparent
|
||||||
|
ownerid
|
||||||
|
ownr_co_nm
|
||||||
|
ownr_ea
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_ph1
|
||||||
|
ownr_ph2
|
||||||
|
production_vars
|
||||||
|
ro_number
|
||||||
|
scheduled_delivery
|
||||||
|
status
|
||||||
|
suspended
|
||||||
|
v_make_desc
|
||||||
|
v_model_desc
|
||||||
|
v_model_yr
|
||||||
|
v_vin
|
||||||
|
vehicleid
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
||||||
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||||
import dayjs from "../../../utils/day";
|
import { useState } from "react";
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { TimeFormatter } from "../../../utils/DateFormatter";
|
import { TimeFormatter } from "../../../utils/DateFormatter";
|
||||||
import { onlyUnique } from "../../../utils/arrayHelper";
|
import { onlyUnique } from "../../../utils/arrayHelper";
|
||||||
|
import dayjs from "../../../utils/day";
|
||||||
import { alphaSort, dateSort } from "../../../utils/sorters";
|
import { alphaSort, dateSort } from "../../../utils/sorters";
|
||||||
import useLocalStorage from "../../../utils/useLocalStorage";
|
import useLocalStorage from "../../../utils/useLocalStorage";
|
||||||
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||||
@@ -169,7 +169,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Alt. Transport",
|
text: s || t("dashboard.errors.atp"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -313,7 +313,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Ins. Co.*",
|
text: s || t("dashboard.errors.insco"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -335,7 +335,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Alt. Transport",
|
text: s || t("dashboard.errors.atp"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
||||||
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||||
import dayjs from "../../../utils/day";
|
import { useState } from "react";
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { TimeFormatter } from "../../../utils/DateFormatter";
|
import { TimeFormatter } from "../../../utils/DateFormatter";
|
||||||
import { onlyUnique } from "../../../utils/arrayHelper";
|
import { onlyUnique } from "../../../utils/arrayHelper";
|
||||||
|
import dayjs from "../../../utils/day";
|
||||||
import { alphaSort, dateSort } from "../../../utils/sorters";
|
import { alphaSort, dateSort } from "../../../utils/sorters";
|
||||||
import useLocalStorage from "../../../utils/useLocalStorage";
|
import useLocalStorage from "../../../utils/useLocalStorage";
|
||||||
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||||
@@ -138,7 +138,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Alt. Transport*",
|
text: s || t("dashboard.errors.atp"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -154,7 +154,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
|||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
key: "status",
|
key: "status",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
filters:
|
filters:
|
||||||
(scheduledOutToday &&
|
(scheduledOutToday &&
|
||||||
@@ -163,7 +163,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Status*",
|
text: s || t("dashboard.errors.status"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -306,7 +306,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Ins. Co.*",
|
text: s || t("dashboard.errors.insco"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -328,7 +328,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
|||||||
.filter(onlyUnique)
|
.filter(onlyUnique)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return {
|
return {
|
||||||
text: s || "No Alt. Transport*",
|
text: s || t("dashboard.errors.atp"),
|
||||||
value: [s]
|
value: [s]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,30 +1,33 @@
|
|||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component.jsx";
|
import JobLifecycleDashboardComponent, {
|
||||||
import {
|
JobLifecycleDashboardGQL
|
||||||
DashboardTotalProductionHours,
|
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx";
|
||||||
DashboardTotalProductionHoursGql
|
|
||||||
} from "../dashboard-components/total-production-hours/total-production-hours.component.jsx";
|
|
||||||
import DashboardProjectedMonthlySales, {
|
|
||||||
DashboardProjectedMonthlySalesGql
|
|
||||||
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx";
|
|
||||||
import DashboardMonthlyRevenueGraph, {
|
|
||||||
DashboardMonthlyRevenueGraphGql
|
|
||||||
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx";
|
|
||||||
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx";
|
|
||||||
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx";
|
|
||||||
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx";
|
|
||||||
import DashboardMonthlyEmployeeEfficiency, {
|
import DashboardMonthlyEmployeeEfficiency, {
|
||||||
DashboardMonthlyEmployeeEfficiencyGql
|
DashboardMonthlyEmployeeEfficiencyGql
|
||||||
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component.jsx";
|
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component.jsx";
|
||||||
|
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx";
|
||||||
|
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx";
|
||||||
|
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx";
|
||||||
|
import DashboardMonthlyRevenueGraph, {
|
||||||
|
DashboardMonthlyRevenueGraphGql
|
||||||
|
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx";
|
||||||
|
import DashboardProjectedMonthlySales, {
|
||||||
|
DashboardProjectedMonthlySalesGql
|
||||||
|
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx";
|
||||||
|
import DashboardScheduledDeliveryToday, {
|
||||||
|
DashboardScheduledDeliveryTodayGql
|
||||||
|
} from "../dashboard-components/scheduled-delivery-today/scheduled-delivery-today.component.jsx";
|
||||||
import DashboardScheduledInToday, {
|
import DashboardScheduledInToday, {
|
||||||
DashboardScheduledInTodayGql
|
DashboardScheduledInTodayGql
|
||||||
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx";
|
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx";
|
||||||
import DashboardScheduledOutToday, {
|
import DashboardScheduledOutToday, {
|
||||||
DashboardScheduledOutTodayGql
|
DashboardScheduledOutTodayGql
|
||||||
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx";
|
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx";
|
||||||
import JobLifecycleDashboardComponent, {
|
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component.jsx";
|
||||||
JobLifecycleDashboardGQL
|
import {
|
||||||
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx";
|
DashboardTotalProductionHours,
|
||||||
|
DashboardTotalProductionHoursGql
|
||||||
|
} from "../dashboard-components/total-production-hours/total-production-hours.component.jsx";
|
||||||
|
|
||||||
const componentList = {
|
const componentList = {
|
||||||
ProductionDollars: {
|
ProductionDollars: {
|
||||||
@@ -118,6 +121,15 @@ const componentList = {
|
|||||||
w: 10,
|
w: 10,
|
||||||
h: 3
|
h: 3
|
||||||
},
|
},
|
||||||
|
ScheduleDeliveryToday: {
|
||||||
|
label: i18next.t("dashboard.titles.scheduleddeliverytoday"),
|
||||||
|
component: DashboardScheduledDeliveryToday,
|
||||||
|
gqlFragment: DashboardScheduledDeliveryTodayGql,
|
||||||
|
minW: 6,
|
||||||
|
minH: 2,
|
||||||
|
w: 10,
|
||||||
|
h: 3
|
||||||
|
},
|
||||||
JobLifecycle: {
|
JobLifecycle: {
|
||||||
label: i18next.t("dashboard.titles.joblifecycle"),
|
label: i18next.t("dashboard.titles.joblifecycle"),
|
||||||
component: JobLifecycleDashboardComponent,
|
component: JobLifecycleDashboardComponent,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
Typography
|
Typography
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -24,16 +23,16 @@ import i18n from "../../translations/i18n";
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
|
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
|
||||||
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
|
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
|
||||||
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket";
|
import { useSocket } from "../../contexts/SocketIO/useSocket";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
|
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
|
||||||
@@ -125,7 +124,9 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
|||||||
})
|
})
|
||||||
: ""
|
: ""
|
||||||
}`.slice(0, 239),
|
}`.slice(0, 239),
|
||||||
inservicedate: dayjs("2019-01-01")
|
inservicedate: dayjs(
|
||||||
|
`${(job.v_model_yr && (job.v_model_yr < 100 ? (job.v_model_yr >= (dayjs().year() + 1) % 100 ? 1900 + parseInt(job.v_model_yr) : 2000 + parseInt(job.v_model_yr)) : job.v_model_yr)) || 2019}-01-01`
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LayoutFormRow grow>
|
<LayoutFormRow grow>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { UploadOutlined, UserAddOutlined } from "@ant-design/icons";
|
import { UploadOutlined, UserAddOutlined } from "@ant-design/icons";
|
||||||
import { Button, Divider, Dropdown, Form, Input, Select, Space, Tabs, Upload } from "antd";
|
import { Button, Divider, Dropdown, Form, Input, Select, Space, Tabs, Upload } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -15,20 +14,24 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
emailConfig: selectEmailConfig
|
emailConfig: selectEmailConfig
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(EmailOverlayComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(EmailOverlayComponent);
|
||||||
|
|
||||||
export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, bodyshop, currentUser }) {
|
export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, bodyshop, currentUser }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleClick = ({ item, key, keyPath }) => {
|
|
||||||
|
const handleClick = ({ item }) => {
|
||||||
const email = item.props.value;
|
const email = item.props.value;
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
to: _.uniq([...form.getFieldValue("to"), ...(typeof email === "string" ? [email] : email)])
|
to: _.uniq([...form.getFieldValue("to"), ...(typeof email === "string" ? [email] : email)])
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const handle_CC_Click = ({ item, key, keyPath }) => {
|
|
||||||
|
const handle_CC_Click = ({ item }) => {
|
||||||
const email = item.props.value;
|
const email = item.props.value;
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
cc: _.uniq([...(form.getFieldValue("cc") || ""), ...(typeof email === "string" ? [email] : email)])
|
cc: _.uniq([...(form.getFieldValue("cc") || ""), ...(typeof email === "string" ? [email] : email)])
|
||||||
@@ -52,6 +55,7 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b
|
|||||||
],
|
],
|
||||||
onClick: handleClick
|
onClick: handleClick
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuCC = {
|
const menuCC = {
|
||||||
items: [
|
items: [
|
||||||
...bodyshop.employees
|
...bodyshop.employees
|
||||||
@@ -136,26 +140,22 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b
|
|||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Divider>{t("emails.labels.preview")}</Divider>
|
<Divider>{t("emails.labels.preview")}</Divider>
|
||||||
{bodyshop.attach_pdf_to_email && <strong>{t("emails.labels.pdfcopywillbeattached")}</strong>}
|
{bodyshop.attach_pdf_to_email && <strong>{t("emails.labels.pdfcopywillbeattached")}</strong>}
|
||||||
|
|
||||||
<Form.Item shouldUpdate>
|
<Form.Item shouldUpdate>
|
||||||
{() => {
|
{() => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "1rem",
|
padding: "1rem",
|
||||||
|
backgroundColor: "var(--preview-bg)",
|
||||||
backgroundColor: "lightgray",
|
borderLeft: "6px solid var(--preview-border-color)"
|
||||||
borderLeft: "6px solid #2196F3"
|
|
||||||
}}
|
}}
|
||||||
dangerouslySetInnerHTML={{ __html: form.getFieldValue("html") }}
|
dangerouslySetInnerHTML={{ __html: form.getFieldValue("html") }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultActiveKey="documents"
|
defaultActiveKey="documents"
|
||||||
items={[
|
items={[
|
||||||
@@ -184,12 +184,10 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b
|
|||||||
return e && e.fileList;
|
return e && e.fileList;
|
||||||
}}
|
}}
|
||||||
rules={[
|
rules={[
|
||||||
({ getFieldValue }) => ({
|
() => ({
|
||||||
validator(rule, value) {
|
validator(rule, value) {
|
||||||
const totalSize = value.reduce((acc, val) => (acc = acc + val.size), 0);
|
const totalSize = value.reduce((acc, val) => (acc = acc + val.size), 0);
|
||||||
|
|
||||||
const limit = 10485760 - new Blob([form.getFieldValue("html")]).size;
|
const limit = 10485760 - new Blob([form.getFieldValue("html")]).size;
|
||||||
|
|
||||||
if (totalSize > limit) {
|
if (totalSize > limit) {
|
||||||
return Promise.reject(t("general.errors.sizelimit"));
|
return Promise.reject(t("general.errors.sizelimit"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOv
|
|||||||
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
destroyOnClose={true}
|
destroyOnHidden
|
||||||
open={modalVisible}
|
open={modalVisible}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
width={"80%"}
|
width={"80%"}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
//To be used as a form element only.
|
//To be used as a form element only.
|
||||||
|
|
||||||
const EmployeeSearchSelect = ({ options, ...props }) => {
|
const EmployeeSearchSelect = ({ options, showEmail, ...props }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,12 +21,16 @@ const EmployeeSearchSelect = ({ options, ...props }) => {
|
|||||||
{options
|
{options
|
||||||
? options.map((o) => (
|
? options.map((o) => (
|
||||||
<Option key={o.id} value={o.id} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
|
<Option key={o.id} value={o.id} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
|
||||||
<Space>
|
<Space size="small">
|
||||||
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
{`${o.employee_number ?? ""} ${o.first_name} ${o.last_name}`}
|
||||||
|
<Tag color="green" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||||
<Tag color="green">
|
|
||||||
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
{showEmail && o.user_email ? (
|
||||||
|
<Tag color="blue" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||||
|
{o.user_email}
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
</Option>
|
</Option>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
.eula-markdown-card {
|
.eula-markdown-card {
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: lightgray;
|
background-color: var(--eula-card-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.eula-markdown-div {
|
.eula-markdown-div {
|
||||||
|
|||||||
@@ -81,8 +81,9 @@ export function HasFeatureAccess({ featureName, bodyshop, bypass, debug = false
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
bodyshop?.features?.allAccess ||
|
bodyshop?.features?.allAccess ||
|
||||||
bodyshop?.features?.[featureName] ||
|
(typeof bodyshop?.features?.[featureName] === "boolean"
|
||||||
dayjs(bodyshop?.features[featureName]).isAfter(dayjs())
|
? bodyshop?.features?.[featureName]
|
||||||
|
: dayjs(bodyshop?.features?.[featureName]).isAfter(dayjs()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DatePicker, Space, TimePicker } from "antd";
|
import { DatePicker, Space, TimePicker } from "antd";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import React, { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -94,7 +94,24 @@ const DateTimePicker = ({
|
|||||||
showTime={false}
|
showTime={false}
|
||||||
format="MM/DD/YYYY"
|
format="MM/DD/YYYY"
|
||||||
value={value ? dayjs(value) : null}
|
value={value ? dayjs(value) : null}
|
||||||
onChange={handleChange}
|
onChange={(dateValue) => {
|
||||||
|
if (dateValue) {
|
||||||
|
// When date changes, preserve the existing time if it exists
|
||||||
|
if (value && dayjs(value).isValid()) {
|
||||||
|
const existingTime = dayjs(value);
|
||||||
|
const newDateTime = dayjs(dateValue)
|
||||||
|
.hour(existingTime.hour())
|
||||||
|
.minute(existingTime.minute())
|
||||||
|
.second(existingTime.second());
|
||||||
|
handleChange(newDateTime);
|
||||||
|
} else {
|
||||||
|
// If no existing time, just set the date without time
|
||||||
|
handleChange(dateValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleChange(dateValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder={t("general.labels.date")}
|
placeholder={t("general.labels.date")}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
disabledDate={handleDisabledDate}
|
disabledDate={handleDisabledDate}
|
||||||
@@ -105,13 +122,25 @@ const DateTimePicker = ({
|
|||||||
<TimePicker
|
<TimePicker
|
||||||
format="hh:mm a"
|
format="hh:mm a"
|
||||||
minuteStep={15}
|
minuteStep={15}
|
||||||
|
value={value && dayjs(value).hour() === 0 && dayjs(value).minute() === 0 ? null : dayjs(value)}
|
||||||
defaultOpenValue={dayjs(value)
|
defaultOpenValue={dayjs(value)
|
||||||
.hour(dayjs().hour())
|
.hour(dayjs().hour())
|
||||||
.minute(Math.floor(dayjs().minute() / 15) * 15)
|
.minute(Math.floor(dayjs().minute() / 15) * 15)
|
||||||
.second(0)}
|
.second(0)}
|
||||||
onChange={(value) => {
|
onChange={(timeValue) => {
|
||||||
handleChange(value);
|
if (timeValue) {
|
||||||
onBlur();
|
// When time changes, combine it with the existing date
|
||||||
|
const existingDate = dayjs(value);
|
||||||
|
const newDateTime = existingDate
|
||||||
|
.hour(timeValue.hour())
|
||||||
|
.minute(timeValue.minute())
|
||||||
|
.second(0);
|
||||||
|
handleChange(newDateTime);
|
||||||
|
} else {
|
||||||
|
// If time is cleared, just update with null time but keep date
|
||||||
|
handleChange(timeValue);
|
||||||
|
}
|
||||||
|
if (onBlur) onBlur();
|
||||||
}}
|
}}
|
||||||
placeholder={t("general.labels.time")}
|
placeholder={t("general.labels.time")}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const LaborTypeFormItem = ({ value, onChange }, ref) => {
|
const LaborTypeFormItem = ({ value }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import React, { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const PartTypeFormItem = ({ value, onChange }, ref) => {
|
const PartTypeFormItem = ({ value }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
|
|
||||||
return <div>{t(`joblines.fields.part_types.${value}`)}</div>;
|
return (
|
||||||
|
<div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{t(`joblines.fields.part_types.${value}`)}</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
export default forwardRef(PartTypeFormItem);
|
export default forwardRef(PartTypeFormItem);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import React, { forwardRef } from "react";
|
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
@@ -8,24 +6,25 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
const ReadOnlyFormItem = ({ bodyshop, value, type = "text", onChange }, ref) => {
|
const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "employee":
|
case "employee": {
|
||||||
const emp = bodyshop.employees.find((e) => e.id === value);
|
const emp = bodyshop.employees.find((e) => e.id === value);
|
||||||
return `${emp?.first_name} ${emp?.last_name}`;
|
return `${emp?.first_name} ${emp?.last_name}`;
|
||||||
|
}
|
||||||
|
|
||||||
case "text":
|
case "text":
|
||||||
return <div>{value}</div>;
|
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
||||||
case "currency":
|
case "currency":
|
||||||
return <div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>;
|
return <div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>;
|
||||||
default:
|
default:
|
||||||
return <div>{value}</div>;
|
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(forwardRef(ReadOnlyFormItem));
|
export default connect(mapStateToProps, mapDispatchToProps)(ReadOnlyFormItem);
|
||||||
|
|||||||
190
client/src/components/header/buildAccountingChildren.jsx
Normal file
190
client/src/components/header/buildAccountingChildren.jsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { FaCreditCard, FaFileInvoiceDollar } from "react-icons/fa";
|
||||||
|
import { GiPayMoney, GiPlayerTime } from "react-icons/gi";
|
||||||
|
import { BankFilled, ExportOutlined, FieldTimeOutlined } from "@ant-design/icons";
|
||||||
|
import LockWrapper from "../../components/lock-wrapper/lock-wrapper.component.jsx";
|
||||||
|
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
||||||
|
|
||||||
|
// --- Menu Item Builders ---
|
||||||
|
const buildAccountingChildren = ({
|
||||||
|
t,
|
||||||
|
bodyshop,
|
||||||
|
currentUser,
|
||||||
|
setBillEnterContext,
|
||||||
|
setPaymentContext,
|
||||||
|
setCardPaymentContext,
|
||||||
|
setTimeTicketContext,
|
||||||
|
ImEXPay,
|
||||||
|
DmsAp,
|
||||||
|
Simple_Inventory
|
||||||
|
}) => [
|
||||||
|
{
|
||||||
|
key: "bills",
|
||||||
|
id: "header-accounting-bills",
|
||||||
|
icon: <FaFileInvoiceDollar />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/bills">
|
||||||
|
<LockWrapper featureName="bills" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.bills")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "enterbills",
|
||||||
|
id: "header-accounting-enterbills",
|
||||||
|
icon: <GiPayMoney />,
|
||||||
|
label: (
|
||||||
|
<LockWrapper featureName="bills" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.enterbills")}
|
||||||
|
</LockWrapper>
|
||||||
|
),
|
||||||
|
onClick: () =>
|
||||||
|
HasFeatureAccess({ featureName: "bills", bodyshop }) && setBillEnterContext({ actions: {}, context: {} })
|
||||||
|
},
|
||||||
|
...(Simple_Inventory.treatment === "on"
|
||||||
|
? [
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "inventory",
|
||||||
|
id: "header-accounting-inventory",
|
||||||
|
icon: <FaFileInvoiceDollar />,
|
||||||
|
label: <Link to="/manage/inventory">{t("menus.header.inventory")}</Link>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "allpayments",
|
||||||
|
id: "header-accounting-allpayments",
|
||||||
|
icon: <BankFilled />,
|
||||||
|
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "enterpayments",
|
||||||
|
id: "header-accounting-enterpayments",
|
||||||
|
icon: <FaCreditCard />,
|
||||||
|
label: t("menus.header.enterpayment"),
|
||||||
|
onClick: () => setPaymentContext({ actions: {}, context: null })
|
||||||
|
},
|
||||||
|
...(ImEXPay.treatment === "on"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "entercardpayments",
|
||||||
|
id: "header-accounting-entercardpayments",
|
||||||
|
icon: <FaCreditCard />,
|
||||||
|
label: t("menus.header.entercardpayment"),
|
||||||
|
onClick: () => setCardPaymentContext({ actions: {}, context: {} })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "timetickets",
|
||||||
|
id: "header-accounting-timetickets",
|
||||||
|
icon: <FieldTimeOutlined />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/timetickets">
|
||||||
|
<LockWrapper featureName="timetickets" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.timetickets")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
...(bodyshop?.md_tasks_presets?.use_approvals
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "ttapprovals",
|
||||||
|
id: "header-accounting-ttapprovals",
|
||||||
|
icon: <FieldTimeOutlined />,
|
||||||
|
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
key: "entertimetickets",
|
||||||
|
id: "header-accounting-entertimetickets",
|
||||||
|
icon: <GiPlayerTime />,
|
||||||
|
label: (
|
||||||
|
<LockWrapper featureName="timetickets" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.entertimeticket")}
|
||||||
|
</LockWrapper>
|
||||||
|
),
|
||||||
|
onClick: () =>
|
||||||
|
HasFeatureAccess({ featureName: "timetickets", bodyshop }) &&
|
||||||
|
setTimeTicketContext({
|
||||||
|
actions: {},
|
||||||
|
context: {
|
||||||
|
created_by: currentUser.displayName ? `${currentUser.email} | ${currentUser.displayName}` : currentUser.email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "accountingexport",
|
||||||
|
id: "header-accounting-export",
|
||||||
|
icon: <ExportOutlined />,
|
||||||
|
label: (
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.export")}
|
||||||
|
</LockWrapper>
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "receivables",
|
||||||
|
id: "header-accounting-receivables",
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/accounting/receivables">
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.accounting-receivables")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "payables",
|
||||||
|
id: "header-accounting-payables",
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/accounting/payables">
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.accounting-payables")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "payments",
|
||||||
|
id: "header-accounting-payments",
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/accounting/payments">
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.accounting-payments")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "exportlogs",
|
||||||
|
id: "header-accounting-exportlogs",
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/accounting/exportlogs">
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.export-logs")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default buildAccountingChildren;
|
||||||
399
client/src/components/header/buildLeftMenuItems.jsx
Normal file
399
client/src/components/header/buildLeftMenuItems.jsx
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
BarChartOutlined,
|
||||||
|
CarFilled,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
ClockCircleFilled,
|
||||||
|
DashboardFilled,
|
||||||
|
DollarCircleFilled,
|
||||||
|
FileAddFilled,
|
||||||
|
FileAddOutlined,
|
||||||
|
FileFilled,
|
||||||
|
HomeFilled,
|
||||||
|
ImportOutlined,
|
||||||
|
LineChartOutlined,
|
||||||
|
OneToOneOutlined,
|
||||||
|
PaperClipOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
PlusCircleOutlined,
|
||||||
|
QuestionCircleFilled,
|
||||||
|
ScheduleOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
ToolFilled,
|
||||||
|
UnorderedListOutlined,
|
||||||
|
UsergroupAddOutlined,
|
||||||
|
UserOutlined
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { FaCalendarAlt, FaCarCrash, FaMoon, FaSun, FaTasks } from "react-icons/fa";
|
||||||
|
import { BsKanban } from "react-icons/bs";
|
||||||
|
import { FiLogOut } from "react-icons/fi";
|
||||||
|
import { GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
||||||
|
import { RiSurveyLine } from "react-icons/ri";
|
||||||
|
import { IoBusinessOutline } from "react-icons/io5";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
import LockWrapper from "../../components/lock-wrapper/lock-wrapper.component.jsx";
|
||||||
|
|
||||||
|
const buildLeftMenuItems = ({
|
||||||
|
t,
|
||||||
|
bodyshop,
|
||||||
|
recentItems,
|
||||||
|
setTaskUpsertContext,
|
||||||
|
setReportCenterContext,
|
||||||
|
signOutStart,
|
||||||
|
accountingChildren,
|
||||||
|
handleDarkModeToggle,
|
||||||
|
darkMode
|
||||||
|
}) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "home",
|
||||||
|
id: "header-home",
|
||||||
|
icon: <HomeFilled />,
|
||||||
|
label: <Link to="/manage/">{t("menus.header.home")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "schedule",
|
||||||
|
id: "header-schedule",
|
||||||
|
icon: <FaCalendarAlt />,
|
||||||
|
label: <Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "jobssubmenu",
|
||||||
|
id: "header-jobs",
|
||||||
|
icon: <FaCarCrash />,
|
||||||
|
label: t("menus.header.jobs"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "activejobs",
|
||||||
|
id: "header-active-jobs",
|
||||||
|
icon: <FileFilled />,
|
||||||
|
label: <Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "readyjobs",
|
||||||
|
id: "header-ready-jobs",
|
||||||
|
icon: <CheckCircleOutlined />,
|
||||||
|
label: <Link to="/manage/jobs/ready">{t("menus.header.readyjobs")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "parts-queue",
|
||||||
|
id: "header-parts-queue",
|
||||||
|
icon: <ToolFilled />,
|
||||||
|
label: <Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "availablejobs",
|
||||||
|
id: "header-jobs-available",
|
||||||
|
icon: <ImportOutlined />,
|
||||||
|
label: <Link to="/manage/available">{t("menus.header.availablejobs")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "newjob",
|
||||||
|
id: "header-new-job",
|
||||||
|
icon: <FileAddOutlined />,
|
||||||
|
label: <Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
|
||||||
|
},
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "alljobs",
|
||||||
|
id: "header-all-jobs",
|
||||||
|
icon: <UnorderedListOutlined />,
|
||||||
|
label: <Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||||
|
},
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "productionlist",
|
||||||
|
id: "header-production-list",
|
||||||
|
icon: <ScheduleOutlined />,
|
||||||
|
label: <Link to="/manage/production/list">{t("menus.header.productionlist")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "productionboard",
|
||||||
|
id: "header-production-board",
|
||||||
|
icon: <BsKanban />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/production/board">
|
||||||
|
<LockWrapper featureName="visualboard" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.productionboard")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
key: "scoreboard",
|
||||||
|
id: "header-scoreboard",
|
||||||
|
icon: <LineChartOutlined />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/scoreboard">
|
||||||
|
<LockWrapper featureName="scoreboard" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.scoreboard")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "customers",
|
||||||
|
id: "header-customers",
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: t("menus.header.customers"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "owners",
|
||||||
|
id: "header-owners",
|
||||||
|
icon: <TeamOutlined />,
|
||||||
|
label: <Link to="/manage/owners">{t("menus.header.owners")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "vehicles",
|
||||||
|
id: "header-vehicles",
|
||||||
|
icon: <CarFilled />,
|
||||||
|
label: <Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ccs",
|
||||||
|
id: "header-css",
|
||||||
|
icon: <CarFilled />,
|
||||||
|
label: (
|
||||||
|
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.courtesycars")}
|
||||||
|
</LockWrapper>
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "courtesycarsall",
|
||||||
|
id: "header-courtesycars-all",
|
||||||
|
icon: <CarFilled />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/courtesycars">
|
||||||
|
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.courtesycars-all")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "contracts",
|
||||||
|
id: "header-contracts",
|
||||||
|
icon: <FileFilled />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/courtesycars/contracts">
|
||||||
|
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.courtesycars-contracts")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "newcontract",
|
||||||
|
id: "header-newcontract",
|
||||||
|
icon: <FileAddFilled />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/courtesycars/contracts/new">
|
||||||
|
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.courtesycars-newcontract")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
...(accountingChildren.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "accounting",
|
||||||
|
id: "header-accounting",
|
||||||
|
icon: <DollarCircleFilled />,
|
||||||
|
label: t("menus.header.accounting"),
|
||||||
|
children: accountingChildren
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
key: "phonebook",
|
||||||
|
id: "header-phonebook",
|
||||||
|
icon: <PhoneOutlined />,
|
||||||
|
label: <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "temporarydocs",
|
||||||
|
id: "header-temporarydocs",
|
||||||
|
icon: <PaperClipOutlined />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/temporarydocs">
|
||||||
|
<LockWrapper featureName="media" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.temporarydocs")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "tasks",
|
||||||
|
id: "tasks",
|
||||||
|
icon: <FaTasks />,
|
||||||
|
label: t("menus.header.tasks"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "createTask",
|
||||||
|
id: "header-create-task",
|
||||||
|
icon: <PlusCircleOutlined />,
|
||||||
|
label: t("menus.header.create_task"),
|
||||||
|
onClick: () => setTaskUpsertContext({ actions: {}, context: {} })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "mytasks",
|
||||||
|
id: "header-my-tasks",
|
||||||
|
icon: <FaTasks />,
|
||||||
|
label: <Link to="/manage/tasks/mytasks">{t("menus.header.my_tasks")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "all_tasks",
|
||||||
|
id: "header-all-tasks",
|
||||||
|
icon: <FaTasks />,
|
||||||
|
label: <Link to="/manage/tasks/alltasks">{t("menus.header.all_tasks")}</Link>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "shopsubmenu",
|
||||||
|
id: "header-shopsubmenu",
|
||||||
|
icon: <SettingOutlined />,
|
||||||
|
label: t("menus.header.shop"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "shop",
|
||||||
|
id: "header-shop",
|
||||||
|
icon: <GiSettingsKnobs />,
|
||||||
|
label: <Link to="/manage/shop?tab=info">{t("menus.header.shop_config")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "dashboard",
|
||||||
|
id: "header-dashboard",
|
||||||
|
icon: <DashboardFilled />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/dashboard">
|
||||||
|
<LockWrapper featureName="bills">{t("menus.header.dashboard")}</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "reportcenter",
|
||||||
|
id: "header-reportcenter",
|
||||||
|
icon: <BarChartOutlined />,
|
||||||
|
label: t("menus.header.reportcenter"),
|
||||||
|
onClick: () => setReportCenterContext({ actions: {}, context: {} })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "shop-vendors",
|
||||||
|
id: "header-shop-vendors",
|
||||||
|
icon: <IoBusinessOutline />,
|
||||||
|
label: <Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "shop-csi",
|
||||||
|
id: "header-shop-csi",
|
||||||
|
icon: <RiSurveyLine />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/shop/csi">
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.shop_csi")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "recent",
|
||||||
|
id: "header-recent",
|
||||||
|
icon: <ClockCircleFilled />,
|
||||||
|
label: t("menus.header.recent"),
|
||||||
|
children: recentItems.map((i, idx) => ({
|
||||||
|
key: idx,
|
||||||
|
id: `header-recent-${idx}`,
|
||||||
|
label: <Link to={i.url}>{i.label}</Link>
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "user",
|
||||||
|
id: "header-user",
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: t("menus.currentuser.profile"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "signout",
|
||||||
|
id: "header-signout",
|
||||||
|
icon: <FiLogOut />,
|
||||||
|
danger: true,
|
||||||
|
label: t("user.actions.signout"),
|
||||||
|
onClick: () => signOutStart()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "darkmode-toggle",
|
||||||
|
id: "header-darkmode-toggle",
|
||||||
|
label: darkMode ? t("user.actions.light_theme") : t("user.actions.dark_theme"),
|
||||||
|
icon: darkMode ? <FaSun /> : <FaMoon />,
|
||||||
|
onClick: handleDarkModeToggle
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "help",
|
||||||
|
id: "header-help",
|
||||||
|
icon: <QuestionCircleFilled />,
|
||||||
|
label: t("menus.header.help"),
|
||||||
|
onClick: () => window.open("https://help.imex.online/", "_blank")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "remoteassist",
|
||||||
|
id: "header-remote-assist",
|
||||||
|
icon: <OneToOneOutlined />,
|
||||||
|
label: t("menus.header.remoteassist"),
|
||||||
|
children: [
|
||||||
|
...(InstanceRenderManager({ imex: true, rome: false })
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "rescue",
|
||||||
|
id: "header-rescue",
|
||||||
|
icon: <PlusCircleOutlined />,
|
||||||
|
label: t("menus.header.rescueme"),
|
||||||
|
onClick: () => window.open("https://imexrescue.com/", "_blank")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
key: "rescue-zoho",
|
||||||
|
id: "header-rescue-zoho",
|
||||||
|
icon: <UsergroupAddOutlined />,
|
||||||
|
label: t("menus.header.rescuemezoho"),
|
||||||
|
onClick: () => window.open("https://join.zoho.com/", "_blank")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "shiftclock",
|
||||||
|
id: "header-shiftclock",
|
||||||
|
icon: <GiPlayerTime />,
|
||||||
|
label: (
|
||||||
|
<Link to="/manage/shiftclock">
|
||||||
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.shiftclock")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "profile",
|
||||||
|
id: "header-profile",
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: <Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default buildLeftMenuItems;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -98,7 +98,7 @@ export function InventoryUpsertModalContainer({ currentUser, bodyshop, inventory
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||||
<InventoryUpsertModal form={form} />
|
<InventoryUpsertModal form={form} />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AlertFilled } from "@ant-design/icons";
|
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 { Button, Divider, Dropdown, Form, Input, Popover, Select, Space } from "antd";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
@@ -8,32 +8,40 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
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 { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||||
import DataLabel from "../data-label/data-label.component";
|
import DataLabel from "../data-label/data-label.component";
|
||||||
|
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||||
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.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 ScheduleAtChange from "./job-at-change.component";
|
||||||
import ScheduleEventColor from "./schedule-event.color.component";
|
import ScheduleEventColor from "./schedule-event.color.component";
|
||||||
import ScheduleEventNote from "./schedule-event.note.component";
|
import ScheduleEventNote from "./schedule-event.note.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
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({
|
export function ScheduleEventComponent({
|
||||||
@@ -43,16 +51,40 @@ export function ScheduleEventComponent({
|
|||||||
event,
|
event,
|
||||||
refetch,
|
refetch,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
setScheduleContext
|
setScheduleContext,
|
||||||
|
insertAuditTrail
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const searchParams = queryString.parse(useLocation().search);
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||||
|
const [mutationUpdateJob] = useMutation(JOB_PRODUCTION_TOGGLE);
|
||||||
const [title, setTitle] = useState(event.title);
|
const [title, setTitle] = useState(event.title);
|
||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
const notification = useNotification();
|
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 = (
|
const blockContent = (
|
||||||
<Space direction="vertical" wrap>
|
<Space direction="vertical" wrap>
|
||||||
@@ -82,13 +114,78 @@ export function ScheduleEventComponent({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={() => handleCancel({ id: event.id })} disabled={event.arrived}>
|
<Button onClick={() => handleCancel({ id: event.id })} disabled={event.arrived}>
|
||||||
{t("appointments.actions.unblock")}
|
{t("appointments.actions.unblock")}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</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 = (
|
const popoverContent = (
|
||||||
<div style={{ maxWidth: "40vw" }}>
|
<div style={{ maxWidth: "40vw" }}>
|
||||||
{!event.isintake ? (
|
{!event.isintake ? (
|
||||||
@@ -109,7 +206,6 @@ export function ScheduleEventComponent({
|
|||||||
<ScheduleEventColor event={event} />
|
<ScheduleEventColor event={event} />
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{event.job ? (
|
{event.job ? (
|
||||||
<div>
|
<div>
|
||||||
<DataLabel label={t("jobs.fields.ro_number")}>{(event.job && event.job.ro_number) || ""}</DataLabel>
|
<DataLabel label={t("jobs.fields.ro_number")}>{(event.job && event.job.ro_number) || ""}</DataLabel>
|
||||||
@@ -270,7 +366,6 @@ export function ScheduleEventComponent({
|
|||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{event.isintake ? (
|
{event.isintake ? (
|
||||||
<Button
|
<Button
|
||||||
disabled={event.arrived}
|
disabled={event.arrived}
|
||||||
@@ -284,7 +379,9 @@ export function ScheduleEventComponent({
|
|||||||
previousEvent: event.id,
|
previousEvent: event.id,
|
||||||
color: event.color,
|
color: event.color,
|
||||||
alt_transport: event.job && event.job.alt_transport,
|
alt_transport: event.job && event.job.alt_transport,
|
||||||
note: event.note
|
note: event.note,
|
||||||
|
scheduled_in: event.job && event.job.scheduled_in,
|
||||||
|
scheduled_completion: event.job && event.job.scheduled_completion
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -294,41 +391,64 @@ export function ScheduleEventComponent({
|
|||||||
) : (
|
) : (
|
||||||
<ScheduleManualEvent event={event} />
|
<ScheduleManualEvent event={event} />
|
||||||
)}
|
)}
|
||||||
{event.isintake ? (
|
{event.job &&
|
||||||
<Link
|
(HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
||||||
to={{
|
<Link
|
||||||
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
to={{
|
||||||
search: `?appointmentId=${event.id}`
|
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
||||||
}}
|
search: `?appointmentId=${event.id}`
|
||||||
>
|
}}
|
||||||
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
>
|
||||||
</Link>
|
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
||||||
) : null}
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Popover //open={open}
|
||||||
|
content={popMenu}
|
||||||
|
open={popOverVisible}
|
||||||
|
onOpenChange={setPopOverVisible}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (event.job?.id) {
|
||||||
|
e.stopPropagation();
|
||||||
|
getJobDetails();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
getPopupContainer={(trigger) => trigger.parentNode}
|
||||||
|
trigger="click"
|
||||||
|
>
|
||||||
|
<Button disabled={event.arrived}>{t("jobs.actions.intake_quick")}</Button>
|
||||||
|
</Popover>
|
||||||
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Adjust event color for dark mode if needed
|
||||||
|
const getEventBackground = () => {
|
||||||
|
if (event?.block) {
|
||||||
|
return "var(--event-block-bg)"; // Use a specific color for dark mode
|
||||||
|
}
|
||||||
|
const baseColor = event.color && event.color.hex ? event.color.hex : event.color || "var(--event-bg-fallback)";
|
||||||
|
// Optionally adjust color for dark mode (e.g., lighten if too dark)
|
||||||
|
return baseColor;
|
||||||
|
};
|
||||||
|
|
||||||
const RegularEvent = event.isintake ? (
|
const RegularEvent = event.isintake ? (
|
||||||
<Space
|
<Space
|
||||||
wrap
|
wrap
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
backgroundColor: getEventBackground()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{event.note && <AlertFilled className="production-alert" />}
|
{event.note && <AlertFilled className="production-alert" />}
|
||||||
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
|
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
|
||||||
|
|
||||||
<OwnerNameDisplay ownerObject={event.job} />
|
<OwnerNameDisplay ownerObject={event.job} />
|
||||||
|
|
||||||
{`${(event.job && event.job.v_model_yr) || ""} ${
|
{`${(event.job && event.job.v_model_yr) || ""} ${
|
||||||
(event.job && event.job.v_make_desc) || ""
|
(event.job && event.job.v_make_desc) || ""
|
||||||
} ${(event.job && event.job.v_model_desc) || ""}`}
|
} ${(event.job && event.job.v_model_desc) || ""}`}
|
||||||
|
|
||||||
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
|
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
|
||||||
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
|
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
|
||||||
})`}
|
})`}
|
||||||
|
|
||||||
{event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
|
{event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
|
||||||
{event?.job?.comment && `C: ${event.job.comment}`}
|
{event?.job?.comment && `C: ${event.job.comment}`}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -337,7 +457,7 @@ export function ScheduleEventComponent({
|
|||||||
style={{
|
style={{
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
backgroundColor: getEventBackground()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>{`${event.title || ""}`}</strong>
|
<strong>{`${event.title || ""}`}</strong>
|
||||||
@@ -353,8 +473,7 @@ export function ScheduleEventComponent({
|
|||||||
style={{
|
style={{
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
backgroundColor: getEventBackground()
|
||||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{RegularEvent}
|
{RegularEvent}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Card, Form, Input, Switch } from "antd";
|
import { Button, Card, Form, Input, Switch } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
@@ -9,7 +9,6 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { logImEXEvent } from "../../../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../../../firebase/firebase.utils";
|
||||||
import { MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED } from "../../../../graphql/appointments.queries";
|
import { MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED } from "../../../../graphql/appointments.queries";
|
||||||
import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
|
import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
|
||||||
import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
|
|
||||||
import { insertAuditTrail } from "../../../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../../../redux/application/application.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
|
||||||
@@ -32,7 +31,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED);
|
const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED);
|
||||||
const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED);
|
const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED);
|
||||||
const [updateOwner] = useMutation(UPDATE_OWNER);
|
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const { jobId } = useParams();
|
const { jobId } = useParams();
|
||||||
@@ -129,24 +127,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "intake" && job.owner && job.owner.id) {
|
|
||||||
//Updae Owner Allow to Text
|
|
||||||
const updateOwnerResult = await updateOwner({
|
|
||||||
variables: {
|
|
||||||
ownerId: job.owner.id,
|
|
||||||
owner: { allow_text_message: values.allow_text_message }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!!updateOwnerResult.errors) {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("checklist.errors.complete", {
|
|
||||||
error: JSON.stringify(result.errors)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (!!!result.errors) {
|
if (!!!result.errors) {
|
||||||
@@ -189,7 +169,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
|||||||
initialValues={{
|
initialValues={{
|
||||||
...(type === "intake" && {
|
...(type === "intake" && {
|
||||||
addToProduction: true,
|
addToProduction: true,
|
||||||
allow_text_message: job.owner && job.owner.allow_text_message,
|
|
||||||
scheduled_completion:
|
scheduled_completion:
|
||||||
(job && job.scheduled_completion && dayjs(job.scheduled_completion)) ||
|
(job && job.scheduled_completion && dayjs(job.scheduled_completion)) ||
|
||||||
(job &&
|
(job &&
|
||||||
@@ -228,14 +207,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
|||||||
>
|
>
|
||||||
<Switch disabled={readOnly} />
|
<Switch disabled={readOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
|
||||||
name="allow_text_message"
|
|
||||||
valuePropName="checked"
|
|
||||||
label={t("checklist.labels.allow_text_message")}
|
|
||||||
disabled={readOnly}
|
|
||||||
>
|
|
||||||
<Switch disabled={readOnly} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="scheduled_completion"
|
name="scheduled_completion"
|
||||||
label={t("jobs.fields.scheduled_completion")}
|
label={t("jobs.fields.scheduled_completion")}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function JobCostingModalContainer({ jobCostingModal, toggleModalVisible }
|
|||||||
}}
|
}}
|
||||||
cancelButtonProps={{ style: { display: "none" } }}
|
cancelButtonProps={{ style: { display: "none" } }}
|
||||||
width="90%"
|
width="90%"
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
{!costingData ? (
|
{!costingData ? (
|
||||||
<LoadingSpinner loading={true} />
|
<LoadingSpinner loading={true} />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const Car = ({ dmg1, dmg2 }) => {
|
const Car = ({ dmg1, dmg2 }) => {
|
||||||
@@ -8,6 +7,7 @@ const Car = ({ dmg1, dmg2 }) => {
|
|||||||
<div style={{ position: "relative", textAlign: "center" }}>
|
<div style={{ position: "relative", textAlign: "center" }}>
|
||||||
{t("jobs.labels.cards.damage")}
|
{t("jobs.labels.cards.damage")}
|
||||||
<svg
|
<svg
|
||||||
|
className="car-svg"
|
||||||
style={{ left: 0, top: 0, width: "100%", height: "100%" }}
|
style={{ left: 0, top: 0, width: "100%", height: "100%" }}
|
||||||
id="svg166"
|
id="svg166"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
setPrintCenterContext: (context) =>
|
||||||
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "printCenter"
|
||||||
|
})
|
||||||
|
),
|
||||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
dispatch(
|
dispatch(
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
@@ -87,7 +93,7 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTra
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{data ? (
|
{data ? (
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function JobEmployeeAssignments({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyTooltipOnHide content={popContent} open={visibility}>
|
<Popover destroyOnHidden content={popContent} open={visibility}>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<DataLabel label={t("jobs.fields.employee_body")}>
|
<DataLabel label={t("jobs.fields.employee_body")}>
|
||||||
{body ? (
|
{body ? (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Badge, Card, Space, Table, Tag } from "antd";
|
import { Badge, Card, Space, Table, Tag } from "antd";
|
||||||
@@ -6,24 +6,24 @@ import { gql, useQuery } from "@apollo/client";
|
|||||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||||
import { isEmpty } from "lodash";
|
import { isEmpty } from "lodash";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import "./job-lifecycle.styles.scss";
|
import "./job-lifecycle.styles.scss";
|
||||||
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
||||||
|
|
||||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||||
|
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
// show text on bar if text can fit
|
// show text on bar if text can fit
|
||||||
export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
export function JobLifecycleComponent({ bodyshop, job, statuses }) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [lifecycleData, setLifecycleData] = useState(null);
|
const [lifecycleData, setLifecycleData] = useState(null);
|
||||||
const { t } = useTranslation(); // Used for tracking external state changes.
|
const { t } = useTranslation(); // Used for tracking external state changes.
|
||||||
@@ -79,7 +79,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
title: t("job_lifecycle.columns.value"),
|
title: t("job_lifecycle.columns.value"),
|
||||||
dataIndex: "value",
|
dataIndex: "value",
|
||||||
key: "value",
|
key: "value",
|
||||||
render: (text, record) => (
|
render: (text) => (
|
||||||
<BlurWrapperComponent
|
<BlurWrapperComponent
|
||||||
featureName="lifecycle"
|
featureName="lifecycle"
|
||||||
bypass
|
bypass
|
||||||
@@ -95,7 +95,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
dataIndex: "start",
|
dataIndex: "start",
|
||||||
key: "start",
|
key: "start",
|
||||||
sorter: (a, b) => dayjs(a.start).unix() - dayjs(b.start).unix(),
|
sorter: (a, b) => dayjs(a.start).unix() - dayjs(b.start).unix(),
|
||||||
render: (text, record) => (
|
render: (text) => (
|
||||||
<BlurWrapperComponent featureName="lifecycle" bypass valueProp="children" overrideValueFunction="RandomDate">
|
<BlurWrapperComponent featureName="lifecycle" bypass valueProp="children" overrideValueFunction="RandomDate">
|
||||||
<span>{DateTimeFormatterFunction(text)}</span>
|
<span>{DateTimeFormatterFunction(text)}</span>
|
||||||
</BlurWrapperComponent>
|
</BlurWrapperComponent>
|
||||||
@@ -119,8 +119,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
}
|
}
|
||||||
return dayjs(a.end).unix() - dayjs(b.end).unix();
|
return dayjs(a.end).unix() - dayjs(b.end).unix();
|
||||||
},
|
},
|
||||||
|
render: (text) => (
|
||||||
render: (text, record) => (
|
|
||||||
<BlurWrapperComponent featureName="lifecycle" bypass valueProp="children" overrideValueFunction="RandomDate">
|
<BlurWrapperComponent featureName="lifecycle" bypass valueProp="children" overrideValueFunction="RandomDate">
|
||||||
<span>{isEmpty(text) ? t("job_lifecycle.content.not_available") : DateTimeFormatterFunction(text)}</span>
|
<span>{isEmpty(text) ? t("job_lifecycle.content.not_available") : DateTimeFormatterFunction(text)}</span>
|
||||||
</BlurWrapperComponent>
|
</BlurWrapperComponent>
|
||||||
@@ -170,7 +169,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
borderWidth: "5px",
|
borderWidth: "5px",
|
||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
borderColor: "#f0f2f5",
|
borderColor: "var(--bar-border-color)",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0
|
padding: 0
|
||||||
}}
|
}}
|
||||||
@@ -189,12 +188,10 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
borderTop: "1px solid var(--bar-border-color)",
|
||||||
borderTop: "1px solid #f0f2f5",
|
borderBottom: "1px solid var(--bar-border-color)",
|
||||||
borderBottom: "1px solid #f0f2f5",
|
borderLeft: isFirst ? "1px solid var(--bar-border-color)" : undefined,
|
||||||
borderLeft: isFirst ? "1px solid #f0f2f5" : undefined,
|
borderRight: isLast ? "1px solid var(--bar-border-color)" : undefined,
|
||||||
borderRight: isLast ? "1px solid #f0f2f5" : undefined,
|
|
||||||
|
|
||||||
backgroundColor: key.color,
|
backgroundColor: key.color,
|
||||||
width: `${key.percentage}%`
|
width: `${key.percentage}%`
|
||||||
}}
|
}}
|
||||||
@@ -206,7 +203,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
<div>{key.roundedPercentage}</div>
|
<div>{key.roundedPercentage}</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#f0f2f5",
|
backgroundColor: "var(--tag-wrapper-bg)",
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
paddingRight: "2px",
|
paddingRight: "2px",
|
||||||
paddingLeft: "2px",
|
paddingLeft: "2px",
|
||||||
@@ -230,8 +227,8 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#f0f2f5",
|
backgroundColor: "var(--tag-wrapper-bg)",
|
||||||
color: "#000",
|
color: "var(--tag-wrapper-text)",
|
||||||
padding: "4px",
|
padding: "4px",
|
||||||
textAlign: "center"
|
textAlign: "center"
|
||||||
}}
|
}}
|
||||||
@@ -315,4 +312,5 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLifecycleComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobLifecycleComponent);
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { PushpinFilled, PushpinOutlined } from "@ant-design/icons";
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { UPDATE_NOTE } from "../../graphql/notes.queries";
|
||||||
|
|
||||||
|
function JobNotesPinToggle({ note }) {
|
||||||
|
const [updateNote] = useMutation(UPDATE_NOTE);
|
||||||
|
|
||||||
|
const handlePinToggle = () => {
|
||||||
|
updateNote({
|
||||||
|
variables: {
|
||||||
|
noteId: note.id,
|
||||||
|
note: { pinned: !note.pinned }
|
||||||
|
},
|
||||||
|
refetchQueries: ["GET_JOB_BY_PK", "QUERY_JOB_CARD_DETAILS", "QUERY_PARTS_QUEUE_CARD_DETAILS"]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return note.pinned ? (
|
||||||
|
<PushpinFilled size="large" onClick={handlePinToggle} style={{ color: "gold" }} />
|
||||||
|
) : (
|
||||||
|
<PushpinOutlined size="large" onClick={handlePinToggle} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JobNotesPinToggle;
|
||||||
@@ -44,7 +44,7 @@ function JobReconciliationModalContainer({ reconciliationModal, toggleModalVisib
|
|||||||
onOk={handleCancel}
|
onOk={handleCancel}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
cancelButtonProps={{ display: "none" }}
|
cancelButtonProps={{ display: "none" }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
className="imex-reconciliation-modal"
|
className="imex-reconciliation-modal"
|
||||||
>
|
>
|
||||||
{loading && <LoadingSpinner loading={loading} />}
|
{loading && <LoadingSpinner loading={loading} />}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export default function JobWatcherToggleComponent({
|
|||||||
handleToggleSelf,
|
handleToggleSelf,
|
||||||
handleRemoveWatcher,
|
handleRemoveWatcher,
|
||||||
handleWatcherSelect,
|
handleWatcherSelect,
|
||||||
handleTeamSelect
|
handleTeamSelect,
|
||||||
|
isEmployee
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -66,22 +67,32 @@ export default function JobWatcherToggleComponent({
|
|||||||
<List>
|
<List>
|
||||||
<List.Item
|
<List.Item
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Tooltip title={!isEmployee ? t("notifications.tooltips.not-employee") : ""} placement="top">
|
||||||
type={isWatching ? "primary" : "default"}
|
<span>
|
||||||
danger={!isWatching}
|
<Button
|
||||||
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
|
type={isWatching ? "primary" : "default"}
|
||||||
size="medium"
|
danger={!isWatching}
|
||||||
onClick={handleToggleSelf}
|
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
|
||||||
loading={adding || removing}
|
size="medium"
|
||||||
>
|
onClick={handleToggleSelf}
|
||||||
{isWatching ? t("notifications.labels.unwatch") : t("notifications.labels.watch")}
|
loading={adding || removing}
|
||||||
</Button>
|
disabled={!isEmployee || adding || removing}
|
||||||
|
>
|
||||||
|
{isWatching ? t("notifications.labels.unwatch") : t("notifications.labels.watch")}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<List.Item.Meta>
|
<List.Item.Meta>
|
||||||
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
|
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
|
||||||
{t("notifications.labels.watching-issue")}
|
{t("notifications.labels.watching-issue")}
|
||||||
</Text>
|
</Text>
|
||||||
|
{!isEmployee && (
|
||||||
|
<Text type="danger" style={{ marginBottom: 8, display: "block" }}>
|
||||||
|
{t("notifications.tooltips.not-employee")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</List.Item.Meta>
|
</List.Item.Meta>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
</List>
|
</List>
|
||||||
@@ -98,12 +109,16 @@ export default function JobWatcherToggleComponent({
|
|||||||
<EmployeeSearchSelectComponent
|
<EmployeeSearchSelectComponent
|
||||||
style={{ minWidth: "100%" }}
|
style={{ minWidth: "100%" }}
|
||||||
options={
|
options={
|
||||||
bodyshop?.employees?.filter((e) =>
|
bodyshop?.employees?.filter(
|
||||||
jobWatchers.every((w) => w.user_email !== e.user_email && e.active && e.user_email)
|
(e) =>
|
||||||
|
e.user_email && // Ensure user_email is not null or undefined
|
||||||
|
e.active && // Ensure employee is active
|
||||||
|
jobWatchers.every((w) => w.user_email !== e.user_email) // Ensure not already a watcher
|
||||||
) || []
|
) || []
|
||||||
}
|
}
|
||||||
placeholder={t("notifications.labels.employee-search")}
|
placeholder={t("notifications.labels.employee-search")}
|
||||||
value={selectedWatcher}
|
value={selectedWatcher}
|
||||||
|
showEmail={true}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSelectedWatcher(value);
|
setSelectedWatcher(value);
|
||||||
handleWatcherSelect(value);
|
handleWatcherSelect(value);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -21,13 +22,14 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
splitKey: bodyshop && bodyshop.imexshopid
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
const userEmail = currentUser.email;
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
const jobid = job.id;
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [selectedWatcher, setSelectedWatcher] = useState(null);
|
const [selectedWatcher, setSelectedWatcher] = useState(null);
|
||||||
const [selectedTeam, setSelectedTeam] = useState(null);
|
const [selectedTeam, setSelectedTeam] = useState(null);
|
||||||
|
|
||||||
|
const userEmail = currentUser.email;
|
||||||
|
const jobid = job.id;
|
||||||
|
|
||||||
// Fetch current watchers with refetch capability
|
// Fetch current watchers with refetch capability
|
||||||
const {
|
const {
|
||||||
data: watcherData,
|
data: watcherData,
|
||||||
@@ -139,13 +141,13 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleToggleSelf = useCallback(async () => {
|
const handleToggleSelf = useCallback(async () => {
|
||||||
if (adding || removing) return;
|
if (adding || removing || !isEmployee) return;
|
||||||
if (isWatching) {
|
if (isWatching) {
|
||||||
await removeWatcher({ variables: { jobid, userEmail } });
|
await removeWatcher({ variables: { jobid, userEmail } });
|
||||||
} else {
|
} else {
|
||||||
await addWatcher({ variables: { jobid, userEmail } });
|
await addWatcher({ variables: { jobid, userEmail } });
|
||||||
}
|
}
|
||||||
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing]);
|
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing, isEmployee]);
|
||||||
|
|
||||||
const handleRemoveWatcher = useCallback(
|
const handleRemoveWatcher = useCallback(
|
||||||
async (email) => {
|
async (email) => {
|
||||||
@@ -187,7 +189,16 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
setSelectedTeam(null);
|
setSelectedTeam(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await Promise.all(newWatchers.map((email) => addWatcher({ variables: { jobid, userEmail: email } })));
|
await Promise.all(
|
||||||
|
newWatchers.map((email) =>
|
||||||
|
addWatcher({
|
||||||
|
variables: {
|
||||||
|
jobid,
|
||||||
|
userEmail: email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[jobWatchers, addWatcher, jobid, adding]
|
[jobWatchers, addWatcher, jobid, adding]
|
||||||
);
|
);
|
||||||
@@ -212,6 +223,7 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
handleWatcherSelect={handleWatcherSelect}
|
handleWatcherSelect={handleWatcherSelect}
|
||||||
handleTeamSelect={handleTeamSelect}
|
handleTeamSelect={handleTeamSelect}
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
|
isEmployee={isEmployee} // Pass isEmployee to the component
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,12 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
|
|||||||
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
|
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
|
||||||
<DateTimePicker />
|
<DateTimePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
|
||||||
|
<DateTimePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.estimate_approved")} name="estimate_approved">
|
||||||
|
<DateTimePicker />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.date_scheduled")} name="date_scheduled">
|
<Form.Item label={t("jobs.fields.date_scheduled")} name="date_scheduled">
|
||||||
<DateTimePicker />
|
<DateTimePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { DownloadOutlined, SyncOutlined } from "@ant-design/icons";
|
import { DownloadOutlined, SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Input, Space, Table } from "antd";
|
import { Button, Card, Input, Space, Table } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { selectPartnerVersion } from "../../redux/application/application.selectors";
|
import { selectPartnerVersion } from "../../redux/application/application.selectors";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
partnerVersion: selectPartnerVersion
|
partnerVersion: selectPartnerVersion
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAvailableScan);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsAvailableScan);
|
||||||
@@ -126,6 +126,7 @@ export function JobsAvailableScan({ partnerVersion, refetch }) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
scanEstimates();
|
scanEstimates();
|
||||||
}}
|
}}
|
||||||
|
id="scan-estimates-button"
|
||||||
>
|
>
|
||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { Col, Row } from "antd";
|
|||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import {
|
import {
|
||||||
DELETE_AVAILABLE_JOB,
|
DELETE_AVAILABLE_JOB,
|
||||||
@@ -33,7 +34,6 @@ import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.contai
|
|||||||
import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
|
import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
|
||||||
import HeaderFields from "./jobs-available-supplement.headerfields";
|
import HeaderFields from "./jobs-available-supplement.headerfields";
|
||||||
import JobsAvailableTableComponent from "./jobs-available-table.component";
|
import JobsAvailableTableComponent from "./jobs-available-table.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -195,7 +195,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
|
|
||||||
await deleteJob({
|
await deleteJob({
|
||||||
variables: { id: estData.id }
|
variables: { id: estData.id }
|
||||||
}).then((r) => {
|
}).then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
setInsertLoading(false);
|
setInsertLoading(false);
|
||||||
});
|
});
|
||||||
@@ -315,7 +315,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
|
|
||||||
deleteJob({
|
deleteJob({
|
||||||
variables: { id: estData.id }
|
variables: { id: estData.id }
|
||||||
}).then((r) => {
|
}).then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
setInsertLoading(false);
|
setInsertLoading(false);
|
||||||
});
|
});
|
||||||
@@ -372,7 +372,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
loadEstData({ variables: { id: record.id } });
|
loadEstData({ variables: { id: record.id } });
|
||||||
modalSearchState[1](record.clm_no);
|
modalSearchState[1](record.clm_no);
|
||||||
setJobModalVisible(true);
|
setJobModalVisible(true);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -456,7 +456,7 @@ function replaceEmpty(someObj, replaceValue = null) {
|
|||||||
return JSON.parse(temp);
|
return JSON.parse(temp);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function CheckTaxRatesUSA(estData, bodyshop) {
|
async function CheckTaxRatesUSA(estData) {
|
||||||
if (!estData.parts_tax_rates?.PAM) {
|
if (!estData.parts_tax_rates?.PAM) {
|
||||||
estData.parts_tax_rates.PAM = estData.parts_tax_rates.PAC;
|
estData.parts_tax_rates.PAM = estData.parts_tax_rates.PAC;
|
||||||
}
|
}
|
||||||
@@ -568,7 +568,7 @@ async function CheckTaxRates(estData, bodyshop) {
|
|||||||
});
|
});
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
function ResolveCCCLineIssues(estData, bodyshop) {
|
function ResolveCCCLineIssues(estData) {
|
||||||
//Find all misc amounts, populate them to the act price.
|
//Find all misc amounts, populate them to the act price.
|
||||||
//This needs to be done before cleansing unq_seq since some misc prices could move over.
|
//This needs to be done before cleansing unq_seq since some misc prices could move over.
|
||||||
estData.joblines.data.forEach((line) => {
|
estData.joblines.data.forEach((line) => {
|
||||||
@@ -585,6 +585,9 @@ function ResolveCCCLineIssues(estData, bodyshop) {
|
|||||||
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
|
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
|
||||||
line.mod_lbr_ty = "LAR";
|
line.mod_lbr_ty = "LAR";
|
||||||
}
|
}
|
||||||
|
if (line.mod_lbr_ty === "OTSL") {
|
||||||
|
line.mod_lbr_ty = line.mod_lbr_hrs === 0 ? null : "LAB";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { DownCircleFilled } from "@ant-design/icons";
|
import { DownCircleFilled } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Dropdown } from "antd";
|
import { Button, Dropdown } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -24,7 +24,6 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [availableStatuses, setAvailableStatuses] = useState([]);
|
const [availableStatuses, setAvailableStatuses] = useState([]);
|
||||||
const [otherStages, setOtherStages] = useState([]);
|
|
||||||
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
|
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
|||||||
mutationUpdateJobstatus({
|
mutationUpdateJobstatus({
|
||||||
variables: { jobId: job.id, status: status }
|
variables: { jobId: job.id, status: status }
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then(() => {
|
||||||
notification["success"]({ message: t("jobs.successes.save") });
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
@@ -41,7 +40,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
|||||||
});
|
});
|
||||||
// refetch();
|
// refetch();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
notification["error"]({ message: t("jobs.errors.saving") });
|
notification["error"]({ message: t("jobs.errors.saving") });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -51,19 +50,14 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
|||||||
if (job && bodyshop) {
|
if (job && bodyshop) {
|
||||||
if (bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status)) {
|
if (bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status)) {
|
||||||
setAvailableStatuses(bodyshop.md_ro_statuses.pre_production_statuses);
|
setAvailableStatuses(bodyshop.md_ro_statuses.pre_production_statuses);
|
||||||
if (bodyshop.md_ro_statuses.production_statuses[0])
|
|
||||||
setOtherStages([bodyshop.md_ro_statuses.production_statuses[0]]);
|
|
||||||
} else if (bodyshop.md_ro_statuses.production_statuses.includes(job.status)) {
|
} else if (bodyshop.md_ro_statuses.production_statuses.includes(job.status)) {
|
||||||
setAvailableStatuses(bodyshop.md_ro_statuses.production_statuses);
|
setAvailableStatuses(bodyshop.md_ro_statuses.production_statuses);
|
||||||
setOtherStages([bodyshop.md_ro_statuses.default_imported, bodyshop.md_ro_statuses.default_delivered]);
|
|
||||||
} else if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status)) {
|
} else if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status)) {
|
||||||
setAvailableStatuses(
|
setAvailableStatuses(
|
||||||
bodyshop.md_ro_statuses.post_production_statuses.filter(
|
bodyshop.md_ro_statuses.post_production_statuses.filter(
|
||||||
(s) => s !== bodyshop.md_ro_statuses.default_invoiced && s !== bodyshop.md_ro_statuses.default_exported
|
(s) => s !== bodyshop.md_ro_statuses.default_invoiced && s !== bodyshop.md_ro_statuses.default_exported
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (bodyshop.md_ro_statuses.production_statuses[0])
|
|
||||||
setOtherStages([bodyshop.md_ro_statuses.production_statuses[0]]);
|
|
||||||
} else {
|
} else {
|
||||||
console.log("Status didn't match any restrictions. Allowing all status changes.");
|
console.log("Status didn't match any restrictions. Allowing all status changes.");
|
||||||
setAvailableStatuses(bodyshop.md_ro_statuses.statuses);
|
setAvailableStatuses(bodyshop.md_ro_statuses.statuses);
|
||||||
@@ -76,16 +70,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
|||||||
...availableStatuses.map((item) => ({
|
...availableStatuses.map((item) => ({
|
||||||
key: item,
|
key: item,
|
||||||
label: item
|
label: item
|
||||||
})),
|
}))
|
||||||
...(job.converted
|
|
||||||
? [
|
|
||||||
{ type: "divider" },
|
|
||||||
...otherStages.map((item) => ({
|
|
||||||
key: item,
|
|
||||||
label: item
|
|
||||||
}))
|
|
||||||
]
|
|
||||||
: [])
|
|
||||||
],
|
],
|
||||||
onClick: (e) => updateJobStatus(e.key)
|
onClick: (e) => updateJobStatus(e.key)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { WarningOutlined } from "@ant-design/icons";
|
||||||
import { Form, Select, Space, Tooltip } from "antd";
|
import { Form, Select, Space, Tooltip } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -8,14 +8,13 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import LaborTypeFormItem from "../form-items-formatted/labor-type-form-item.component";
|
import LaborTypeFormItem from "../form-items-formatted/labor-type-form-item.component";
|
||||||
import PartTypeFormItem from "../form-items-formatted/part-type-form-item.component";
|
import PartTypeFormItem from "../form-items-formatted/part-type-form-item.component";
|
||||||
import ReadOnlyFormItem from "../form-items-formatted/read-only-form-item.component";
|
import ReadOnlyFormItem from "../form-items-formatted/read-only-form-item.component";
|
||||||
import { WarningOutlined } from "@ant-design/icons";
|
|
||||||
import "./jobs-close-lines.styles.scss";
|
import "./jobs-close-lines.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
jobRO: selectJobReadOnly
|
jobRO: selectJobReadOnly
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.List name={["joblines"]}>
|
<Form.List name={["joblines"]}>
|
||||||
{(fields, { add, remove, move }) => {
|
{(fields) => {
|
||||||
return (
|
return (
|
||||||
<table className="jobs-close-table">
|
<table className="jobs-close-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
td {
|
td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid var(--table-border-color);
|
||||||
|
|
||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
margin-bottom: 0px !important;
|
margin-bottom: 0px !important;
|
||||||
@@ -14,6 +14,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr:hover {
|
tr:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: var(--table-hover-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Form, Input, Switch } from "antd";
|
import { Form, Input } from "antd";
|
||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||||
@@ -129,13 +129,6 @@ export default function JobsCreateOwnerInfoNewComponent() {
|
|||||||
<Form.Item label={t("owners.fields.preferred_contact")} name={["owner", "data", "preferred_contact"]}>
|
<Form.Item label={t("owners.fields.preferred_contact")} name={["owner", "data", "preferred_contact"]}>
|
||||||
<Input disabled={!state.owner.new} />
|
<Input disabled={!state.owner.new} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
|
||||||
label={t("owners.fields.allow_text_message")}
|
|
||||||
valuePropName="checked"
|
|
||||||
name={["owner", "data", "allow_text_message"]}
|
|
||||||
>
|
|
||||||
<Switch disabled={!state.owner.new} />
|
|
||||||
</Form.Item>
|
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Card, Input, Table } from "antd";
|
import { Card, Input, Table } from "antd";
|
||||||
import React, { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
@@ -91,6 +91,7 @@ export default function JobsCreateOwnerInfoSearchComponent({ loading, owners })
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
enterButton
|
enterButton
|
||||||
|
id="search-owner"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -112,9 +113,9 @@ export default function JobsCreateOwnerInfoSearchComponent({ loading, owners })
|
|||||||
type: "radio",
|
type: "radio",
|
||||||
selectedRowKeys: [state.owner.selectedid]
|
selectedRowKeys: [state.owner.selectedid]
|
||||||
}}
|
}}
|
||||||
onRow={(record, rowIndex) => {
|
onRow={(record) => {
|
||||||
return {
|
return {
|
||||||
onClick: (event) => {
|
onClick: () => {
|
||||||
if (record) {
|
if (record) {
|
||||||
if (record.id) {
|
if (record.id) {
|
||||||
setState({
|
setState({
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
|
|||||||
open={open}
|
open={open}
|
||||||
placement="left"
|
placement="left"
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
destroyTooltipOnHide
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<SearchOutlined style={{ cursor: "pointer" }} />
|
<SearchOutlined style={{ cursor: "pointer" }} />
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useContext, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Card, Input, Space, Table } from "antd";
|
import { Card, Input, Space, Table } from "antd";
|
||||||
|
import { useContext, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
|
||||||
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||||
|
|
||||||
export default function JobsCreateVehicleInfoSearchComponent({ loading, vehicles }) {
|
export default function JobsCreateVehicleInfoSearchComponent({ loading, vehicles }) {
|
||||||
@@ -63,6 +63,7 @@ export default function JobsCreateVehicleInfoSearchComponent({ loading, vehicles
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
enterButton
|
enterButton
|
||||||
|
id="search-vehicle"
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -91,9 +92,9 @@ export default function JobsCreateVehicleInfoSearchComponent({ loading, vehicles
|
|||||||
type: "radio",
|
type: "radio",
|
||||||
selectedRowKeys: [state.vehicle.selectedid]
|
selectedRowKeys: [state.vehicle.selectedid]
|
||||||
}}
|
}}
|
||||||
onRow={(record, rowIndex) => {
|
onRow={(record) => {
|
||||||
return {
|
return {
|
||||||
onClick: (event) => {
|
onClick: () => {
|
||||||
if (record) {
|
if (record) {
|
||||||
if (record.id) {
|
if (record.id) {
|
||||||
setState({
|
setState({
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Form, Statistic, Tooltip } from "antd";
|
import { Form, Statistic, Tooltip } from "antd";
|
||||||
import React, { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import FormRow from "../layout-form-row/layout-form-row.component";
|
import FormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
@@ -40,6 +41,20 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
<Form.Item label={t("jobs.fields.date_rentalresp")} name="date_rentalresp">
|
<Form.Item label={t("jobs.fields.date_rentalresp")} name="date_rentalresp">
|
||||||
<DateTimePicker disabled={jobRO} />
|
<DateTimePicker disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
|
||||||
|
<DateTimePicker
|
||||||
|
disabled={jobRO}
|
||||||
|
value={job.estimate_sent_approval ? dayjs(job.estimate_sent_approval) : null}
|
||||||
|
placeholder={t("general.labels.na")}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.estimate_approved")} name="estimate_approved">
|
||||||
|
<DateTimePicker
|
||||||
|
disabled={jobRO}
|
||||||
|
value={job.estimate_approved ? dayjs(job.estimate_approved) : null}
|
||||||
|
placeholder={t("general.labels.na")}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
|
|
||||||
<FormRow header={t("jobs.forms.scheddates")}>
|
<FormRow header={t("jobs.forms.scheddates")}>
|
||||||
@@ -48,7 +63,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Tooltip title={t("jobs.labels.scheduledinchange")}>
|
<Tooltip title={t("jobs.labels.scheduledinchange")}>
|
||||||
<Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in">
|
<Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
||||||
@@ -76,21 +91,15 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
<DateTimePicker disabled={jobRO} />
|
<DateTimePicker disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item shouldUpdate>
|
<Form.Item shouldUpdate>
|
||||||
{() => {
|
{() => (
|
||||||
return (
|
<Form.Item
|
||||||
<Form.Item
|
label={t("jobs.fields.actual_completion")}
|
||||||
label={t("jobs.fields.actual_completion")}
|
name="actual_completion"
|
||||||
name="actual_completion"
|
rules={[{ required: jobInPostProduction }]}
|
||||||
rules={[
|
>
|
||||||
{
|
<DateTimePicker disabled={jobRO} />
|
||||||
required: jobInPostProduction
|
</Form.Item>
|
||||||
}
|
)}
|
||||||
]}
|
|
||||||
>
|
|
||||||
<DateTimePicker disabled={jobRO} />
|
|
||||||
</Form.Item>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.scheduled_delivery")} name="scheduled_delivery">
|
<Form.Item label={t("jobs.fields.scheduled_delivery")} name="scheduled_delivery">
|
||||||
<DateTimePicker disabled={jobRO} />
|
<DateTimePicker disabled={jobRO} />
|
||||||
@@ -101,19 +110,16 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow header={t("jobs.forms.admindates")}>
|
<FormRow header={t("jobs.forms.admindates")}>
|
||||||
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
|
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
|
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale">
|
<Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
import { Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -188,6 +187,12 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
<Form.Item label={t("jobs.fields.tlos_ind")} name="tlos_ind" valuePropName="checked">
|
<Form.Item label={t("jobs.fields.tlos_ind")} name="tlos_ind" valuePropName="checked">
|
||||||
<Switch disabled={jobRO} />
|
<Switch disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.hit_and_run")} name="hit_and_run" valuePropName="checked">
|
||||||
|
<Switch disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.acv_amount")} name="acv_amount">
|
||||||
|
<CurrencyInput disabled={jobRO} min={0} />
|
||||||
|
</Form.Item>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...lossColDamage}>
|
<Col {...lossColDamage}>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
||||||
@@ -32,7 +33,6 @@ import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
|||||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -673,7 +673,9 @@ export function JobsDetailHeaderActions({
|
|||||||
context: {
|
context: {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
job: job,
|
job: job,
|
||||||
alt_transport: job.alt_transport
|
alt_transport: job.alt_transport,
|
||||||
|
scheduled_in: job.scheduled_in,
|
||||||
|
scheduled_completion: job.scheduled_completion
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1078,17 +1080,22 @@ export function JobsDetailHeaderActions({
|
|||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "deletejob",
|
key: "deletejob",
|
||||||
id: "job-actions-deletejob",
|
id: "job-actions-deletejob",
|
||||||
label: (
|
label:
|
||||||
<Popconfirm
|
job.job_watchers.length === 0 ? (
|
||||||
title={t("jobs.labels.deleteconfirm")}
|
<Popconfirm
|
||||||
okText={t("general.labels.yes")}
|
title={t("jobs.labels.deleteconfirm")}
|
||||||
cancelText={t("general.labels.no")}
|
okText={t("general.labels.yes")}
|
||||||
onClick={(e) => e.stopPropagation()}
|
cancelText={t("general.labels.no")}
|
||||||
onConfirm={handleDeleteJob}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
onConfirm={handleDeleteJob}
|
||||||
{t("menus.jobsactions.deletejob")}
|
>
|
||||||
</Popconfirm>
|
{t("menus.jobsactions.deletejob")}
|
||||||
)
|
</Popconfirm>
|
||||||
|
) : (
|
||||||
|
<Popconfirm title={t("jobs.labels.deletewatchers")} onClick={(e) => e.stopPropagation()} showCancel={false}>
|
||||||
|
{t("menus.jobsactions.deletejob")}
|
||||||
|
</Popconfirm>
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1109,8 +1116,8 @@ export function JobsDetailHeaderActions({
|
|||||||
<RbacWrapper action="jobs:void" noauth>
|
<RbacWrapper action="jobs:void" noauth>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t("jobs.labels.voidjob")}
|
title={t("jobs.labels.voidjob")}
|
||||||
okText="Yes"
|
okText={t("general.labels.yes")}
|
||||||
cancelText="No"
|
cancelText={t("general.labels.no")}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onConfirm={handleVoidJob}
|
onConfirm={handleVoidJob}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { GET_JOB_BY_PK_QUICK_INTAKE, JOB_PRODUCTION_TOGGLE } from "../../graphql/jobs.queries";
|
import { GET_JOB_BY_PK_QUICK_INTAKE, JOB_PRODUCTION_TOGGLE } from "../../graphql/jobs.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
@@ -12,7 +13,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -44,9 +44,16 @@ export function JobsDetailHeaderActionsToggleProduction({
|
|||||||
variables: { id: job.id },
|
variables: { id: job.id },
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
if (data?.jobs_by_pk) {
|
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({
|
form.setFieldsValue({
|
||||||
actual_in: data.jobs_by_pk.actual_in ? data.jobs_by_pk.actual_in : dayjs(),
|
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,
|
actual_completion: data.jobs_by_pk.actual_completion,
|
||||||
scheduled_delivery: data.jobs_by_pk.scheduled_delivery,
|
scheduled_delivery: data.jobs_by_pk.scheduled_delivery,
|
||||||
actual_delivery: data.jobs_by_pk.actual_delivery
|
actual_delivery: data.jobs_by_pk.actual_delivery
|
||||||
@@ -160,7 +167,18 @@ export function JobsDetailHeaderActionsToggleProduction({
|
|||||||
<FormDateTimePickerComponent disabled={jobRO} />
|
<FormDateTimePickerComponent disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name={["actual_delivery"]} label={t("jobs.fields.actual_delivery")}>
|
<Form.Item
|
||||||
|
name={["actual_delivery"]}
|
||||||
|
label={t("jobs.fields.actual_delivery")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: bodyshop.deliverchecklist.actual_delivery
|
||||||
|
? bodyshop.deliverchecklist.actual_delivery
|
||||||
|
: false
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
<FormDateTimePickerComponent disabled={jobRO} />
|
<FormDateTimePickerComponent disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled } from "@ant-design/icons";
|
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled } from "@ant-design/icons";
|
||||||
import { Card, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
|
import { useMutation } from "@apollo/client";
|
||||||
import React, { useState } from "react";
|
import { Card, Checkbox, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
|
||||||
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter, DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||||
import DataLabel from "../data-label/data-label.component";
|
import DataLabel from "../data-label/data-label.component";
|
||||||
@@ -17,11 +23,11 @@ import JobAltTransportChange from "../job-at-change/job-at-change.component";
|
|||||||
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
|
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
|
||||||
import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
|
import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
|
||||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
|
import PinnedJobNotes from "../pinned-job-notes/pinned-job-notes.component.jsx";
|
||||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||||
import "./jobs-detail-header.styles.scss";
|
import "./jobs-detail-header.styles.scss";
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -29,255 +35,325 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" }))
|
setPrintCenterContext: (context) =>
|
||||||
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "printCenter"
|
||||||
|
})
|
||||||
|
),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
const colSpan = {
|
const colSpan = {
|
||||||
xs: {
|
xs: { span: 24 },
|
||||||
span: 24
|
sm: { span: 24 },
|
||||||
},
|
md: { span: 12 },
|
||||||
sm: {
|
lg: { span: 6 },
|
||||||
span: 24
|
xl: { span: 6 }
|
||||||
},
|
|
||||||
md: {
|
|
||||||
span: 12
|
|
||||||
},
|
|
||||||
lg: {
|
|
||||||
span: 6
|
|
||||||
},
|
|
||||||
xl: {
|
|
||||||
span: 6
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { notification } = useNotification();
|
||||||
const [notesClamped, setNotesClamped] = useState(true);
|
const [notesClamped, setNotesClamped] = useState(true);
|
||||||
const vehicleTitle = `${job.v_model_yr || ""} ${job.v_color || ""}
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
${job.v_make_desc || ""}
|
const vehicleTitle =
|
||||||
${job.v_model_desc || ""}`.trim();
|
`${job.v_model_yr || ""} ${job.v_color || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim();
|
||||||
|
|
||||||
const bodyHrs = job.joblines.filter((j) => j.mod_lbr_ty !== "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
const bodyHrs = job.joblines.filter((j) => j.mod_lbr_ty !== "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
||||||
const refinishHrs = job.joblines
|
const refinishHrs = job.joblines
|
||||||
.filter((line) => line.mod_lbr_ty === "LAR")
|
.filter((line) => line.mod_lbr_ty === "LAR")
|
||||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
||||||
|
|
||||||
const ownerTitle = OwnerNameDisplayFunction(job).trim();
|
const ownerTitle = OwnerNameDisplayFunction(job).trim();
|
||||||
|
|
||||||
|
// Handle checkbox changes
|
||||||
|
const handleCheckboxChange = async (field, checked) => {
|
||||||
|
const value = checked ? dayjs().toISOString() : null;
|
||||||
|
try {
|
||||||
|
const ret = await updateJob({
|
||||||
|
variables: {
|
||||||
|
jobId: job.id,
|
||||||
|
job: { [field]: value }
|
||||||
|
},
|
||||||
|
refetchQueries: ["GET_JOB_BY_PK"],
|
||||||
|
awaitRefetchQueries: true
|
||||||
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: job.id,
|
||||||
|
operation: AuditTrailMapping.jobfieldchange(
|
||||||
|
field,
|
||||||
|
ret.data.update_jobs.returning[0][field]
|
||||||
|
? DateTimeFormatterFunction(ret.data.update_jobs.returning[0][field])
|
||||||
|
: checked
|
||||||
|
),
|
||||||
|
type: "jobfieldchange"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
message: t("jobs.errors.saving", { error: error.message })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
|
<>
|
||||||
<Col {...colSpan}>
|
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
|
||||||
<Card title={"Job Status"} style={{ height: "100%" }}>
|
<Col {...colSpan}>
|
||||||
<div>
|
<Card title={"Job Status"} style={{ height: "100%" }}>
|
||||||
<DataLabel label={t("jobs.fields.status")}>
|
<div>
|
||||||
|
<DataLabel label={t("jobs.fields.status")}>
|
||||||
|
<Space wrap>
|
||||||
|
{job.status}
|
||||||
|
{job.inproduction && <Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>}
|
||||||
|
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||||
|
{job.iouparent && (
|
||||||
|
<Link to={`/manage/jobs/${job.iouparent}`}>
|
||||||
|
<Tooltip title={t("jobs.labels.iou")}>
|
||||||
|
<BranchesOutlined style={{ color: "orangered" }} />
|
||||||
|
</Tooltip>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{job.production_vars && job.production_vars.alert ? (
|
||||||
|
<ExclamationCircleFilled className="production-alert" />
|
||||||
|
) : null}
|
||||||
|
{job.status === bodyshop.md_ro_statuses.default_scheduled && job.scheduled_in ? (
|
||||||
|
<Tag>
|
||||||
|
<Link to={`/manage/schedule?date=${dayjs(job.scheduled_in).format("YYYY-MM-DD")}`}>
|
||||||
|
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
|
||||||
|
</Link>
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||||
|
<ProductionListColumnComment record={job} />
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
|
||||||
|
{job.po_number}
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.repairtotal")}>
|
||||||
|
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||||
|
<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} />
|
||||||
|
</DataLabel>
|
||||||
|
{job?.cccontracts?.length > 0 && (
|
||||||
|
<DataLabel label={t("jobs.labels.contracts")}>
|
||||||
|
{job.cccontracts.map((c, index) => (
|
||||||
|
<Space key={c.id} wrap>
|
||||||
|
<Link to={`/manage/courtesycars/contracts/${c.id}`}>
|
||||||
|
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
|
||||||
|
{index !== job.cccontracts.length - 1 ? "," : null}
|
||||||
|
</Link>
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
</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>
|
<Space wrap>
|
||||||
{job.status}
|
{job.special_coverage_policy && (
|
||||||
{job.inproduction && (
|
<Tag color="tomato">
|
||||||
<Tag color="#f50" key="production">
|
<Space>
|
||||||
{t("jobs.labels.inproduction")}
|
<WarningFilled />
|
||||||
|
<span>{t("jobs.labels.specialcoveragepolicy")}</span>
|
||||||
|
</Space>
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
{job.ca_gst_registrant && (
|
||||||
{job.iouparent && (
|
<Tag color="geekblue">
|
||||||
<Link to={`/manage/jobs/${job.iouparent}`}>
|
<Space>
|
||||||
<Tooltip title={t("jobs.labels.iou")}>
|
<WarningFilled />
|
||||||
<BranchesOutlined style={{ color: "orangered" }} />
|
<span>{t("jobs.fields.ca_gst_registrant")}</span>
|
||||||
</Tooltip>
|
</Space>
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{job.production_vars && job.production_vars.alert ? (
|
|
||||||
<ExclamationCircleFilled className="production-alert" />
|
|
||||||
) : null}
|
|
||||||
{job.status === bodyshop.md_ro_statuses.default_scheduled && job.scheduled_in ? (
|
|
||||||
<Tag>
|
|
||||||
<Link to={`/manage/schedule?date=${dayjs(job.scheduled_in).format("YYYY-MM-DD")}`}>
|
|
||||||
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
|
|
||||||
</Link>
|
|
||||||
</Tag>
|
</Tag>
|
||||||
) : null}
|
)}
|
||||||
|
{job.hit_and_run && (
|
||||||
|
<Tag color="green">
|
||||||
|
<Space>
|
||||||
|
<WarningFilled />
|
||||||
|
<span>{t("jobs.fields.hit_and_run")}</span>
|
||||||
|
</Space>
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</DataLabel>
|
</div>
|
||||||
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
</Card>
|
||||||
<ProductionListColumnComment record={job} />
|
</Col>
|
||||||
</DataLabel>
|
<Col {...colSpan}>
|
||||||
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>
|
<Card
|
||||||
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
style={{ height: "100%" }}
|
||||||
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
|
title={
|
||||||
{job.po_number}
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel label={t("jobs.fields.repairtotal")}>
|
|
||||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
|
||||||
<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} />
|
|
||||||
</DataLabel>
|
|
||||||
{job?.cccontracts?.length > 0 && (
|
|
||||||
<DataLabel label={t("jobs.labels.contracts")}>
|
|
||||||
{job.cccontracts.map((c, index) => (
|
|
||||||
<Space key={c.id} wrap>
|
|
||||||
<Link to={`/manage/courtesycars/contracts/${c.id}`}>
|
|
||||||
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
|
|
||||||
{index !== job.cccontracts.length - 1 ? "," : null}
|
|
||||||
</Link>
|
|
||||||
</Space>
|
|
||||||
))}
|
|
||||||
</DataLabel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
|
||||||
<ProductionListColumnProductionNote record={job} />
|
|
||||||
</DataLabel>
|
|
||||||
|
|
||||||
<Space wrap>
|
|
||||||
{job.special_coverage_policy && (
|
|
||||||
<Tag color="tomato">
|
|
||||||
<Space>
|
|
||||||
<WarningFilled />
|
|
||||||
<span>{t("jobs.labels.specialcoveragepolicy")}</span>
|
|
||||||
</Space>
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
{job.ca_gst_registrant && (
|
|
||||||
<Tag color="geekblue">
|
|
||||||
<Space>
|
|
||||||
<WarningFilled />
|
|
||||||
<span>{t("jobs.fields.ca_gst_registrant")}</span>
|
|
||||||
</Space>
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col {...colSpan}>
|
|
||||||
<Card
|
|
||||||
style={{ height: "100%" }}
|
|
||||||
title={
|
|
||||||
disabled ? (
|
|
||||||
<>{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}</>
|
|
||||||
) : (
|
|
||||||
<Link to={`/manage/owners/${job.owner.id}`}>
|
|
||||||
{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
|
|
||||||
{disabled ? (
|
|
||||||
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
|
|
||||||
) : (
|
|
||||||
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
|
|
||||||
)}
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
|
|
||||||
{disabled ? (
|
|
||||||
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
|
|
||||||
) : (
|
|
||||||
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
|
|
||||||
)}
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel key="3" label={t("owners.fields.address")}>
|
|
||||||
{`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${
|
|
||||||
job.ownr_city || ""
|
|
||||||
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
|
|
||||||
{disabled ? (
|
|
||||||
<>{job.ownr_ea || ""}</>
|
|
||||||
) : job.ownr_ea ? (
|
|
||||||
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
|
|
||||||
) : null}
|
|
||||||
</DataLabel>
|
|
||||||
{job.owner?.tax_number && (
|
|
||||||
<DataLabel key="5" label={t("owners.fields.tax_number")}>
|
|
||||||
{job.owner?.tax_number || ""}
|
|
||||||
</DataLabel>
|
|
||||||
)}
|
|
||||||
<DataLabel label={t("owners.fields.note")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
||||||
{job.owner?.note || ""}
|
|
||||||
</DataLabel>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col {...colSpan}>
|
|
||||||
<Card
|
|
||||||
style={{ height: "100%" }}
|
|
||||||
title={
|
|
||||||
job.vehicle ? (
|
|
||||||
disabled ? (
|
disabled ? (
|
||||||
<>{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")} </>
|
<>{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}</>
|
||||||
) : (
|
) : (
|
||||||
<Link to={job.vehicle && `/manage/vehicles/${job.vehicle.id}`}>
|
<Link to={`/manage/owners/${job.owner.id}`}>
|
||||||
{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")}
|
{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
) : (
|
}
|
||||||
<span></span>
|
>
|
||||||
)
|
<div>
|
||||||
}
|
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
|
||||||
>
|
{disabled ? (
|
||||||
<div>
|
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
|
||||||
<DataLabel key="2" label={t("vehicles.fields.plate_no")}>
|
) : (
|
||||||
{`${job.plate_no || t("general.labels.na")} (${`${job.plate_st || t("general.labels.na")}`})`}
|
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
|
||||||
</DataLabel>
|
)}
|
||||||
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
|
|
||||||
<VehicleVinDisplay>{`${job.v_vin || t("general.labels.na")}`}</VehicleVinDisplay>
|
|
||||||
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
|
||||||
job.v_vin?.length !== 17 ? (
|
|
||||||
<WarningFilled style={{ color: "tomato", marginLeft: ".3rem" }} />
|
|
||||||
) : null
|
|
||||||
) : null}
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel label={t("jobs.fields.regie_number")}>{job.regie_number || t("general.labels.na")}</DataLabel>
|
|
||||||
<DataLabel label={t("jobs.labels.relatedros")}>
|
|
||||||
<JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
|
|
||||||
</DataLabel>
|
|
||||||
{job.vehicle && job.vehicle.notes && (
|
|
||||||
<DataLabel
|
|
||||||
label={t("vehicles.fields.notes")}
|
|
||||||
valueStyle={{ whiteSpace: "pre-wrap" }}
|
|
||||||
valueClassName={notesClamped ? "clamp" : ""}
|
|
||||||
onValueClick={() => setNotesClamped(!notesClamped)}
|
|
||||||
>
|
|
||||||
{job.vehicle.notes}
|
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
)}
|
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
|
||||||
{job.vehicle && job.vehicle.v_paint_codes && (
|
{disabled ? (
|
||||||
<DataLabel label={t("vehicles.fields.v_paint_codes", { number: "" })}>
|
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
|
||||||
<span style={{ whiteSpace: "pre" }}>
|
) : (
|
||||||
{Object.keys(job.vehicle.v_paint_codes)
|
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
|
||||||
.filter(
|
)}
|
||||||
(key) =>
|
|
||||||
job.vehicle.v_paint_codes[key] !== "" &&
|
|
||||||
job.vehicle.v_paint_codes[key] !== null &&
|
|
||||||
job.vehicle.v_paint_codes[key] !== undefined
|
|
||||||
)
|
|
||||||
.map((key, idx) => (
|
|
||||||
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
)}
|
<DataLabel key="3" label={t("owners.fields.address")}>
|
||||||
</div>
|
{`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${
|
||||||
</Card>
|
job.ownr_city || ""
|
||||||
</Col>
|
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
|
||||||
<Col {...colSpan}>
|
</DataLabel>
|
||||||
<Card style={{ height: "100%" }} title={t("jobs.labels.employeeassignments")}>
|
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
|
||||||
<div>
|
{disabled ? (
|
||||||
<JobEmployeeAssignments job={job} />
|
<>{job.ownr_ea || ""}</>
|
||||||
<Divider style={{ margin: ".5rem" }} />
|
) : job.ownr_ea ? (
|
||||||
<DataLabel label={t("jobs.labels.labor_hrs")}>
|
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
|
||||||
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} / {(bodyHrs + refinishHrs).toFixed(1)}
|
) : null}
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
</div>
|
{job.owner?.tax_number && (
|
||||||
</Card>
|
<DataLabel key="5" label={t("owners.fields.tax_number")}>
|
||||||
</Col>
|
{job.owner?.tax_number || ""}
|
||||||
</Row>
|
</DataLabel>
|
||||||
|
)}
|
||||||
|
<DataLabel label={t("owners.fields.note")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||||
|
{job.owner?.note || ""}
|
||||||
|
</DataLabel>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col {...colSpan}>
|
||||||
|
<Card
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
title={
|
||||||
|
job.vehicle ? (
|
||||||
|
disabled ? (
|
||||||
|
<>{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")} </>
|
||||||
|
) : (
|
||||||
|
<Link to={job.vehicle && `/manage/vehicles/${job.vehicle.id}`}>
|
||||||
|
{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span></span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<DataLabel key="2" label={t("vehicles.fields.plate_no")}>
|
||||||
|
{`${job.plate_no || t("general.labels.na")} (${`${job.plate_st || t("general.labels.na")}`})`}
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
|
||||||
|
<VehicleVinDisplay>{`${job.v_vin || t("general.labels.na")}`}</VehicleVinDisplay>
|
||||||
|
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
||||||
|
job.v_vin?.length !== 17 ? (
|
||||||
|
<WarningFilled style={{ color: "tomato", marginLeft: ".3rem" }} />
|
||||||
|
) : null
|
||||||
|
) : null}
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.regie_number")}>{job.regie_number || t("general.labels.na")}</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.labels.relatedros")}>
|
||||||
|
<JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
|
||||||
|
</DataLabel>
|
||||||
|
{job.vehicle && job.vehicle.notes && (
|
||||||
|
<DataLabel
|
||||||
|
label={t("vehicles.fields.notes")}
|
||||||
|
valueStyle={{ whiteSpace: "pre-wrap" }}
|
||||||
|
valueClassName={notesClamped ? "clamp" : ""}
|
||||||
|
onValueClick={() => setNotesClamped(!notesClamped)}
|
||||||
|
>
|
||||||
|
{job.vehicle.notes}
|
||||||
|
</DataLabel>
|
||||||
|
)}
|
||||||
|
{job.vehicle && job.vehicle.v_paint_codes && (
|
||||||
|
<DataLabel label={t("vehicles.fields.v_paint_codes", { number: "" })}>
|
||||||
|
<span style={{ whiteSpace: "pre" }}>
|
||||||
|
{Object.keys(job.vehicle.v_paint_codes)
|
||||||
|
.filter(
|
||||||
|
(key) =>
|
||||||
|
job.vehicle.v_paint_codes[key] !== "" &&
|
||||||
|
job.vehicle.v_paint_codes[key] !== null &&
|
||||||
|
job.vehicle.v_paint_codes[key] !== undefined
|
||||||
|
)
|
||||||
|
.map((key, idx) => (
|
||||||
|
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</DataLabel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col {...colSpan}>
|
||||||
|
<Card
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
title={<span id="job-employee-assignments-title">{t("jobs.labels.employeeassignments")}</span>}
|
||||||
|
id={"job-employee-assignments"}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<JobEmployeeAssignments job={job} />
|
||||||
|
<Divider style={{ margin: ".5rem" }} />
|
||||||
|
<DataLabel label={t("jobs.labels.labor_hrs")}>
|
||||||
|
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} / {(bodyHrs + refinishHrs).toFixed(1)}
|
||||||
|
</DataLabel>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<PinnedJobNotes job={job} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function JobsDocumentsContainer({
|
|||||||
variables: { jobId: jobId },
|
variables: { jobId: jobId },
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
skip: Imgproxy.treatment === "on" || !!billId
|
skip: !!billId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { Button, Space } from "antd";
|
import { Button, Space } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import cleanAxios from "../../utils/CleanAxios";
|
|
||||||
import formatBytes from "../../utils/formatbytes";
|
import formatBytes from "../../utils/formatbytes";
|
||||||
//import yauzl from "yauzl";
|
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
@@ -14,7 +11,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -28,7 +25,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
|
||||||
|
|
||||||
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier }) {
|
export function JobsDocumentsImgproxyDownloadButton({ galleryImages, identifier, jobId }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [download, setDownload] = useState(null);
|
const [download, setDownload] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -46,32 +43,42 @@ export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, i
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function standardMediaDownload(bufferData) {
|
function standardMediaDownload(bufferData) {
|
||||||
const a = document.createElement("a");
|
try {
|
||||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||||
a.download = `${identifier || "documents"}.zip`;
|
a.href = url;
|
||||||
a.click();
|
a.download = `${identifier || "documents"}.zip`;
|
||||||
|
a.click();
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
setDownload(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
logImEXEvent("jobs_documents_download");
|
logImEXEvent("jobs_documents_download");
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const zipUrl = await axios({
|
try {
|
||||||
url: "/media/imgproxy/download",
|
const response = await axios({
|
||||||
method: "POST",
|
url: "/media/imgproxy/download",
|
||||||
data: { documentids: imagesToDownload.map((_) => _.id) }
|
method: "POST",
|
||||||
});
|
responseType: "blob",
|
||||||
|
data: { jobId, documentids: imagesToDownload.map((_) => _.id) },
|
||||||
|
onDownloadProgress: downloadProgress
|
||||||
|
});
|
||||||
|
|
||||||
const theDownloadedZip = await cleanAxios({
|
setLoading(false);
|
||||||
url: zipUrl.data.url,
|
setDownload(null);
|
||||||
method: "GET",
|
|
||||||
responseType: "arraybuffer",
|
|
||||||
onDownloadProgress: downloadProgress
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
setDownload(null);
|
|
||||||
|
|
||||||
standardMediaDownload(theDownloadedZip.data);
|
// Use the response data (Blob) to trigger download
|
||||||
|
standardMediaDownload(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
setDownload(null);
|
||||||
|
// handle error (optional)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ function JobsDocumentsImgproxyComponent({
|
|||||||
const [modalState, setModalState] = useState({ open: false, index: 0 });
|
const [modalState, setModalState] = useState({ open: false, index: 0 });
|
||||||
|
|
||||||
const fetchThumbnails = useCallback(() => {
|
const fetchThumbnails = useCallback(() => {
|
||||||
fetchImgproxyThumbnails({ setStateCallback: setGalleryImages, jobId });
|
fetchImgproxyThumbnails({ setStateCallback: setGalleryImages, jobId, billId });
|
||||||
}, [jobId, setGalleryImages]);
|
}, [jobId, billId, setGalleryImages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -75,7 +75,7 @@ function JobsDocumentsImgproxyComponent({
|
|||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setGalleryImages} />
|
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setGalleryImages} />
|
||||||
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} />
|
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} jobId={jobId} />
|
||||||
<JobsDocumentsDeleteButton
|
<JobsDocumentsDeleteButton
|
||||||
galleryImages={galleryImages}
|
galleryImages={galleryImages}
|
||||||
deletionCallback={billsCallback || fetchThumbnails || refetch}
|
deletionCallback={billsCallback || fetchThumbnails || refetch}
|
||||||
@@ -98,7 +98,13 @@ function JobsDocumentsImgproxyComponent({
|
|||||||
jobId={jobId}
|
jobId={jobId}
|
||||||
totalSize={totalSize}
|
totalSize={totalSize}
|
||||||
billId={billId}
|
billId={billId}
|
||||||
callbackAfterUpload={billsCallback || fetchThumbnails || refetch}
|
callbackAfterUpload={
|
||||||
|
billsCallback ||
|
||||||
|
function () {
|
||||||
|
isFunction(refetch) && refetch();
|
||||||
|
isFunction(fetchThumbnails) && fetchThumbnails();
|
||||||
|
}
|
||||||
|
}
|
||||||
ignoreSizeLimit={ignoreSizeLimit}
|
ignoreSizeLimit={ignoreSizeLimit}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -202,8 +208,8 @@ function JobsDocumentsImgproxyComponent({
|
|||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyComponent);
|
||||||
|
|
||||||
export const fetchImgproxyThumbnails = async ({ setStateCallback, jobId, imagesOnly }) => {
|
export const fetchImgproxyThumbnails = async ({ setStateCallback, jobId, billId, imagesOnly }) => {
|
||||||
const result = await axios.post("/media/imgproxy/thumbnails", { jobid: jobId });
|
const result = await axios.post("/media/imgproxy/thumbnails", { jobid: jobId, billid: billId });
|
||||||
const documents = result.data.reduce(
|
const documents = result.data.reduce(
|
||||||
(acc, value) => {
|
(acc, value) => {
|
||||||
if (value.type.startsWith("image")) {
|
if (value.type.startsWith("image")) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Checkbox, Divider, Input, Space, Table } from "antd";
|
import { Button, Checkbox, Divider, Input, Space, Table } from "antd";
|
||||||
import dayjs from "../../utils/day";
|
import { useState } from "react";
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
@@ -223,9 +223,9 @@ export default function JobsFindModalComponent({
|
|||||||
type: "radio",
|
type: "radio",
|
||||||
selectedRowKeys: [selectedJob]
|
selectedRowKeys: [selectedJob]
|
||||||
}}
|
}}
|
||||||
onRow={(record, rowIndex) => {
|
onRow={(record) => {
|
||||||
return {
|
return {
|
||||||
onClick: (event) => {
|
onClick: () => {
|
||||||
handleOnRowClick(record);
|
handleOnRowClick(record);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -241,15 +241,17 @@ export default function JobsFindModalComponent({
|
|||||||
overrideHeaders: e.target.checked
|
overrideHeaders: e.target.checked
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
id="override_header"
|
||||||
>
|
>
|
||||||
{t("jobs.labels.override_header")}
|
{t("jobs.labels.override_header")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox checked={partsQueueToggle} onChange={(e) => setPartsQueueToggle(e.target.checked)}>
|
<Checkbox checked={partsQueueToggle} onChange={(e) => setPartsQueueToggle(e.target.checked)} id="parts_queue_toggle">
|
||||||
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
|
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={updateSchComp.checked}
|
checked={updateSchComp.checked}
|
||||||
onChange={(e) => setSchComp({ ...updateSchComp, checked: e.target.checked })}
|
onChange={(e) => setSchComp({ ...updateSchComp, checked: e.target.checked })}
|
||||||
|
id="update_scheduled_completion"
|
||||||
>
|
>
|
||||||
{t("jobs.labels.update_scheduled_completion")}
|
{t("jobs.labels.update_scheduled_completion")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
@@ -261,6 +263,7 @@ export default function JobsFindModalComponent({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSchComp({ ...updateSchComp, scheduled_completion: e });
|
setSchComp({ ...updateSchComp, scheduled_completion: e });
|
||||||
}}
|
}}
|
||||||
|
id="scheduled_completion_date_time_picker"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -273,6 +276,7 @@ export default function JobsFindModalComponent({
|
|||||||
automatic: true
|
automatic: true
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
id="calculate_scheduled_completion"
|
||||||
>
|
>
|
||||||
{t("jobs.labels.calc_scheuled_completion")}
|
{t("jobs.labels.calc_scheuled_completion")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { Modal } from "antd";
|
import { Modal } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -65,8 +64,8 @@ export default connect(
|
|||||||
<Modal
|
<Modal
|
||||||
title={t("jobs.labels.existing_jobs")}
|
title={t("jobs.labels.existing_jobs")}
|
||||||
width={"80%"}
|
width={"80%"}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
okButtonProps={{ disabled: selectedJob ? false : true }}
|
okButtonProps={{ disabled: selectedJob ? false : true, id: "jobs-find-modal-container-ok" }}
|
||||||
{...modalProps}
|
{...modalProps}
|
||||||
>
|
>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button, Card, Input, Space, Table, Typography } from "antd";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -20,7 +20,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,6 +203,8 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
id="all-jobs-list"
|
||||||
|
title={t("titles.bc.jobs-all")}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{search.search && (
|
{search.search && (
|
||||||
@@ -256,6 +258,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
|||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={search?.search ? openSearchResults : jobs}
|
dataSource={search?.search ? openSearchResults : jobs}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
id="all-jobs-list-table"
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, SyncOut
|
|||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";
|
import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -22,7 +22,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({});
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
export function JobsList({ bodyshop }) {
|
export function JobsList({ bodyshop }) {
|
||||||
const searchParams = queryString.parse(useLocation().search);
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
@@ -342,13 +342,14 @@ export function JobsList({ bodyshop }) {
|
|||||||
type: "radio"
|
type: "radio"
|
||||||
}}
|
}}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
onRow={(record, rowIndex) => {
|
onRow={(record) => {
|
||||||
return {
|
return {
|
||||||
onClick: (event) => {
|
onClick: () => {
|
||||||
handleOnRowClick(record);
|
handleOnRowClick(record);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
|
id="active-jobs-list-table"
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import useLocalStorage from "../../utils/useLocalStorage";
|
|||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
|
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
|
||||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
|
import JobNotesPinToggle from "../job-notes-pin-toggle/job-notes-pin-toggle.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly
|
jobRO: selectJobReadOnly
|
||||||
@@ -47,6 +48,9 @@ export function JobNotesComponent({
|
|||||||
key: "icons",
|
key: "icons",
|
||||||
width: 80,
|
width: 80,
|
||||||
filteredValue: filter?.icons || null,
|
filteredValue: filter?.icons || null,
|
||||||
|
defaultSortOrder: "desc",
|
||||||
|
multiple: 1,
|
||||||
|
sorter: (a, b) => a.pinned - b.pinned,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
text: t("notes.labels.usernotes"),
|
text: t("notes.labels.usernotes"),
|
||||||
@@ -63,6 +67,7 @@ export function JobNotesComponent({
|
|||||||
{record.critical ? <WarningFilled style={{ margin: 4, color: "red" }} /> : null}
|
{record.critical ? <WarningFilled style={{ margin: 4, color: "red" }} /> : null}
|
||||||
{record.private ? <EyeInvisibleFilled style={{ margin: 4 }} /> : null}
|
{record.private ? <EyeInvisibleFilled style={{ margin: 4 }} /> : null}
|
||||||
{record.audit ? <AuditOutlined style={{ margin: 4 }} /> : null}
|
{record.audit ? <AuditOutlined style={{ margin: 4 }} /> : null}
|
||||||
|
<JobNotesPinToggle note={record} />
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -100,6 +105,7 @@ export function JobNotesComponent({
|
|||||||
dataIndex: "updated_at",
|
dataIndex: "updated_at",
|
||||||
key: "updated_at",
|
key: "updated_at",
|
||||||
defaultSortOrder: "descend",
|
defaultSortOrder: "descend",
|
||||||
|
multiple: 2,
|
||||||
width: 200,
|
width: 200,
|
||||||
sorter: (a, b) => new Date(a.updated_at) - new Date(b.updated_at),
|
sorter: (a, b) => new Date(a.updated_at) - new Date(b.updated_at),
|
||||||
render: (text, record) => <DateTimeFormatter>{record.updated_at}</DateTimeFormatter>
|
render: (text, record) => <DateTimeFormatter>{record.updated_at}</DateTimeFormatter>
|
||||||
|
|||||||
@@ -23,17 +23,22 @@ export function NoteUpsertModalComponent({ form, noteUpsertModal }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={8}>
|
<Col span={6}>
|
||||||
<Form.Item label={t("notes.fields.critical")} name="critical" valuePropName="checked">
|
<Form.Item label={t("notes.fields.critical")} name="critical" valuePropName="checked">
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={6}>
|
||||||
<Form.Item label={t("notes.fields.private")} name="private" valuePropName="checked">
|
<Form.Item label={t("notes.fields.private")} name="private" valuePropName="checked">
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={6}>
|
||||||
|
<Form.Item label={t("notes.fields.pinned")} name="pinned" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
<Form.Item label={t("notes.fields.type")} name="type" initialValue="general">
|
<Form.Item label={t("notes.fields.type")} name="type" initialValue="general">
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={[
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Form, Modal } from "antd";
|
import { Form, Modal } from "antd";
|
||||||
import React, { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries.js";
|
||||||
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
|
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
@@ -12,7 +14,6 @@ import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
|
|||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import NoteUpsertModalComponent from "./note-upsert-modal.component";
|
import NoteUpsertModalComponent from "./note-upsert-modal.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
@@ -20,7 +21,14 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
|
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
|
||||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleModalVisible, insertAuditTrail }) {
|
export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleModalVisible, insertAuditTrail }) {
|
||||||
@@ -34,7 +42,7 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
|
|||||||
const { refetch } = actions;
|
const { refetch } = actions;
|
||||||
|
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//Required to prevent infinite looping.
|
//Required to prevent infinite looping.
|
||||||
if (existingNote && open) {
|
if (existingNote && open) {
|
||||||
@@ -58,8 +66,9 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
|
|||||||
variables: {
|
variables: {
|
||||||
noteId: existingNote.id,
|
noteId: existingNote.id,
|
||||||
note: values
|
note: values
|
||||||
}
|
},
|
||||||
}).then((r) => {
|
refetchQueries: ["GET_JOB_BY_PK", "QUERY_JOB_CARD_DETAILS", "QUERY_PARTS_QUEUE_CARD_DETAILS"]
|
||||||
|
}).then(() => {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("notes.successes.updated")
|
message: t("notes.successes.updated")
|
||||||
});
|
});
|
||||||
@@ -79,6 +88,33 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
|
|||||||
variables: {
|
variables: {
|
||||||
noteInput: [{ ...values, jobid: jobId, created_by: currentUser.email }]
|
noteInput: [{ ...values, jobid: jobId, created_by: currentUser.email }]
|
||||||
},
|
},
|
||||||
|
update(cache, { data: { updateNote: updatedNote } }) {
|
||||||
|
try {
|
||||||
|
const existingJob = cache.readQuery({
|
||||||
|
query: GET_JOB_BY_PK,
|
||||||
|
variables: { id: jobId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingJob) {
|
||||||
|
cache.writeQuery({
|
||||||
|
query: GET_JOB_BY_PK,
|
||||||
|
variables: { id: jobId },
|
||||||
|
data: {
|
||||||
|
...existingJob,
|
||||||
|
job: {
|
||||||
|
...existingJob.job,
|
||||||
|
notes: updatedNote.pinned
|
||||||
|
? [updatedNote, ...existingJob.job.notes]
|
||||||
|
: existingJob.job.notes.filter((n) => n.id !== updatedNote.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Cache miss is okay, query hasn't been executed yet
|
||||||
|
console.log("Cache miss for GET_JOB_BY_PK");
|
||||||
|
}
|
||||||
|
},
|
||||||
refetchQueries: ["QUERY_NOTES_BY_JOB_PK"]
|
refetchQueries: ["QUERY_NOTES_BY_JOB_PK"]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,7 +159,7 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||||
<NoteUpsertModalComponent form={form} />
|
<NoteUpsertModalComponent form={form} />
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
|
import { Alert, Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
|
||||||
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
|
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import "./notification-center.styles.scss";
|
import "./notification-center.styles.scss";
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
import { forwardRef, useRef, useEffect } from "react";
|
import { forwardRef, useEffect, useRef } from "react";
|
||||||
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
@@ -26,7 +26,8 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
markAllRead,
|
markAllRead,
|
||||||
loadMore,
|
loadMore,
|
||||||
onNotificationClick,
|
onNotificationClick,
|
||||||
unreadCount
|
unreadCount,
|
||||||
|
isEmployee
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
@@ -93,7 +94,12 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
) : (
|
) : (
|
||||||
<EyeOutlined className="notification-toggle-icon" />
|
<EyeOutlined className="notification-toggle-icon" />
|
||||||
)}
|
)}
|
||||||
<Switch checked={showUnreadOnly} onChange={(checked) => toggleUnreadOnly(checked)} size="small" />
|
<Switch
|
||||||
|
checked={showUnreadOnly}
|
||||||
|
onChange={(checked) => toggleUnreadOnly(checked)}
|
||||||
|
size="small"
|
||||||
|
disabled={!isEmployee}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
||||||
@@ -106,17 +112,25 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Virtuoso
|
{!isEmployee ? (
|
||||||
ref={virtuosoRef}
|
<div style={{ padding: 10 }}>
|
||||||
style={{ height: "400px", width: "100%" }}
|
<Alert message={t("notifications.labels.employee-notification")} type="warning" />
|
||||||
data={notifications}
|
</div>
|
||||||
totalCount={notifications.length}
|
) : (
|
||||||
endReached={loadMore}
|
<Virtuoso
|
||||||
itemContent={renderNotification}
|
ref={virtuosoRef}
|
||||||
/>
|
style={{ height: "400px", width: "100%" }}
|
||||||
|
data={notifications}
|
||||||
|
totalCount={notifications.length}
|
||||||
|
endReached={loadMore}
|
||||||
|
itemContent={renderNotification}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
NotificationCenterComponent.displayName = "NotificationCenterComponent";
|
||||||
|
|
||||||
export default NotificationCenterComponent;
|
export default NotificationCenterComponent;
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { connect } from "react-redux";
|
|||||||
import NotificationCenterComponent from "./notification-center.component";
|
import NotificationCenterComponent from "./notification-center.component";
|
||||||
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
// This will be used to poll for notifications when the socket is disconnected
|
// This will be used to poll for notifications when the socket is disconnected
|
||||||
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
||||||
@@ -17,17 +18,18 @@ const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
|||||||
* @param onClose
|
* @param onClose
|
||||||
* @param bodyshop
|
* @param bodyshop
|
||||||
* @param unreadCount
|
* @param unreadCount
|
||||||
|
* @param currentUser
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
|
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount, currentUser }) => {
|
||||||
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
||||||
const notificationRef = useRef(null);
|
const notificationRef = useRef(null);
|
||||||
|
|
||||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||||
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
|
||||||
const baseWhereClause = useMemo(() => {
|
const baseWhereClause = useMemo(() => {
|
||||||
return { associationid: { _eq: userAssociationId } };
|
return { associationid: { _eq: userAssociationId } };
|
||||||
@@ -51,7 +53,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
fetchPolicy: "cache-and-network",
|
fetchPolicy: "cache-and-network",
|
||||||
notifyOnNetworkStatusChange: true,
|
notifyOnNetworkStatusChange: true,
|
||||||
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
||||||
skip: !userAssociationId,
|
skip: !userAssociationId || !isEmployee,
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error(`Error polling Notifications: ${err?.message || ""}`);
|
console.error(`Error polling Notifications: ${err?.message || ""}`);
|
||||||
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
||||||
@@ -71,7 +73,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
}, [visible, onClose]);
|
}, [visible, onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.notifications) {
|
if (data?.notifications && isEmployee) {
|
||||||
const processedNotifications = data.notifications
|
const processedNotifications = data.notifications
|
||||||
.map((notif) => {
|
.map((notif) => {
|
||||||
let scenarioText;
|
let scenarioText;
|
||||||
@@ -101,11 +103,13 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
})
|
})
|
||||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||||
setNotifications(processedNotifications);
|
setNotifications(processedNotifications);
|
||||||
|
} else if (!isEmployee) {
|
||||||
|
setNotifications([]); // Clear notifications if not an employee
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data, isEmployee]);
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
if (!queryLoading && data?.notifications.length) {
|
if (!queryLoading && data?.notifications.length && isEmployee) {
|
||||||
setIsLoading(true); // Show spinner during fetchMore
|
setIsLoading(true); // Show spinner during fetchMore
|
||||||
fetchMore({
|
fetchMore({
|
||||||
variables: { offset: data.notifications.length, where: whereClause },
|
variables: { offset: data.notifications.length, where: whereClause },
|
||||||
@@ -121,13 +125,14 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false)); // Hide spinner when done
|
.finally(() => setIsLoading(false)); // Hide spinner when done
|
||||||
}
|
}
|
||||||
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause]);
|
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause, isEmployee]);
|
||||||
|
|
||||||
const handleToggleUnreadOnly = (value) => {
|
const handleToggleUnreadOnly = (value) => {
|
||||||
setShowUnreadOnly(value);
|
setShowUnreadOnly(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkAllRead = useCallback(() => {
|
const handleMarkAllRead = useCallback(() => {
|
||||||
|
if (!isEmployee) return; // Do nothing if not an employee
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
markAllNotificationsRead()
|
markAllNotificationsRead()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -147,7 +152,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
})
|
})
|
||||||
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
|
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly]);
|
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly, isEmployee]);
|
||||||
|
|
||||||
const handleNotificationClick = useCallback(
|
const handleNotificationClick = useCallback(
|
||||||
(notificationId) => {
|
(notificationId) => {
|
||||||
@@ -170,17 +175,18 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && !isConnected) {
|
if (visible && !isConnected && isEmployee) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
refetch()
|
refetch()
|
||||||
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
|
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}
|
}
|
||||||
}, [visible, isConnected, refetch]);
|
}, [visible, isConnected, refetch, isEmployee]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationCenterComponent
|
<NotificationCenterComponent
|
||||||
ref={notificationRef}
|
ref={notificationRef}
|
||||||
|
isEmployee={isEmployee}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
notifications={notifications}
|
notifications={notifications}
|
||||||
@@ -196,7 +202,8 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(NotificationCenterContainer);
|
export default connect(mapStateToProps, null)(NotificationCenterContainer);
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
background: #fff;
|
background: var(--notification-bg);
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: var(--notification-text);
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid var(--notification-border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), 0 3px 6px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), 0 3px 6px rgba(0, 0, 0, 0.06);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
@@ -19,23 +19,22 @@
|
|||||||
|
|
||||||
.notification-header {
|
.notification-header {
|
||||||
padding: 4px 16px;
|
padding: 4px 16px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid var(--notification-header-border);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #fafafa;
|
background: var(--notification-header-bg);
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: var(--notification-header-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-controls {
|
.notification-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
// Styles for the eye icon and switch (custom classes)
|
// Styles for the eye icon and switch (custom classes)
|
||||||
.notification-toggle {
|
.notification-toggle {
|
||||||
align-items: center; // Ensure vertical alignment
|
align-items: center; // Ensure vertical alignment
|
||||||
@@ -43,7 +42,7 @@
|
|||||||
|
|
||||||
.notification-toggle-icon {
|
.notification-toggle-icon {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #1677ff;
|
color: var(--notification-toggle-icon);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +58,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.ant-switch-checked {
|
&.ant-switch-checked {
|
||||||
background-color: #1677ff;
|
background-color: var(--notification-switch-bg);
|
||||||
|
|
||||||
.ant-switch-handle {
|
.ant-switch-handle {
|
||||||
left: calc(100% - 14px);
|
left: calc(100% - 14px);
|
||||||
}
|
}
|
||||||
@@ -70,37 +70,37 @@
|
|||||||
// Styles for the "Mark All Read" button (restore original link button style)
|
// Styles for the "Mark All Read" button (restore original link button style)
|
||||||
.ant-btn-link {
|
.ant-btn-link {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #1677ff;
|
color: var(--notification-btn-link);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #69b1ff;
|
color: var(--notification-btn-link-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
color: rgba(0, 0, 0, 0.25);
|
color: var(--notification-btn-link-disabled);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: #0958d9;
|
color: var(--notification-btn-link-active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-read {
|
.notification-read {
|
||||||
background: #fff;
|
background: var(--notification-read-bg);
|
||||||
color: rgba(0, 0, 0, 0.65);
|
color: var(--notification-read-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-unread {
|
.notification-unread {
|
||||||
background: #f5f5f5;
|
background: var(--notification-unread-bg);
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: var(--notification-unread-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-item {
|
.notification-item {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid var(--notification-header-border);
|
||||||
display: block;
|
display: block;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #fafafa;
|
background: var(--notification-item-hover-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-content {
|
.notification-content {
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
|
|
||||||
.ro-number {
|
.ro-number {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #1677ff;
|
color: var(--notification-ro-number);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
.relative-time {
|
.relative-time {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(0, 0, 0, 0.45);
|
color: var(--notification-relative-time);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@@ -164,12 +164,12 @@
|
|||||||
|
|
||||||
.ant-alert {
|
.ant-alert {
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
background: #fff1f0;
|
background: var(--alert-bg);
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: var(--alert-text);
|
||||||
border: 1px solid #ffa39e;
|
border: 1px solid var(--alert-border);
|
||||||
|
|
||||||
.ant-alert-message {
|
.ant-alert-message {
|
||||||
color: #ff4d4f;
|
color: var(--alert-message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,41 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button, Card, Checkbox, Form, Space, Table } from "antd";
|
import { Alert, Button, Card, Checkbox, Divider, Form, Space, Switch, Table, Typography } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import { QUERY_NOTIFICATION_SETTINGS, UPDATE_NOTIFICATION_SETTINGS } from "../../graphql/user.queries.js";
|
import {
|
||||||
|
QUERY_NOTIFICATION_SETTINGS,
|
||||||
|
UPDATE_NOTIFICATION_SETTINGS,
|
||||||
|
UPDATE_NOTIFICATIONS_AUTOADD
|
||||||
|
} from "../../graphql/user.queries.js";
|
||||||
import { notificationScenarios } from "../../utils/jobNotificationScenarios.js";
|
import { notificationScenarios } from "../../utils/jobNotificationScenarios.js";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
|
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifications Settings Form
|
* Notifications Settings Form
|
||||||
* @param currentUser
|
* @param currentUser
|
||||||
|
* @param bodyshop
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const NotificationSettingsForm = ({ currentUser }) => {
|
const NotificationSettingsForm = ({ currentUser, bodyshop }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [initialValues, setInitialValues] = useState({});
|
const [initialValues, setInitialValues] = useState({});
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
|
const [autoAddEnabled, setAutoAddEnabled] = useState(false);
|
||||||
|
const [initialAutoAdd, setInitialAutoAdd] = useState(false);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
|
||||||
// Fetch notification settings.
|
// Fetch notification settings and notifications_autoadd
|
||||||
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
@@ -34,13 +43,16 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
skip: !currentUser
|
skip: !currentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateNotificationSettings, { loading: saving }] = useMutation(UPDATE_NOTIFICATION_SETTINGS);
|
const [updateNotificationSettings, { loading: savingSettings }] = useMutation(UPDATE_NOTIFICATION_SETTINGS);
|
||||||
|
const [updateNotificationsAutoAdd, { loading: savingAutoAdd }] = useMutation(UPDATE_NOTIFICATIONS_AUTOADD);
|
||||||
|
|
||||||
// Populate form with fetched data.
|
// Populate form with fetched data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.associations?.length > 0) {
|
if (data?.associations?.length > 0) {
|
||||||
const settings = data.associations[0].notification_settings || {};
|
const settings = data.associations[0].notification_settings || {};
|
||||||
// Ensure each scenario has an object with { app, email, fcm }.
|
const autoAdd = data.associations[0].notifications_autoadd ?? false;
|
||||||
|
|
||||||
|
// Ensure each scenario has an object with { app, email, fcm }
|
||||||
const formattedValues = notificationScenarios.reduce((acc, scenario) => {
|
const formattedValues = notificationScenarios.reduce((acc, scenario) => {
|
||||||
acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false };
|
acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false };
|
||||||
return acc;
|
return acc;
|
||||||
@@ -48,32 +60,66 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
|
|
||||||
setInitialValues(formattedValues);
|
setInitialValues(formattedValues);
|
||||||
form.setFieldsValue(formattedValues);
|
form.setFieldsValue(formattedValues);
|
||||||
setIsDirty(false); // Reset dirty state when new data loads.
|
setAutoAddEnabled(autoAdd);
|
||||||
|
setInitialAutoAdd(autoAdd);
|
||||||
|
setIsDirty(false); // Reset dirty state when new data loads
|
||||||
}
|
}
|
||||||
}, [data, form]);
|
}, [data, form]);
|
||||||
|
|
||||||
|
// Handle toggle of notifications_autoadd
|
||||||
|
const handleAutoAddToggle = async (checked) => {
|
||||||
|
if (data?.associations?.length > 0) {
|
||||||
|
const userId = data.associations[0].id;
|
||||||
|
try {
|
||||||
|
const result = await updateNotificationsAutoAdd({
|
||||||
|
variables: { id: userId, autoadd: checked }
|
||||||
|
});
|
||||||
|
if (!result?.errors) {
|
||||||
|
setAutoAddEnabled(checked);
|
||||||
|
setInitialAutoAdd(checked);
|
||||||
|
notification.success({ message: t("notifications.labels.auto-add-success") });
|
||||||
|
setIsDirty(false); // Reset dirty state if only auto-add was changed
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to update auto-add setting");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setAutoAddEnabled(!checked); // Revert on error
|
||||||
|
notification.error({ message: t("notifications.labels.auto-add-failure") });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle save of notification settings
|
||||||
const handleSave = async (values) => {
|
const handleSave = async (values) => {
|
||||||
if (data?.associations?.length > 0) {
|
if (data?.associations?.length > 0) {
|
||||||
const userId = data.associations[0].id;
|
const userId = data.associations[0].id;
|
||||||
// Save the updated notification settings.
|
try {
|
||||||
const result = await updateNotificationSettings({ variables: { id: userId, ns: values } });
|
const result = await updateNotificationSettings({ variables: { id: userId, ns: values } });
|
||||||
if (!result?.errors) {
|
if (!result?.errors) {
|
||||||
notification.success({ message: t("notifications.labels.notification-settings-success") });
|
notification.success({ message: t("notifications.labels.notification-settings-success") });
|
||||||
setInitialValues(values);
|
setInitialValues(values);
|
||||||
setIsDirty(false);
|
setIsDirty(false);
|
||||||
} else {
|
} else {
|
||||||
|
throw new Error("Failed to update notification settings");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
notification.error({ message: t("notifications.labels.notification-settings-failure") });
|
notification.error({ message: t("notifications.labels.notification-settings-failure") });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mark the form as dirty on any manual change.
|
// Mark the form as dirty on any manual change
|
||||||
const handleFormChange = () => {
|
const handleFormChange = () => {
|
||||||
setIsDirty(true);
|
setIsDirty(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if auto-add has changed
|
||||||
|
const isAutoAddDirty = autoAddEnabled !== initialAutoAdd;
|
||||||
|
|
||||||
|
// Handle reset of form and auto-add
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
form.setFieldsValue(initialValues);
|
form.setFieldsValue(initialValues);
|
||||||
|
setAutoAddEnabled(initialAutoAdd);
|
||||||
setIsDirty(false);
|
setIsDirty(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,17 +185,30 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
title={t("notifications.labels.notificationscenarios")}
|
title={t("notifications.labels.notificationscenarios")}
|
||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="default" onClick={handleReset} disabled={!isDirty}>
|
<Typography.Text type="secondary">{t("notifications.labels.auto-add")}</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
checked={autoAddEnabled}
|
||||||
|
onChange={handleAutoAddToggle}
|
||||||
|
loading={savingAutoAdd}
|
||||||
|
// checkedChildren={t("notifications.labels.auto-add-on")}
|
||||||
|
// unCheckedChildren={t("notifications.labels.auto-add-off")}
|
||||||
|
/>
|
||||||
|
<Button type="default" onClick={handleReset} disabled={!isDirty && !isAutoAddDirty}>
|
||||||
{t("general.actions.clear")}
|
{t("general.actions.clear")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="primary" htmlType="submit" disabled={!isDirty} loading={savingSettings}>
|
||||||
<Button type="primary" htmlType="submit" disabled={!isDirty} loading={saving}>
|
|
||||||
{t("notifications.labels.save")}
|
{t("notifications.labels.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{!isEmployee && (
|
||||||
|
<div style={{ width: "100%", marginBottom: "10px" }}>
|
||||||
|
<Alert message={t("notifications.labels.employee-notification")} type="warning" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
|
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
|
||||||
|
<Divider />
|
||||||
</Card>
|
</Card>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
@@ -158,11 +217,13 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
NotificationSettingsForm.propTypes = {
|
NotificationSettingsForm.propTypes = {
|
||||||
currentUser: PropTypes.shape({
|
currentUser: PropTypes.shape({
|
||||||
email: PropTypes.string.isRequired
|
email: PropTypes.string.isRequired
|
||||||
}).isRequired
|
}).isRequired,
|
||||||
|
bodyshop: PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(NotificationSettingsForm);
|
export default connect(mapStateToProps)(NotificationSettingsForm);
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Form, Input, Switch } from "antd";
|
import { Form, Input, Tooltip } from "antd";
|
||||||
import React from "react";
|
import { CloseCircleFilled } from "@ant-design/icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
||||||
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
import { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||||
|
|
||||||
export default function OwnerDetailFormComponent({ form, loading }) {
|
export default function OwnerDetailFormComponent({ form, loading, isPhone1OptedOut, isPhone2OptedOut }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getFieldValue } = form;
|
const { getFieldValue } = form;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormFieldsChanged form={form} />
|
<FormFieldsChanged form={form} />
|
||||||
@@ -26,7 +27,7 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("owners.fields.accountingid")} name="accountingid">
|
<Form.Item label={t("owners.fields.accountingid")} name="accountingid">
|
||||||
<Input disabled/>
|
<Input disabled />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("owners.forms.address")}>
|
<LayoutFormRow header={t("owners.forms.address")}>
|
||||||
@@ -50,9 +51,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("owners.forms.contact")}>
|
<LayoutFormRow header={t("owners.forms.contact")}>
|
||||||
<Form.Item label={t("owners.fields.allow_text_message")} name="allow_text_message" valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("owners.fields.ownr_ea")}
|
label={t("owners.fields.ownr_ea")}
|
||||||
name="ownr_ea"
|
name="ownr_ea"
|
||||||
@@ -65,19 +63,55 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
>
|
>
|
||||||
<FormItemEmail email={getFieldValue("ownr_ea")} />
|
<FormItemEmail email={getFieldValue("ownr_ea")} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("owners.fields.ownr_ph1")} style={{ marginBottom: 0 }}>
|
||||||
label={t("owners.fields.ownr_ph1")}
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
name="ownr_ph1"
|
<Form.Item
|
||||||
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]}
|
name="ownr_ph1"
|
||||||
>
|
noStyle
|
||||||
<FormItemPhone />
|
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]}
|
||||||
|
>
|
||||||
|
<Input style={{ flex: 1, minWidth: "150px" }} />
|
||||||
|
</Form.Item>
|
||||||
|
{isPhone1OptedOut && (
|
||||||
|
<Tooltip title={t("consent.text_body")}>
|
||||||
|
<CloseCircleFilled
|
||||||
|
style={{
|
||||||
|
color: "#ff4d4f",
|
||||||
|
fontSize: 16,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: "100%"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("owners.fields.ownr_ph2")} style={{ marginBottom: 0 }}>
|
||||||
label={t("owners.fields.ownr_ph2")}
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
name="ownr_ph2"
|
<Form.Item
|
||||||
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]}
|
name="ownr_ph2"
|
||||||
>
|
noStyle
|
||||||
<FormItemPhone />
|
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]}
|
||||||
|
>
|
||||||
|
<Input style={{ flex: 1, minWidth: "150px" }} />
|
||||||
|
</Form.Item>
|
||||||
|
{isPhone2OptedOut && (
|
||||||
|
<Tooltip title={t("consent.text_body")}>
|
||||||
|
<CloseCircleFilled
|
||||||
|
style={{
|
||||||
|
color: "#ff4d4f",
|
||||||
|
fontSize: 16,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: "100%"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("owners.fields.preferred_contact")} name="preferred_contact">
|
<Form.Item label={t("owners.fields.preferred_contact")} name="preferred_contact">
|
||||||
<Input />
|
<Input />
|
||||||
|
|||||||
@@ -1,69 +1,115 @@
|
|||||||
import { Button, Form, Popconfirm } from "antd";
|
import { Button, Form, Popconfirm } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useApolloClient, useMutation } from "@apollo/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries";
|
import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors"; // Adjust path
|
||||||
|
import { phoneNumberOptOutService } from "../../utils/phoneOptOutService.js"; // Adjust path
|
||||||
import OwnerDetailFormComponent from "./owner-detail-form.component";
|
import OwnerDetailFormComponent from "./owner-detail-form.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { phone } from "phone"; // Import phone utility for formatting
|
||||||
|
|
||||||
function OwnerDetailFormContainer({ owner, refetch }) {
|
// Connect to Redux to access bodyshop
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
function OwnerDetailFormContainer({ owner, refetch, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const history = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [optedOutPhones, setOptedOutPhones] = useState(new Set());
|
||||||
const [updateOwner] = useMutation(UPDATE_OWNER);
|
const [updateOwner] = useMutation(UPDATE_OWNER);
|
||||||
const [deleteOwner] = useMutation(DELETE_OWNER);
|
const [deleteOwner] = useMutation(DELETE_OWNER);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
|
// Fetch opt-out status on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchOptOutStatus = async () => {
|
||||||
|
if (bodyshop?.id && bodyshop?.messagingservicesid && (owner?.ownr_ph1 || owner?.ownr_ph2)) {
|
||||||
|
const phoneNumbers = [owner.ownr_ph1, owner.ownr_ph2].filter(Boolean);
|
||||||
|
const optOutSet = await phoneNumberOptOutService(apolloClient, bodyshop.id, phoneNumbers);
|
||||||
|
setOptedOutPhones(optOutSet);
|
||||||
|
} else {
|
||||||
|
setOptedOutPhones(new Set());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchOptOutStatus();
|
||||||
|
}, [apolloClient, bodyshop?.id, bodyshop?.messagingservicesid, owner?.ownr_ph1, owner?.ownr_ph2]);
|
||||||
|
|
||||||
|
// Reset form fields when owner changes
|
||||||
|
useEffect(() => {
|
||||||
|
form.setFieldsValue({
|
||||||
|
ownr_ph1: owner?.ownr_ph1,
|
||||||
|
ownr_ph2: owner?.ownr_ph2,
|
||||||
|
...owner
|
||||||
|
});
|
||||||
|
}, [owner, form]);
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await deleteOwner({
|
try {
|
||||||
variables: { id: owner.id }
|
const result = await deleteOwner({
|
||||||
});
|
variables: { id: owner.id }
|
||||||
console.log(result);
|
});
|
||||||
if (result.errors) {
|
if (result.errors) {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
|
message: t("owners.errors.deleting", {
|
||||||
|
error: JSON.stringify(result.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification.success({
|
||||||
|
message: t("owners.successes.delete")
|
||||||
|
});
|
||||||
|
navigate(`/manage/owners`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
message: t("owners.errors.deleting", {
|
message: t("owners.errors.deleting", {
|
||||||
error: JSON.stringify(result.errors)
|
error: error.message
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else {
|
|
||||||
notification["success"]({
|
|
||||||
message: t("owners.successes.delete")
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
history(`/manage/owners`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await updateOwner({
|
try {
|
||||||
variables: { ownerId: owner.id, owner: values }
|
const result = await updateOwner({
|
||||||
});
|
variables: { ownerId: owner.id, owner: values }
|
||||||
|
});
|
||||||
if (!!result.errors) {
|
if (result.errors) {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
|
message: t("owners.errors.saving", {
|
||||||
|
error: JSON.stringify(result.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification.success({
|
||||||
|
message: t("owners.successes.save")
|
||||||
|
});
|
||||||
|
if (refetch) await refetch();
|
||||||
|
form.resetFields();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
message: t("owners.errors.saving", {
|
message: t("owners.errors.saving", {
|
||||||
error: JSON.stringify(result.errors)
|
error: error.message
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notification["success"]({
|
|
||||||
message: t("owners.successes.save")
|
|
||||||
});
|
|
||||||
|
|
||||||
if (refetch) await refetch();
|
|
||||||
form.resetFields();
|
|
||||||
form.resetFields();
|
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -72,6 +118,7 @@ function OwnerDetailFormContainer({ owner, refetch }) {
|
|||||||
title={t("menus.header.owners")}
|
title={t("menus.header.owners")}
|
||||||
extra={[
|
extra={[
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
|
key="delete"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
disabled={owner.jobs.length !== 0}
|
disabled={owner.jobs.length !== 0}
|
||||||
@@ -81,16 +128,29 @@ function OwnerDetailFormContainer({ owner, refetch }) {
|
|||||||
{t("general.actions.delete")}
|
{t("general.actions.delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
<Button type="primary" loading={loading} onClick={() => form.submit()}>
|
<Button key="save" type="primary" loading={loading} onClick={() => form.submit()}>
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Form form={form} onFinish={handleFinish} autoComplete="off" layout="vertical" initialValues={owner}>
|
<Form form={form} onFinish={handleFinish} autoComplete="off" layout="vertical" initialValues={owner}>
|
||||||
<OwnerDetailFormComponent loading={loading} form={form} />
|
<OwnerDetailFormComponent
|
||||||
|
loading={loading}
|
||||||
|
form={form}
|
||||||
|
isPhone1OptedOut={
|
||||||
|
bodyshop?.messagingservicesid &&
|
||||||
|
owner?.ownr_ph1 &&
|
||||||
|
optedOutPhones.has(phone(owner.ownr_ph1, "CA").phoneNumber?.replace(/^\+1/, ""))
|
||||||
|
}
|
||||||
|
isPhone2OptedOut={
|
||||||
|
bodyshop?.messagingservicesid &&
|
||||||
|
owner?.ownr_ph2 &&
|
||||||
|
optedOutPhones.has(phone(owner.ownr_ph2, "CA").phoneNumber?.replace(/^\+1/, ""))
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OwnerDetailFormContainer;
|
export default connect(mapStateToProps)(OwnerDetailFormContainer);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useLazyQuery } from "@apollo/client";
|
import { useLazyQuery } from "@apollo/client";
|
||||||
import { Input, Modal } from "antd";
|
import { Input, Modal } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { QUERY_SEARCH_OWNER_BY_IDX } from "../../graphql/owners.queries";
|
import { QUERY_SEARCH_OWNER_BY_IDX } from "../../graphql/owners.queries";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import OwnerFindModalComponent from "./owner-find-modal.component";
|
|
||||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
|
import OwnerFindModalComponent from "./owner-find-modal.component";
|
||||||
|
|
||||||
export default function OwnerFindModalContainer({
|
export default function OwnerFindModalContainer({
|
||||||
loading,
|
loading,
|
||||||
@@ -41,6 +41,7 @@ export default function OwnerFindModalContainer({
|
|||||||
<Modal
|
<Modal
|
||||||
title={<span id="owner-find-modal-title">{t("owners.labels.existing_owners")}</span>}
|
title={<span id="owner-find-modal-title">{t("owners.labels.existing_owners")}</span>}
|
||||||
width={"80%"}
|
width={"80%"}
|
||||||
|
okButtonProps={{ id: "owner-find-modal-ok-button" }}
|
||||||
{...modalProps}
|
{...modalProps}
|
||||||
>
|
>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function PartsOrderBackorderEta({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
|
<Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
|
||||||
<DateFormatter>{backordered_eta}</DateFormatter>
|
<DateFormatter>{backordered_eta}</DateFormatter>
|
||||||
{isAlreadyBackordered && <CalendarFilled style={{ cursor: "pointer" }} onClick={handlePopover} />}
|
{isAlreadyBackordered && <CalendarFilled style={{ cursor: "pointer" }} onClick={handlePopover} />}
|
||||||
{loading && <Spin />}
|
{loading && <Spin />}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user