Merge branch 'master-AIO' into feature/IO-3210-Podium-Datapump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -128,3 +128,5 @@ vitest-coverage/
|
|||||||
*.vitest.log
|
*.vitest.log
|
||||||
test-output.txt
|
test-output.txt
|
||||||
server/job/test/fixtures
|
server/job/test/fixtures
|
||||||
|
|
||||||
|
.github
|
||||||
|
|||||||
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>
|
||||||
|
|||||||
2712
client/package-lock.json
generated
2712
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.13",
|
||||||
"@firebase/app": "^0.11.4",
|
"@firebase/app": "^0.12.1",
|
||||||
"@firebase/auth": "^1.10.0",
|
"@firebase/auth": "^1.10.2",
|
||||||
"@firebase/firestore": "^4.7.10",
|
"@firebase/firestore": "^4.7.12",
|
||||||
"@firebase/messaging": "^0.12.17",
|
"@firebase/messaging": "^0.12.18",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.6.1",
|
"@reduxjs/toolkit": "^2.8.1",
|
||||||
"@sentry/cli": "^2.43.0",
|
"@sentry/cli": "^2.45.0",
|
||||||
"@sentry/react": "^9.11.0",
|
"@sentry/react": "^9.18.0",
|
||||||
"@sentry/vite-plugin": "^3.3.1",
|
"@sentry/vite-plugin": "^3.4.0",
|
||||||
"@splitsoftware/splitio-react": "^2.1.1",
|
"@splitsoftware/splitio-react": "^2.1.1",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"antd": "^5.24.6",
|
"antd": "^5.25.1",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^4.2.0",
|
"apollo-link-sentry": "^4.3.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -37,18 +37,18 @@
|
|||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.11.0",
|
||||||
"i18next": "^24.2.3",
|
"i18next": "^24.2.3",
|
||||||
"i18next-browser-languagedetector": "^8.0.4",
|
"i18next-browser-languagedetector": "^8.1.0",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.12.6",
|
"libphonenumber-js": "^1.12.8",
|
||||||
"logrocket": "^9.0.2",
|
"logrocket": "^9.0.2",
|
||||||
"markerjs2": "^2.32.4",
|
"markerjs2": "^2.32.4",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-url": "^8.0.1",
|
"normalize-url": "^8.0.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.1.1",
|
"query-string": "^9.1.2",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.18.0",
|
"react-big-calendar": "^1.18.0",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"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.4.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^6.30.0",
|
"react-router-dom": "^6.30.0",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtuoso": "^4.12.5",
|
"react-virtuoso": "^4.12.7",
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
@@ -77,9 +77,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.88.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.17",
|
"styled-components": "^6.1.18",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"use-memo-one": "^1.1.3",
|
"use-memo-one": "^1.1.3",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
@@ -129,18 +129,18 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^6.0.0",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.27.1",
|
||||||
"@dotenvx/dotenvx": "^1.39.1",
|
"@dotenvx/dotenvx": "^1.44.0",
|
||||||
"@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.26.0",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.51.1",
|
||||||
"@sentry/webpack-plugin": "^3.3.1",
|
"@sentry/webpack-plugin": "^3.4.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.3.4",
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.24.5",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
@@ -148,19 +148,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.1",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"playwright": "^1.51.1",
|
"playwright": "^1.51.1",
|
||||||
"react-error-overlay": "^6.1.0",
|
"react-error-overlay": "^6.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^6.2.5",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-babel": "^1.3.0",
|
"vite-plugin-babel": "^1.3.1",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
"vite-plugin-pwa": "^1.0.0",
|
"vite-plugin-pwa": "^1.0.0",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"vitest": "^3.1.1",
|
"vitest": "^3.1.3",
|
||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,21 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
setPartsOrderContext: (context) =>
|
||||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "partsOrder"
|
||||||
|
})
|
||||||
|
),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
|
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
|
||||||
@@ -69,7 +82,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, b
|
|||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
title={t("bills.actions.return")}
|
title={t("bills.actions.return")}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function BillDetailEditcontainer() {
|
|||||||
delete search.billid;
|
delete search.billid;
|
||||||
history({ search: queryString.stringify(search) });
|
history({ search: queryString.stringify(search) });
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
open={search.billid}
|
open={search.billid}
|
||||||
>
|
>
|
||||||
<BillDetailEditComponent />
|
<BillDetailEditComponent />
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisi
|
|||||||
title={t("payments.labels.findermodal")}
|
title={t("payments.labels.findermodal")}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
onOk={() => toggleModalVisible()}
|
onOk={() => toggleModalVisible()}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
forceRender
|
forceRender
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodys
|
|||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
width="80%"
|
width="80%"
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<CardPaymentModalComponent />
|
<CardPaymentModalComponent />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function ContractsFindModalContainer({
|
|||||||
title={t("contracts.labels.findermodal")}
|
title={t("contracts.labels.findermodal")}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
onOk={() => toggleModalVisible()}
|
onOk={() => toggleModalVisible()}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
forceRender
|
forceRender
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOv
|
|||||||
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
destroyOnClose={true}
|
destroyOnHidden
|
||||||
open={modalVisible}
|
open={modalVisible}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
width={"80%"}
|
width={"80%"}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
//To be used as a form element only.
|
//To be used as a form element only.
|
||||||
|
|
||||||
const EmployeeSearchSelect = ({ options, ...props }) => {
|
const EmployeeSearchSelect = ({ options, showEmail, ...props }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,12 +21,16 @@ const EmployeeSearchSelect = ({ options, ...props }) => {
|
|||||||
{options
|
{options
|
||||||
? options.map((o) => (
|
? options.map((o) => (
|
||||||
<Option key={o.id} value={o.id} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
|
<Option key={o.id} value={o.id} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
|
||||||
<Space>
|
<Space size="small">
|
||||||
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
{`${o.employee_number ?? ""} ${o.first_name} ${o.last_name}`}
|
||||||
|
<Tag color="green" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||||
<Tag color="green">
|
|
||||||
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
{showEmail && o.user_email ? (
|
||||||
|
<Tag color="blue" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||||
|
{o.user_email}
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
</Option>
|
</Option>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
|||||||
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
||||||
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
// Redux mappings
|
// Redux mappings
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -98,6 +99,7 @@ function Header({
|
|||||||
const baseTitleRef = useRef(document.title || "");
|
const baseTitleRef = useRef(document.title || "");
|
||||||
const lastSetTitleRef = useRef("");
|
const lastSetTitleRef = useRef("");
|
||||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||||
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: unreadData,
|
data: unreadData,
|
||||||
@@ -682,7 +684,7 @@ function Header({
|
|||||||
icon: unreadLoading ? (
|
icon: unreadLoading ? (
|
||||||
<Spin size="small" />
|
<Spin size="small" />
|
||||||
) : (
|
) : (
|
||||||
<Badge offset={[8, 0]} size="small" count={unreadCount}>
|
<Badge offset={[8, 0]} size="small" count={isEmployee ? unreadCount : 0}>
|
||||||
<BellFilled />
|
<BellFilled />
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export function InventoryUpsertModalContainer({ currentUser, bodyshop, inventory
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||||
<InventoryUpsertModal form={form} />
|
<InventoryUpsertModal form={form} />
|
||||||
|
|||||||
@@ -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,24 +8,30 @@ 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
|
||||||
@@ -33,7 +39,8 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
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 +50,42 @@ 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>
|
||||||
@@ -89,6 +122,74 @@ export function ScheduleEventComponent({
|
|||||||
</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 ? (
|
||||||
@@ -294,7 +395,7 @@ export function ScheduleEventComponent({
|
|||||||
) : (
|
) : (
|
||||||
<ScheduleManualEvent event={event} />
|
<ScheduleManualEvent event={event} />
|
||||||
)}
|
)}
|
||||||
{event.isintake ? (
|
{event.isintake && HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
||||||
<Link
|
<Link
|
||||||
to={{
|
to={{
|
||||||
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
||||||
@@ -303,7 +404,23 @@ export function ScheduleEventComponent({
|
|||||||
>
|
>
|
||||||
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : (
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function JobCostingModalContainer({ jobCostingModal, toggleModalVisible }
|
|||||||
}}
|
}}
|
||||||
cancelButtonProps={{ style: { display: "none" } }}
|
cancelButtonProps={{ style: { display: "none" } }}
|
||||||
width="90%"
|
width="90%"
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
{!costingData ? (
|
{!costingData ? (
|
||||||
<LoadingSpinner loading={true} />
|
<LoadingSpinner loading={true} />
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
setPrintCenterContext: (context) =>
|
||||||
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "printCenter"
|
||||||
|
})
|
||||||
|
),
|
||||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
dispatch(
|
dispatch(
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
@@ -87,7 +93,7 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTra
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{data ? (
|
{data ? (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import FormRow from "../layout-form-row/layout-form-row.component";
|
import FormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -40,6 +41,20 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
<Form.Item label={t("jobs.fields.date_rentalresp")} name="date_rentalresp">
|
<Form.Item label={t("jobs.fields.date_rentalresp")} name="date_rentalresp">
|
||||||
<DateTimePicker disabled={jobRO} />
|
<DateTimePicker disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
|
||||||
|
<DateTimePicker
|
||||||
|
disabled={true}
|
||||||
|
value={job.estimate_sent_approval ? dayjs(job.estimate_sent_approval) : null}
|
||||||
|
placeholder={t("general.labels.na")}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.estimate_approved")} name="estimate_approved">
|
||||||
|
<DateTimePicker
|
||||||
|
disabled={true}
|
||||||
|
value={job.estimate_approved ? dayjs(job.estimate_approved) : null}
|
||||||
|
placeholder={t("general.labels.na")}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
|
|
||||||
<FormRow header={t("jobs.forms.scheddates")}>
|
<FormRow header={t("jobs.forms.scheddates")}>
|
||||||
@@ -76,21 +91,15 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
<DateTimePicker disabled={jobRO} />
|
<DateTimePicker disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item shouldUpdate>
|
<Form.Item shouldUpdate>
|
||||||
{() => {
|
{() => (
|
||||||
return (
|
<Form.Item
|
||||||
<Form.Item
|
label={t("jobs.fields.actual_completion")}
|
||||||
label={t("jobs.fields.actual_completion")}
|
name="actual_completion"
|
||||||
name="actual_completion"
|
rules={[{ required: jobInPostProduction }]}
|
||||||
rules={[
|
>
|
||||||
{
|
<DateTimePicker disabled={jobRO} />
|
||||||
required: jobInPostProduction
|
</Form.Item>
|
||||||
}
|
)}
|
||||||
]}
|
|
||||||
>
|
|
||||||
<DateTimePicker disabled={jobRO} />
|
|
||||||
</Form.Item>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.scheduled_delivery")} name="scheduled_delivery">
|
<Form.Item label={t("jobs.fields.scheduled_delivery")} name="scheduled_delivery">
|
||||||
<DateTimePicker disabled={jobRO} />
|
<DateTimePicker disabled={jobRO} />
|
||||||
@@ -103,15 +112,12 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
|
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true || jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true || jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
|
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true || jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale">
|
<Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true || jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
import { Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -188,6 +187,12 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
<Form.Item label={t("jobs.fields.tlos_ind")} name="tlos_ind" valuePropName="checked">
|
<Form.Item label={t("jobs.fields.tlos_ind")} name="tlos_ind" valuePropName="checked">
|
||||||
<Switch disabled={jobRO} />
|
<Switch disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.hit_and_run")} name="hit_and_run" valuePropName="checked">
|
||||||
|
<Switch disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.acv_amount")} name="acv_amount">
|
||||||
|
<CurrencyInput disabled={jobRO} min={0} />
|
||||||
|
</Form.Item>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...lossColDamage}>
|
<Col {...lossColDamage}>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
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 { Card, Checkbox, Col, Divider, Row, Space, Tag, Tooltip } 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";
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||||
import DataLabel from "../data-label/data-label.component";
|
import DataLabel from "../data-label/data-label.component";
|
||||||
@@ -21,7 +24,7 @@ import ProductionListColumnComment from "../production-list-columns/production-l
|
|||||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||||
import "./jobs-detail-header.styles.scss";
|
import "./jobs-detail-header.styles.scss";
|
||||||
import dayjs from "../../utils/day";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -29,41 +32,55 @@ 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"
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
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 }) {
|
||||||
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 {
|
||||||
|
await updateJob({
|
||||||
|
variables: {
|
||||||
|
jobId: job.id,
|
||||||
|
job: { [field]: value }
|
||||||
|
},
|
||||||
|
refetchQueries: ["GET_JOB_BY_PK"],
|
||||||
|
awaitRefetchQueries: true
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
message: t("jobs.errors.saving", { error: error.message })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
|
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
|
||||||
<Col {...colSpan}>
|
<Col {...colSpan}>
|
||||||
@@ -72,11 +89,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
<DataLabel label={t("jobs.fields.status")}>
|
<DataLabel label={t("jobs.fields.status")}>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{job.status}
|
{job.status}
|
||||||
{job.inproduction && (
|
{job.inproduction && <Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>}
|
||||||
<Tag color="#f50" key="production">
|
|
||||||
{t("jobs.labels.inproduction")}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||||
{job.iouparent && (
|
{job.iouparent && (
|
||||||
<Link to={`/manage/jobs/${job.iouparent}`}>
|
<Link to={`/manage/jobs/${job.iouparent}`}>
|
||||||
@@ -110,7 +123,6 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
<span style={{ margin: "0rem .5rem" }}>/</span>
|
<span style={{ margin: "0rem .5rem" }}>/</span>
|
||||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
|
|
||||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||||
{job.alt_transport}
|
{job.alt_transport}
|
||||||
<JobAltTransportChange job={job} />
|
<JobAltTransportChange job={job} />
|
||||||
@@ -127,11 +139,39 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
))}
|
))}
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
||||||
<ProductionListColumnProductionNote record={job} />
|
<ProductionListColumnProductionNote record={job} />
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.estimate_sent_approval")}>
|
||||||
|
<Space>
|
||||||
|
<Checkbox
|
||||||
|
checked={!!job.estimate_sent_approval}
|
||||||
|
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{job.estimate_sent_approval && (
|
||||||
|
<span style={{ color: "#888" }}>
|
||||||
|
<DateTimeFormatter>{job.estimate_sent_approval}</DateTimeFormatter>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Checkbox>
|
||||||
|
</Space>
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.estimate_approved")}>
|
||||||
|
<Space>
|
||||||
|
<Checkbox
|
||||||
|
checked={!!job.estimate_approved}
|
||||||
|
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{job.estimate_approved && (
|
||||||
|
<span style={{ color: "#888" }}>
|
||||||
|
<DateTimeFormatter>{job.estimate_approved}</DateTimeFormatter>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Checkbox>
|
||||||
|
</Space>
|
||||||
|
</DataLabel>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{job.special_coverage_policy && (
|
{job.special_coverage_policy && (
|
||||||
<Tag color="tomato">
|
<Tag color="tomato">
|
||||||
@@ -149,6 +189,14 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
</Space>
|
</Space>
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
|
{job.hit_and_run && (
|
||||||
|
<Tag color="green">
|
||||||
|
<Space>
|
||||||
|
<WarningFilled />
|
||||||
|
<span>{t("jobs.fields.hit_and_run")}</span>
|
||||||
|
</Space>
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
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 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";
|
||||||
@@ -28,7 +26,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({ bodyshop, 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,6 +44,7 @@ export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, i
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function standardMediaDownload(bufferData) {
|
function standardMediaDownload(bufferData) {
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||||
@@ -53,13 +52,14 @@ export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, i
|
|||||||
a.download = `${identifier || "documents"}.zip`;
|
a.download = `${identifier || "documents"}.zip`;
|
||||||
a.click();
|
a.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
logImEXEvent("jobs_documents_download");
|
logImEXEvent("jobs_documents_download");
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const zipUrl = await axios({
|
const zipUrl = await axios({
|
||||||
url: "/media/imgproxy/download",
|
url: "/media/imgproxy/download",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: { documentids: imagesToDownload.map((_) => _.id) }
|
data: { jobId, documentids: imagesToDownload.map((_) => _.id) }
|
||||||
});
|
});
|
||||||
|
|
||||||
const theDownloadedZip = await cleanAxios({
|
const theDownloadedZip = await cleanAxios({
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default connect(
|
|||||||
<Modal
|
<Modal
|
||||||
title={t("jobs.labels.existing_jobs")}
|
title={t("jobs.labels.existing_jobs")}
|
||||||
width={"80%"}
|
width={"80%"}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
okButtonProps={{ disabled: selectedJob ? false : true }}
|
okButtonProps={{ disabled: selectedJob ? false : true }}
|
||||||
{...modalProps}
|
{...modalProps}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,7 +20,14 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
|
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
|
||||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleModalVisible, insertAuditTrail }) {
|
export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleModalVisible, insertAuditTrail }) {
|
||||||
@@ -123,7 +130,7 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||||
<NoteUpsertModalComponent form={form} />
|
<NoteUpsertModalComponent form={form} />
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
|
import { Alert, Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
|
||||||
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
|
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import "./notification-center.styles.scss";
|
import "./notification-center.styles.scss";
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
import { forwardRef, useRef, useEffect } from "react";
|
import { forwardRef, useEffect, useRef } from "react";
|
||||||
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
@@ -26,7 +26,8 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
markAllRead,
|
markAllRead,
|
||||||
loadMore,
|
loadMore,
|
||||||
onNotificationClick,
|
onNotificationClick,
|
||||||
unreadCount
|
unreadCount,
|
||||||
|
isEmployee
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
@@ -93,7 +94,12 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
) : (
|
) : (
|
||||||
<EyeOutlined className="notification-toggle-icon" />
|
<EyeOutlined className="notification-toggle-icon" />
|
||||||
)}
|
)}
|
||||||
<Switch checked={showUnreadOnly} onChange={(checked) => toggleUnreadOnly(checked)} size="small" />
|
<Switch
|
||||||
|
checked={showUnreadOnly}
|
||||||
|
onChange={(checked) => toggleUnreadOnly(checked)}
|
||||||
|
size="small"
|
||||||
|
disabled={!isEmployee}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
||||||
@@ -106,14 +112,20 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Virtuoso
|
{!isEmployee ? (
|
||||||
ref={virtuosoRef}
|
<div style={{ padding: 10 }}>
|
||||||
style={{ height: "400px", width: "100%" }}
|
<Alert message={t("notifications.labels.employee-notification")} type="warning" />
|
||||||
data={notifications}
|
</div>
|
||||||
totalCount={notifications.length}
|
) : (
|
||||||
endReached={loadMore}
|
<Virtuoso
|
||||||
itemContent={renderNotification}
|
ref={virtuosoRef}
|
||||||
/>
|
style={{ height: "400px", width: "100%" }}
|
||||||
|
data={notifications}
|
||||||
|
totalCount={notifications.length}
|
||||||
|
endReached={loadMore}
|
||||||
|
itemContent={renderNotification}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { connect } from "react-redux";
|
|||||||
import NotificationCenterComponent from "./notification-center.component";
|
import NotificationCenterComponent from "./notification-center.component";
|
||||||
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
// This will be used to poll for notifications when the socket is disconnected
|
// This will be used to poll for notifications when the socket is disconnected
|
||||||
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
||||||
@@ -17,17 +18,18 @@ const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
|||||||
* @param onClose
|
* @param onClose
|
||||||
* @param bodyshop
|
* @param bodyshop
|
||||||
* @param unreadCount
|
* @param unreadCount
|
||||||
|
* @param currentUser
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
|
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount, currentUser }) => {
|
||||||
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
||||||
const notificationRef = useRef(null);
|
const notificationRef = useRef(null);
|
||||||
|
|
||||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||||
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
|
||||||
const baseWhereClause = useMemo(() => {
|
const baseWhereClause = useMemo(() => {
|
||||||
return { associationid: { _eq: userAssociationId } };
|
return { associationid: { _eq: userAssociationId } };
|
||||||
@@ -51,7 +53,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
fetchPolicy: "cache-and-network",
|
fetchPolicy: "cache-and-network",
|
||||||
notifyOnNetworkStatusChange: true,
|
notifyOnNetworkStatusChange: true,
|
||||||
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
||||||
skip: !userAssociationId,
|
skip: !userAssociationId || !isEmployee,
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error(`Error polling Notifications: ${err?.message || ""}`);
|
console.error(`Error polling Notifications: ${err?.message || ""}`);
|
||||||
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
||||||
@@ -71,7 +73,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
}, [visible, onClose]);
|
}, [visible, onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.notifications) {
|
if (data?.notifications && isEmployee) {
|
||||||
const processedNotifications = data.notifications
|
const processedNotifications = data.notifications
|
||||||
.map((notif) => {
|
.map((notif) => {
|
||||||
let scenarioText;
|
let scenarioText;
|
||||||
@@ -101,11 +103,13 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
})
|
})
|
||||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||||
setNotifications(processedNotifications);
|
setNotifications(processedNotifications);
|
||||||
|
} else if (!isEmployee) {
|
||||||
|
setNotifications([]); // Clear notifications if not an employee
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data, isEmployee]);
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
if (!queryLoading && data?.notifications.length) {
|
if (!queryLoading && data?.notifications.length && isEmployee) {
|
||||||
setIsLoading(true); // Show spinner during fetchMore
|
setIsLoading(true); // Show spinner during fetchMore
|
||||||
fetchMore({
|
fetchMore({
|
||||||
variables: { offset: data.notifications.length, where: whereClause },
|
variables: { offset: data.notifications.length, where: whereClause },
|
||||||
@@ -121,13 +125,14 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false)); // Hide spinner when done
|
.finally(() => setIsLoading(false)); // Hide spinner when done
|
||||||
}
|
}
|
||||||
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause]);
|
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause, isEmployee]);
|
||||||
|
|
||||||
const handleToggleUnreadOnly = (value) => {
|
const handleToggleUnreadOnly = (value) => {
|
||||||
setShowUnreadOnly(value);
|
setShowUnreadOnly(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkAllRead = useCallback(() => {
|
const handleMarkAllRead = useCallback(() => {
|
||||||
|
if (!isEmployee) return; // Do nothing if not an employee
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
markAllNotificationsRead()
|
markAllNotificationsRead()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -147,7 +152,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
})
|
})
|
||||||
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
|
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly]);
|
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly, isEmployee]);
|
||||||
|
|
||||||
const handleNotificationClick = useCallback(
|
const handleNotificationClick = useCallback(
|
||||||
(notificationId) => {
|
(notificationId) => {
|
||||||
@@ -170,17 +175,18 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && !isConnected) {
|
if (visible && !isConnected && isEmployee) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
refetch()
|
refetch()
|
||||||
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
|
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}
|
}
|
||||||
}, [visible, isConnected, refetch]);
|
}, [visible, isConnected, refetch, isEmployee]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationCenterComponent
|
<NotificationCenterComponent
|
||||||
ref={notificationRef}
|
ref={notificationRef}
|
||||||
|
isEmployee={isEmployee}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
notifications={notifications}
|
notifications={notifications}
|
||||||
@@ -196,7 +202,8 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(NotificationCenterContainer);
|
export default connect(mapStateToProps, null)(NotificationCenterContainer);
|
||||||
|
|||||||
@@ -1,32 +1,41 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button, Card, Checkbox, Form, Space, Table } from "antd";
|
import { Alert, Button, Card, Checkbox, Divider, Form, Space, Switch, Table, Typography } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import { QUERY_NOTIFICATION_SETTINGS, UPDATE_NOTIFICATION_SETTINGS } from "../../graphql/user.queries.js";
|
import {
|
||||||
|
QUERY_NOTIFICATION_SETTINGS,
|
||||||
|
UPDATE_NOTIFICATION_SETTINGS,
|
||||||
|
UPDATE_NOTIFICATIONS_AUTOADD
|
||||||
|
} from "../../graphql/user.queries.js";
|
||||||
import { notificationScenarios } from "../../utils/jobNotificationScenarios.js";
|
import { notificationScenarios } from "../../utils/jobNotificationScenarios.js";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
|
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifications Settings Form
|
* Notifications Settings Form
|
||||||
* @param currentUser
|
* @param currentUser
|
||||||
|
* @param bodyshop
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const NotificationSettingsForm = ({ currentUser }) => {
|
const NotificationSettingsForm = ({ currentUser, bodyshop }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [initialValues, setInitialValues] = useState({});
|
const [initialValues, setInitialValues] = useState({});
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
|
const [autoAddEnabled, setAutoAddEnabled] = useState(false);
|
||||||
|
const [initialAutoAdd, setInitialAutoAdd] = useState(false);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
|
||||||
// Fetch notification settings.
|
// Fetch notification settings and notifications_autoadd
|
||||||
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
@@ -34,13 +43,16 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
skip: !currentUser
|
skip: !currentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateNotificationSettings, { loading: saving }] = useMutation(UPDATE_NOTIFICATION_SETTINGS);
|
const [updateNotificationSettings, { loading: savingSettings }] = useMutation(UPDATE_NOTIFICATION_SETTINGS);
|
||||||
|
const [updateNotificationsAutoAdd, { loading: savingAutoAdd }] = useMutation(UPDATE_NOTIFICATIONS_AUTOADD);
|
||||||
|
|
||||||
// Populate form with fetched data.
|
// Populate form with fetched data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.associations?.length > 0) {
|
if (data?.associations?.length > 0) {
|
||||||
const settings = data.associations[0].notification_settings || {};
|
const settings = data.associations[0].notification_settings || {};
|
||||||
// Ensure each scenario has an object with { app, email, fcm }.
|
const autoAdd = data.associations[0].notifications_autoadd ?? false;
|
||||||
|
|
||||||
|
// Ensure each scenario has an object with { app, email, fcm }
|
||||||
const formattedValues = notificationScenarios.reduce((acc, scenario) => {
|
const formattedValues = notificationScenarios.reduce((acc, scenario) => {
|
||||||
acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false };
|
acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false };
|
||||||
return acc;
|
return acc;
|
||||||
@@ -48,32 +60,66 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
|
|
||||||
setInitialValues(formattedValues);
|
setInitialValues(formattedValues);
|
||||||
form.setFieldsValue(formattedValues);
|
form.setFieldsValue(formattedValues);
|
||||||
setIsDirty(false); // Reset dirty state when new data loads.
|
setAutoAddEnabled(autoAdd);
|
||||||
|
setInitialAutoAdd(autoAdd);
|
||||||
|
setIsDirty(false); // Reset dirty state when new data loads
|
||||||
}
|
}
|
||||||
}, [data, form]);
|
}, [data, form]);
|
||||||
|
|
||||||
|
// Handle toggle of notifications_autoadd
|
||||||
|
const handleAutoAddToggle = async (checked) => {
|
||||||
|
if (data?.associations?.length > 0) {
|
||||||
|
const userId = data.associations[0].id;
|
||||||
|
try {
|
||||||
|
const result = await updateNotificationsAutoAdd({
|
||||||
|
variables: { id: userId, autoadd: checked }
|
||||||
|
});
|
||||||
|
if (!result?.errors) {
|
||||||
|
setAutoAddEnabled(checked);
|
||||||
|
setInitialAutoAdd(checked);
|
||||||
|
notification.success({ message: t("notifications.labels.auto-add-success") });
|
||||||
|
setIsDirty(false); // Reset dirty state if only auto-add was changed
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to update auto-add setting");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setAutoAddEnabled(!checked); // Revert on error
|
||||||
|
notification.error({ message: t("notifications.labels.auto-add-failure") });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle save of notification settings
|
||||||
const handleSave = async (values) => {
|
const handleSave = async (values) => {
|
||||||
if (data?.associations?.length > 0) {
|
if (data?.associations?.length > 0) {
|
||||||
const userId = data.associations[0].id;
|
const userId = data.associations[0].id;
|
||||||
// Save the updated notification settings.
|
try {
|
||||||
const result = await updateNotificationSettings({ variables: { id: userId, ns: values } });
|
const result = await updateNotificationSettings({ variables: { id: userId, ns: values } });
|
||||||
if (!result?.errors) {
|
if (!result?.errors) {
|
||||||
notification.success({ message: t("notifications.labels.notification-settings-success") });
|
notification.success({ message: t("notifications.labels.notification-settings-success") });
|
||||||
setInitialValues(values);
|
setInitialValues(values);
|
||||||
setIsDirty(false);
|
setIsDirty(false);
|
||||||
} else {
|
} else {
|
||||||
|
throw new Error("Failed to update notification settings");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
notification.error({ message: t("notifications.labels.notification-settings-failure") });
|
notification.error({ message: t("notifications.labels.notification-settings-failure") });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mark the form as dirty on any manual change.
|
// Mark the form as dirty on any manual change
|
||||||
const handleFormChange = () => {
|
const handleFormChange = () => {
|
||||||
setIsDirty(true);
|
setIsDirty(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if auto-add has changed
|
||||||
|
const isAutoAddDirty = autoAddEnabled !== initialAutoAdd;
|
||||||
|
|
||||||
|
// Handle reset of form and auto-add
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
form.setFieldsValue(initialValues);
|
form.setFieldsValue(initialValues);
|
||||||
|
setAutoAddEnabled(initialAutoAdd);
|
||||||
setIsDirty(false);
|
setIsDirty(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,17 +185,30 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
title={t("notifications.labels.notificationscenarios")}
|
title={t("notifications.labels.notificationscenarios")}
|
||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="default" onClick={handleReset} disabled={!isDirty}>
|
<Typography.Text type="secondary">{t("notifications.labels.auto-add")}</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
checked={autoAddEnabled}
|
||||||
|
onChange={handleAutoAddToggle}
|
||||||
|
loading={savingAutoAdd}
|
||||||
|
// checkedChildren={t("notifications.labels.auto-add-on")}
|
||||||
|
// unCheckedChildren={t("notifications.labels.auto-add-off")}
|
||||||
|
/>
|
||||||
|
<Button type="default" onClick={handleReset} disabled={!isDirty && !isAutoAddDirty}>
|
||||||
{t("general.actions.clear")}
|
{t("general.actions.clear")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="primary" htmlType="submit" disabled={!isDirty} loading={savingSettings}>
|
||||||
<Button type="primary" htmlType="submit" disabled={!isDirty} loading={saving}>
|
|
||||||
{t("notifications.labels.save")}
|
{t("notifications.labels.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{!isEmployee && (
|
||||||
|
<div style={{ width: "100%", marginBottom: "10px" }}>
|
||||||
|
<Alert message={t("notifications.labels.employee-notification")} type="warning" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
|
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
|
||||||
|
<Divider />
|
||||||
</Card>
|
</Card>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
@@ -158,11 +217,13 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
NotificationSettingsForm.propTypes = {
|
NotificationSettingsForm.propTypes = {
|
||||||
currentUser: PropTypes.shape({
|
currentUser: PropTypes.shape({
|
||||||
email: PropTypes.string.isRequired
|
email: PropTypes.string.isRequired
|
||||||
}).isRequired
|
}).isRequired,
|
||||||
|
bodyshop: PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(NotificationSettingsForm);
|
export default connect(mapStateToProps)(NotificationSettingsForm);
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ export function PartsOrderModalContainer({
|
|||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
okButtonProps={{ loading: saving }}
|
okButtonProps={{ loading: saving }}
|
||||||
cancelButtonProps={{ loading: saving }}
|
cancelButtonProps={{ loading: saving }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
width="75%"
|
width="75%"
|
||||||
forceRender
|
forceRender
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function PartsQueueDetailCard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{data ? (
|
{data ? (
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function PartsReceiveModalContainer({ partsReceiveModal, toggleModalVisib
|
|||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
okButtonProps={{ loading: loading }}
|
okButtonProps={{ loading: loading }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
forceRender
|
forceRender
|
||||||
width="50%"
|
width="50%"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop }) {
|
|||||||
<Modal
|
<Modal
|
||||||
title={!context || (context && !context.id) ? t("payments.labels.new") : t("payments.labels.edit")}
|
title={!context || (context && !context.id) ? t("payments.labels.new") : t("payments.labels.edit")}
|
||||||
open={open}
|
open={open}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
okText={t("general.actions.save")}
|
okText={t("general.actions.save")}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
width="50%"
|
width="50%"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function PrintCenterModalContainer({ printCenterModal, toggleModalVisible
|
|||||||
okText={t("general.actions.close")}
|
okText={t("general.actions.close")}
|
||||||
width="90%"
|
width="90%"
|
||||||
title={t("printcenter.labels.title")}
|
title={t("printcenter.labels.title")}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<PrintCenterModalComponent context={context} />
|
<PrintCenterModalComponent context={context} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from "react";
|
|
||||||
import { Card, Form, Select } from "antd";
|
import { Card, Form, Select } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const FilterSettings = ({
|
const FilterSettings = ({
|
||||||
selectedMdInsCos,
|
selectedMdInsCos,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Card, Checkbox, Col, Form, Row } from "antd";
|
import { Card, Checkbox, Col, Form, Row } from "antd";
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const InformationSettings = ({ t }) => (
|
const InformationSettings = ({ t }) => (
|
||||||
<Card title={t("production.settings.information")}>
|
<Card title={t("production.settings.information")} style={{ maxWidth: "100%", overflowX: "auto" }}>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]} wrap>
|
||||||
{[
|
{[
|
||||||
"model_info",
|
"model_info",
|
||||||
"ownr_nm",
|
"ownr_nm",
|
||||||
@@ -21,7 +20,7 @@ const InformationSettings = ({ t }) => (
|
|||||||
"subtotal",
|
"subtotal",
|
||||||
"tasks"
|
"tasks"
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<Col span={4} key={item}>
|
<Col xs={24} sm={12} md={8} lg={6} key={item}>
|
||||||
<Form.Item name={item} valuePropName="checked">
|
<Form.Item name={item} valuePropName="checked">
|
||||||
<Checkbox>{t(`production.labels.${item}`)}</Checkbox>
|
<Checkbox>{t(`production.labels.${item}`)}</Checkbox>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Card, Col, Form, Radio, Row } from "antd";
|
import { Card, Col, Form, Radio, Row } from "antd";
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const LayoutSettings = ({ t }) => (
|
const LayoutSettings = ({ t }) => (
|
||||||
<Card title={t("production.settings.layout")}>
|
<Card title={t("production.settings.layout")} style={{ maxWidth: "100%", overflowX: "auto" }}>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
@@ -48,9 +47,9 @@ const LayoutSettings = ({ t }) => (
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
].map(({ name, label, options }) => (
|
].map(({ name, label, options }) => (
|
||||||
<Col span={4} key={name}>
|
<Col xs={24} sm={16} md={10} lg={8} key={name}>
|
||||||
<Form.Item name={name} label={label}>
|
<Form.Item name={name} label={label}>
|
||||||
<Radio.Group>
|
<Radio.Group style={{ display: "flex", flexWrap: "nowrap" }}>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<Radio.Button key={option.value.toString()} value={option.value}>
|
<Radio.Button key={option.value.toString()} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { Card, Checkbox, Form } from "antd";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
import { DragDropContext, Draggable, Droppable } from "../trello-board/dnd/lib/index.js";
|
import { DragDropContext, Draggable, Droppable } from "../trello-board/dnd/lib/index.js";
|
||||||
import { statisticsItems } from "./defaultKanbanSettings.js";
|
import { statisticsItems } from "./defaultKanbanSettings.js";
|
||||||
import { Card, Checkbox, Form } from "antd";
|
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChanges }) => {
|
const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChanges }) => {
|
||||||
const onDragEnd = (result) => {
|
const onDragEnd = (result) => {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
|
import { SettingOutlined } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Card, Col, Form, Popover, Row, Tabs } from "antd";
|
import { Button, Card, Col, Form, Popover, Row, Tabs } from "antd";
|
||||||
|
import { isFunction } from "lodash";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
|
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
|
||||||
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
|
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
|
||||||
import LayoutSettings from "./LayoutSettings.jsx";
|
|
||||||
import InformationSettings from "./InformationSettings.jsx";
|
|
||||||
import StatisticsSettings from "./StatisticsSettings.jsx";
|
|
||||||
import FilterSettings from "./FilterSettings.jsx";
|
import FilterSettings from "./FilterSettings.jsx";
|
||||||
import PropTypes from "prop-types";
|
import InformationSettings from "./InformationSettings.jsx";
|
||||||
import { isFunction } from "lodash";
|
import LayoutSettings from "./LayoutSettings.jsx";
|
||||||
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
|
import StatisticsSettings from "./StatisticsSettings.jsx";
|
||||||
import { SettingOutlined } from "@ant-design/icons";
|
|
||||||
|
|
||||||
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
|
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@@ -87,7 +87,7 @@ function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bod
|
|||||||
};
|
};
|
||||||
|
|
||||||
const overlay = (
|
const overlay = (
|
||||||
<Card style={{ minWidth: "80vw" }}>
|
<Card style={{ maxWidth: "80vw", width: "100%"}}>
|
||||||
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
|
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultActiveKey="1"
|
defaultActiveKey="1"
|
||||||
|
|||||||
@@ -100,26 +100,28 @@ const BoardContainer = ({
|
|||||||
const onLaneDrag = useCallback(
|
const onLaneDrag = useCallback(
|
||||||
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
|
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
setDragTime(source.droppableId);
|
|
||||||
if (!type || type !== "lane" || !source || !destination || isEqual(source, destination)) return;
|
|
||||||
|
|
||||||
setIsProcessing(true);
|
// Only update drag time if it's a valid drop with a different destination
|
||||||
|
if (type === "lane" && source && destination && !isEqual(source, destination)) {
|
||||||
|
setDragTime(source.droppableId);
|
||||||
|
setIsProcessing(true);
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
actions.moveCardAcrossLanes({
|
actions.moveCardAcrossLanes({
|
||||||
fromLaneId: source.droppableId,
|
fromLaneId: source.droppableId,
|
||||||
toLaneId: destination.droppableId,
|
toLaneId: destination.droppableId,
|
||||||
cardId: draggableId,
|
cardId: draggableId,
|
||||||
index: destination.index
|
index: destination.index
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error in onLaneDrag", err);
|
console.error("Error in onLaneDrag", err);
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, onDragEnd, setDragTime]
|
[dispatch, onDragEnd, setDragTime]
|
||||||
|
|||||||
@@ -120,15 +120,14 @@ const Lane = ({
|
|||||||
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
|
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
|
||||||
const FinalComponent = collapsed ? "div" : Component;
|
const FinalComponent = collapsed ? "div" : Component;
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
useWindowScroll: true,
|
data: renderedCards,
|
||||||
data: renderedCards
|
customScrollParent: laneRef.current
|
||||||
};
|
};
|
||||||
|
|
||||||
const verticalProps = {
|
const verticalProps = {
|
||||||
...commonProps,
|
...commonProps,
|
||||||
listClassName: "grid-container",
|
listClassName: "grid-container",
|
||||||
itemClassName: "grid-item",
|
itemClassName: "grid-item",
|
||||||
customScrollParent: laneRef.current,
|
|
||||||
components: {
|
components: {
|
||||||
List: ListComponent,
|
List: ListComponent,
|
||||||
Item: ItemComponent
|
Item: ItemComponent
|
||||||
@@ -142,7 +141,6 @@ const Lane = ({
|
|||||||
components: { Item: HeightPreservingItem },
|
components: { Item: HeightPreservingItem },
|
||||||
overscan: { main: 3, reverse: 3 },
|
overscan: { main: 3, reverse: 3 },
|
||||||
itemContent: (index, item) => renderDraggable(index, item),
|
itemContent: (index, item) => renderDraggable(index, item),
|
||||||
scrollerRef: provided.innerRef,
|
|
||||||
style: {
|
style: {
|
||||||
minWidth: maxCardWidth,
|
minWidth: maxCardWidth,
|
||||||
minHeight: maxLaneHeight
|
minHeight: maxLaneHeight
|
||||||
@@ -180,13 +178,14 @@ const Lane = ({
|
|||||||
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
|
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
{...provided.droppableProps}
|
ref={laneRef} // Ensure laneRef is set here
|
||||||
ref={provided.innerRef}
|
style={{ height: "100%", width: "100%" }} // Make it scrollable
|
||||||
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
|
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
|
||||||
style={{ ...provided.droppableProps.style }}
|
|
||||||
>
|
>
|
||||||
<FinalComponent {...finalComponentProps} />
|
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...provided.droppableProps.style }}>
|
||||||
{shouldRenderPlaceholder && provided.placeholder}
|
<FinalComponent {...finalComponentProps} />
|
||||||
|
{shouldRenderPlaceholder && provided.placeholder}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HeightMemoryWrapper>
|
</HeightMemoryWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ export function ProfileShopsContainer({ bodyshop, currentUser }) {
|
|||||||
|
|
||||||
//Force window refresh.
|
//Force window refresh.
|
||||||
|
|
||||||
|
//Ping the new partner to refresh.
|
||||||
|
axios.post("http://localhost:1337/refresh");
|
||||||
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function ReportCenterModalContainer({ reportCenterModal, toggleModalVisib
|
|||||||
onOk={() => toggleModalVisible()}
|
onOk={() => toggleModalVisible()}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
cancelButtonProps={{ style: { display: "none" } }}
|
cancelButtonProps={{ style: { display: "none" } }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
width="80%"
|
width="80%"
|
||||||
>
|
>
|
||||||
<RbacWrapperComponent action="shop:reportcenter">
|
<RbacWrapperComponent action="shop:reportcenter">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd";
|
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } 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";
|
||||||
@@ -8,16 +8,16 @@ import { calculateScheduleLoad } from "../../redux/application/application.actio
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
|
import BlurWrapper from "../feature-wrapper/blur-wrapper.component";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
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 EmailInput from "../form-items-formatted/email-form-item.component";
|
import EmailInput from "../form-items-formatted/email-form-item.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||||
import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container";
|
import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container";
|
||||||
import ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component";
|
import ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component";
|
||||||
import "./schedule-job-modal.scss";
|
|
||||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
|
||||||
import BlurWrapper from "../feature-wrapper/blur-wrapper.component";
|
|
||||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||||
|
import "./schedule-job-modal.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -60,10 +60,12 @@ export function ScheduleJobModalComponent({
|
|||||||
const totalHours =
|
const totalHours =
|
||||||
lbrHrsData.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs + lbrHrsData.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs;
|
lbrHrsData.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs + lbrHrsData.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs;
|
||||||
|
|
||||||
if (values.start && !values.scheduled_completion)
|
if (values.start && !values.scheduled_completion) {
|
||||||
form.setFieldsValue({
|
const addDays = bodyshop.ss_configuration.nobusinessdays
|
||||||
scheduled_completion: dayjs(values.start).businessDaysAdd(totalHours / bodyshop.target_touchtime, "day")
|
? dayjs(values.start).add(totalHours / (bodyshop.target_touchtime || 1), "day")
|
||||||
});
|
: dayjs(values.start).businessDaysAdd(totalHours / (bodyshop.target_touchtime || 1), "day");
|
||||||
|
form.setFieldsValue({ scheduled_completion: addDays });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export function ScheduleJobModalContainer({
|
|||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
width={"90%"}
|
width={"90%"}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
okButtonProps={{
|
okButtonProps={{
|
||||||
loading: loading
|
loading: loading
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
|||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
open={state.open}
|
open={state.open}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
width="80%"
|
width="80%"
|
||||||
closable={false}
|
closable={false}
|
||||||
cancelButtonProps={{ style: { display: "none" } }}
|
cancelButtonProps={{ style: { display: "none" } }}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Card, Tabs } from "antd";
|
import { Button, Card, Tabs } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -24,6 +23,8 @@ import ShopInfoRoGuard from "./shop-info.roguard.component";
|
|||||||
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
import ShopInfoNotificationsAutoadd from "./shop-info.notifications-autoadd.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -41,6 +42,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
|||||||
names: ["CriticalPartsScanning", "Enhanced_Payroll"],
|
names: ["CriticalPartsScanning", "Enhanced_Payroll"],
|
||||||
splitKey: bodyshop.imexshopid
|
splitKey: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
const { scenarioNotificationsOn } = useSocket();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
@@ -137,9 +139,21 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
key: "intellipay",
|
key: "intellipay",
|
||||||
label: InstanceRenderManager({ rome: t("bodyshop.labels.romepay"), imex: t("bodyshop.labels.imexpay") }),
|
label: InstanceRenderManager({
|
||||||
|
rome: t("bodyshop.labels.romepay"),
|
||||||
|
imex: t("bodyshop.labels.imexpay")
|
||||||
|
}),
|
||||||
children: <ShopInfoIntellipay form={form} />
|
children: <ShopInfoIntellipay form={form} />
|
||||||
}
|
},
|
||||||
|
...(scenarioNotificationsOn
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "notifications_autoadd",
|
||||||
|
label: t("bodyshop.labels.notifications.followers"),
|
||||||
|
children: <ShopInfoNotificationsAutoadd form={form} bodyshop={bodyshop} />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Form, Typography } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component.jsx";
|
||||||
|
|
||||||
|
const { Text, Paragraph } = Typography;
|
||||||
|
|
||||||
|
export default function ShopInfoNotificationsAutoadd({ bodyshop }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// Filter employee options to ensure active employees with valid IDs
|
||||||
|
const employeeOptions = bodyshop?.employees?.filter((e) => e.active && e.user_email && e.id) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Paragraph>{t("bodyshop.fields.notifications.description")}</Paragraph>
|
||||||
|
<Text type="secondary">{t("bodyshop.labels.notifications.followers")}</Text>
|
||||||
|
{employeeOptions.length > 0 ? (
|
||||||
|
<Form.Item
|
||||||
|
name="notification_followers"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
type: "array",
|
||||||
|
message: t("general.validation.array")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: async (_, value) => {
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return Promise.resolve(); // Allow empty array
|
||||||
|
}
|
||||||
|
const hasInvalid = value.some((id) => id == null || typeof id !== "string" || id.trim() === "");
|
||||||
|
if (hasInvalid) {
|
||||||
|
return Promise.reject(new Error(t("bodyshop.fields.notifications.invalid_followers")));
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<EmployeeSearchSelectComponent
|
||||||
|
style={{ minWidth: "100%" }}
|
||||||
|
mode="multiple"
|
||||||
|
options={employeeOptions}
|
||||||
|
placeholder={t("bodyshop.fields.notifications.placeholder")}
|
||||||
|
showEmail={true}
|
||||||
|
onChange={(value) => {
|
||||||
|
// Filter out null or invalid values before passing to Form
|
||||||
|
const cleanedValue = value?.filter((id) => id != null && typeof id === "string" && id.trim() !== "");
|
||||||
|
return cleanedValue;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Text type="secondary">{t("bodyshop.fields.no_employees_available")}</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch, TimePicker } from "antd";
|
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch, TimePicker } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component";
|
import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import { ColorPicker } from "./shop-info.rostatus.component";
|
import { ColorPicker } from "./shop-info.rostatus.component";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
@@ -78,6 +77,13 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
|||||||
>
|
>
|
||||||
<InputNumber min={0} />
|
<InputNumber min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={["ss_configuration", "nobusinessdays"]}
|
||||||
|
label={t("bodyshop.fields.ss_configuration.nobusinessdays")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["md_lost_sale_reasons"]}
|
name={["md_lost_sale_reasons"]}
|
||||||
label={t("bodyshop.fields.md_lost_sale_reasons")}
|
label={t("bodyshop.fields.md_lost_sale_reasons")}
|
||||||
|
|||||||
@@ -25,23 +25,6 @@ export function ShopTemplateTestRender({ bodyshop, query, emailEditorRef, style
|
|||||||
|
|
||||||
emailEditorRef.current.exportHtml(async (data) => {
|
emailEditorRef.current.exportHtml(async (data) => {
|
||||||
try {
|
try {
|
||||||
// const inlineHtml = await axios.post("/render/inlinecss", {
|
|
||||||
// html: data.html,
|
|
||||||
// url: `${window.location.protocol}://${window.location.host}/`,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const { data: contextData } = await client.query({
|
|
||||||
// query: gql(query),
|
|
||||||
// variables: variables,
|
|
||||||
//
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const renderResponse = await axios.post("/render", {
|
|
||||||
// view: inlineHtml.data,
|
|
||||||
// context: { ...contextData, bodyshop: bodyshop },
|
|
||||||
// });
|
|
||||||
// displayTemplateInWindowNoprint(renderResponse.data);
|
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
|
|||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}}
|
}}
|
||||||
okButtonProps={{ disabled: !isTouched }}
|
okButtonProps={{ disabled: !isTouched }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{data ? (
|
{data ? (
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function TimeTicketListTeamPay({ bodyshop, context, actions }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal width={"80%"} open={visible} destroyOnClose onOk={handleOk} onCancel={() => setVisible(false)}>
|
<Modal width={"80%"} open={visible} destroyOnHidden onOk={handleOk} onCancel={() => setVisible(false)}>
|
||||||
<Form layout="vertical" form={form} initialValues={{ jobid: jobId }}>
|
<Form layout="vertical" form={form} initialValues={{ jobid: jobId }}>
|
||||||
<LayoutFormRow grow noDivider>
|
<LayoutFormRow grow noDivider>
|
||||||
<Form.Item shouldUpdate>
|
<Form.Item shouldUpdate>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useLazyQuery } from "@apollo/client";
|
import { useLazyQuery } from "@apollo/client";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Form, Input, InputNumber, Select, Switch } from "antd";
|
import { Card, Form, Input, InputNumber, 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";
|
||||||
@@ -19,6 +18,7 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
|||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
||||||
|
import JobEmployeeAssignmentsContainer from "./../job-employee-assignments/job-employee-assignments.container";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -319,10 +319,15 @@ export function TimeTicketModalComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideTimeTickets = false }) {
|
export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideTimeTickets = false }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
if (loading) return <LoadingSkeleton />;
|
if (loading) return <LoadingSkeleton />;
|
||||||
if (!lineTicketData) return null;
|
if (!lineTicketData) return null;
|
||||||
|
if (!jobid) return null;
|
||||||
return (
|
return (
|
||||||
<div>
|
<Space direction="vertical" style={{ width: "100%" }}>
|
||||||
|
<Card style={{ height: "100%" }} title={t("jobs.labels.employeeassignments")}>
|
||||||
|
<JobEmployeeAssignmentsContainer job={lineTicketData.jobs_by_pk} />
|
||||||
|
</Card>
|
||||||
<LaborAllocationsTable
|
<LaborAllocationsTable
|
||||||
jobId={jobid}
|
jobId={jobid}
|
||||||
joblines={lineTicketData.joblines}
|
joblines={lineTicketData.joblines}
|
||||||
@@ -332,6 +337,6 @@ export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideT
|
|||||||
{!hideTimeTickets && (
|
{!hideTimeTickets && (
|
||||||
<TimeTicketList loading={loading} timetickets={jobid ? lineTicketData.timetickets : []} techConsole />
|
<TimeTicketList loading={loading} timetickets={jobid ? lineTicketData.timetickets : []} techConsole />
|
||||||
)}
|
)}
|
||||||
</div>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { PageHeader } from "@ant-design/pro-layout";
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Form, Modal, Space } from "antd";
|
import { Button, Form, Modal, Space } 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 { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries";
|
import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries";
|
||||||
import { INSERT_NEW_TIME_TICKET, UPDATE_TIME_TICKET } from "../../graphql/timetickets.queries";
|
import { INSERT_NEW_TIME_TICKET, UPDATE_TIME_TICKET } from "../../graphql/timetickets.queries";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
@@ -14,7 +15,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
|
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
|
||||||
import TimeTicketModalComponent from "./time-ticket-modal.component";
|
import TimeTicketModalComponent from "./time-ticket-modal.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
timeTicketModal: selectTimeTicket,
|
timeTicketModal: selectTimeTicket,
|
||||||
@@ -81,7 +81,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMutationSuccess = (response) => {
|
const handleMutationSuccess = () => {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("timetickets.successes.created")
|
message: t("timetickets.successes.created")
|
||||||
});
|
});
|
||||||
@@ -123,7 +123,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
|||||||
if (timeTicketModal.open) form.resetFields();
|
if (timeTicketModal.open) form.resetFields();
|
||||||
}, [timeTicketModal.open, form]);
|
}, [timeTicketModal.open, form]);
|
||||||
|
|
||||||
const handleFieldsChange = (changedFields, allFields) => {
|
const handleFieldsChange = (changedFields) => {
|
||||||
if (!!changedFields.employeeid && !!EmployeeAutoCompleteData) {
|
if (!!changedFields.employeeid && !!EmployeeAutoCompleteData) {
|
||||||
const emps = EmployeeAutoCompleteData.employees.filter((e) => e.id === changedFields.employeeid);
|
const emps = EmployeeAutoCompleteData.employees.filter((e) => e.id === changedFields.employeeid);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
@@ -181,7 +181,8 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
|||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
|
id="time-ticket-modal"
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export function TimeTickeTaskModalContainer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export function UpdateAlert({ updateAvailable }) {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col sm={24} md={8} lg={6}>
|
<Col sm={24} md={8} lg={6}>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => window.open("https://imex-online.noticeable.news/", "_blank")}>
|
<Button onClick={() => window.open("https://shopmanagement.canny.io/changelog", "_blank")}>
|
||||||
{i18n.t("general.actions.viewreleasenotes")}
|
{i18n.t("general.actions.viewreleasenotes")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button loading={loading} type="primary" onClick={() => ReloadNewVersion()}>
|
<Button loading={loading} type="primary" onClick={() => ReloadNewVersion()}>
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export const QUERY_BODYSHOP = gql`
|
|||||||
use_paint_scale_data
|
use_paint_scale_data
|
||||||
intellipay_config
|
intellipay_config
|
||||||
md_ro_guard
|
md_ro_guard
|
||||||
|
notification_followers
|
||||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -271,6 +272,7 @@ export const UPDATE_SHOP = gql`
|
|||||||
md_tasks_presets
|
md_tasks_presets
|
||||||
intellipay_config
|
intellipay_config
|
||||||
md_ro_guard
|
md_ro_guard
|
||||||
|
notification_followers
|
||||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -35,6 +35,30 @@ export const GET_LINE_TICKET_BY_PK = gql`
|
|||||||
lbr_adjustments
|
lbr_adjustments
|
||||||
converted
|
converted
|
||||||
status
|
status
|
||||||
|
employee_body
|
||||||
|
employee_body_rel {
|
||||||
|
id
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
|
}
|
||||||
|
employee_csr
|
||||||
|
employee_csr_rel {
|
||||||
|
id
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
|
}
|
||||||
|
employee_prep
|
||||||
|
employee_prep_rel {
|
||||||
|
id
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
|
}
|
||||||
|
employee_refinish
|
||||||
|
employee_refinish_rel {
|
||||||
|
id
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
joblines(where: { jobid: { _eq: $id }, removed: { _eq: false } }) {
|
joblines(where: { jobid: { _eq: $id }, removed: { _eq: false } }) {
|
||||||
id
|
id
|
||||||
|
|||||||
@@ -423,6 +423,7 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
actual_completion
|
actual_completion
|
||||||
actual_delivery
|
actual_delivery
|
||||||
actual_in
|
actual_in
|
||||||
|
acv_amount
|
||||||
adjustment_bottom_line
|
adjustment_bottom_line
|
||||||
alt_transport
|
alt_transport
|
||||||
area_of_damage
|
area_of_damage
|
||||||
@@ -511,6 +512,7 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
est_ph1
|
est_ph1
|
||||||
flat_rate_ats
|
flat_rate_ats
|
||||||
federal_tax_rate
|
federal_tax_rate
|
||||||
|
hit_and_run
|
||||||
id
|
id
|
||||||
inproduction
|
inproduction
|
||||||
ins_addr1
|
ins_addr1
|
||||||
@@ -683,6 +685,8 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
scheduled_delivery
|
scheduled_delivery
|
||||||
scheduled_in
|
scheduled_in
|
||||||
selling_dealer
|
selling_dealer
|
||||||
|
estimate_approved
|
||||||
|
estimate_sent_approval
|
||||||
selling_dealer_contact
|
selling_dealer_contact
|
||||||
servicing_dealer
|
servicing_dealer
|
||||||
servicing_dealer_contact
|
servicing_dealer_contact
|
||||||
@@ -927,6 +931,8 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
|||||||
date_exported
|
date_exported
|
||||||
date_repairstarted
|
date_repairstarted
|
||||||
date_scheduled
|
date_scheduled
|
||||||
|
estimate_sent_approval
|
||||||
|
estimate_approved
|
||||||
date_estimated
|
date_estimated
|
||||||
employee_body_rel {
|
employee_body_rel {
|
||||||
id
|
id
|
||||||
@@ -1075,6 +1081,8 @@ export const UPDATE_JOB = gql`
|
|||||||
date_repairstarted
|
date_repairstarted
|
||||||
date_void
|
date_void
|
||||||
date_lost_sale
|
date_lost_sale
|
||||||
|
estimate_sent_approval
|
||||||
|
estimate_approved
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2429,6 +2437,8 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
|||||||
plate_st
|
plate_st
|
||||||
po_number
|
po_number
|
||||||
production_vars
|
production_vars
|
||||||
|
estimate_sent_approval
|
||||||
|
estimate_approved
|
||||||
ro_number
|
ro_number
|
||||||
scheduled_completion
|
scheduled_completion
|
||||||
scheduled_delivery
|
scheduled_delivery
|
||||||
@@ -2570,6 +2580,20 @@ export const GET_JOB_BY_PK_QUICK_INTAKE = gql`
|
|||||||
actual_completion
|
actual_completion
|
||||||
scheduled_delivery
|
scheduled_delivery
|
||||||
actual_delivery
|
actual_delivery
|
||||||
|
labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
larhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _eq: "LAR" } }, { removed: { _eq: false } }] }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export const QUERY_NOTIFICATION_SETTINGS = gql`
|
|||||||
associations(where: { _and: { useremail: { _eq: $email }, active: { _eq: true } } }) {
|
associations(where: { _and: { useremail: { _eq: $email }, active: { _eq: true } } }) {
|
||||||
id
|
id
|
||||||
notification_settings
|
notification_settings
|
||||||
|
notifications_autoadd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -103,3 +104,12 @@ export const UPDATE_NOTIFICATION_SETTINGS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const UPDATE_NOTIFICATIONS_AUTOADD = gql`
|
||||||
|
mutation UPDATE_NOTIFICATIONS_AUTOADD($id: uuid!, $autoadd: Boolean!) {
|
||||||
|
update_associations_by_pk(pk_columns: { id: $id }, _set: { notifications_autoadd: $autoadd }) {
|
||||||
|
id
|
||||||
|
notifications_autoadd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
42
client/src/pages/feature-request/feature-request.page.jsx
Normal file
42
client/src/pages/feature-request/feature-request.page.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||||
|
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function FeedbackPage({ setBreadcrumbs, setSelectedHeader }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = t("titles.feature-request", {
|
||||||
|
app: InstanceRenderManager({
|
||||||
|
imex: "$t(titles.imexonline)",
|
||||||
|
rome: "$t(titles.romeonline)"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
setBreadcrumbs([{ link: "/manage/feature-request", label: t("titles.bc.feature-request") }]);
|
||||||
|
}, [t, setBreadcrumbs, setSelectedHeader]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function RenderCanny() {
|
||||||
|
const ssoToken = await axios.post("/sso/canny");
|
||||||
|
window.Canny("render", {
|
||||||
|
boardToken: "bba97b06-70db-0334-dee7-8108d73ef614",
|
||||||
|
basePath: `/manage/feature-request`, // See step 2
|
||||||
|
ssoToken: ssoToken.data, // See step 3,
|
||||||
|
theme: "light" // options: light [default], dark, auto
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RenderCanny();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div data-canny />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(FeedbackPage);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FloatButton, Layout, Spin } from "antd";
|
import { Button, FloatButton, Layout, Space, Spin } from "antd";
|
||||||
|
import { AlertOutlined, BulbOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
// import preval from "preval.macro";
|
// import preval from "preval.macro";
|
||||||
import React, { lazy, Suspense, useEffect, useState } from "react";
|
import React, { lazy, Suspense, useEffect, useState } from "react";
|
||||||
@@ -19,7 +20,6 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
|
|||||||
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
|
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
|
||||||
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
||||||
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
||||||
import { requestForToken } from "../../firebase/firebase.utils";
|
|
||||||
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
||||||
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||||
@@ -56,6 +56,7 @@ const ContractCreatePage = lazy(() => import("../contract-create/contract-create
|
|||||||
const ContractDetailPage = lazy(() => import("../contract-detail/contract-detail.page.container"));
|
const ContractDetailPage = lazy(() => import("../contract-detail/contract-detail.page.container"));
|
||||||
const ContractsList = lazy(() => import("../contracts/contracts.page.container"));
|
const ContractsList = lazy(() => import("../contracts/contracts.page.container"));
|
||||||
const BillsListPage = lazy(() => import("../bills/bills.page.container"));
|
const BillsListPage = lazy(() => import("../bills/bills.page.container"));
|
||||||
|
const FeatureRequestPage = lazy(() => import("../feature-request/feature-request.page.jsx"));
|
||||||
|
|
||||||
const JobCostingModal = lazy(() => import("../../components/job-costing-modal/job-costing-modal.container"));
|
const JobCostingModal = lazy(() => import("../../components/job-costing-modal/job-costing-modal.container"));
|
||||||
const ReportCenterModal = lazy(() => import("../../components/report-center-modal/report-center-modal.container"));
|
const ReportCenterModal = lazy(() => import("../../components/report-center-modal/report-center-modal.container"));
|
||||||
@@ -180,15 +181,12 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [alerts, displayedAlertIds, notification]);
|
}, [alerts, displayedAlertIds, notification]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const widgetId = InstanceRenderManager({
|
window.Canny("initChangelog", {
|
||||||
imex: "IABVNO4scRKY11XBQkNr",
|
appID: "680bd2c7ee501290377f6686",
|
||||||
rome: "mQdqARMzkZRUVugJ6TdS"
|
position: "top",
|
||||||
});
|
align: "left",
|
||||||
window.noticeable.render("widget", widgetId);
|
theme: "light" // options: light [default], dark, auto
|
||||||
requestForToken().catch((error) => {
|
|
||||||
console.error(`Unable to request for token.`, error);
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -480,6 +478,8 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
// element={<ShopTemplates />}
|
// element={<ShopTemplates />}
|
||||||
// />
|
// />
|
||||||
}
|
}
|
||||||
|
<Route path="/feature-request/*" index element={<FeatureRequestPage />} />
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/shop/vendors"
|
path="/shop/vendors"
|
||||||
element={
|
element={
|
||||||
@@ -669,7 +669,12 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
margin: "1rem 0rem"
|
margin: "1rem 0rem"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex" }}>
|
<Link to="/manage/feature-request">
|
||||||
|
<Button icon={<BulbOutlined />} type="text">
|
||||||
|
{t("general.labels.feature-request")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Space>
|
||||||
<WssStatusDisplayComponent />
|
<WssStatusDisplayComponent />
|
||||||
<div onClick={broadcastMessage}>
|
<div onClick={broadcastMessage}>
|
||||||
{`${InstanceRenderManager({
|
{`${InstanceRenderManager({
|
||||||
@@ -677,8 +682,10 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
rome: t("titles.romeonline")
|
rome: t("titles.romeonline")
|
||||||
})} - ${import.meta.env.VITE_APP_GIT_SHA_DATE}`}
|
})} - ${import.meta.env.VITE_APP_GIT_SHA_DATE}`}
|
||||||
</div>
|
</div>
|
||||||
<div id="noticeable-widget" style={{ marginLeft: "1rem" }} />
|
<Button icon={<AlertOutlined />} data-canny-changelog type="text">
|
||||||
</div>
|
{t("general.labels.changelog")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
||||||
Disclaimer & Notices
|
Disclaimer & Notices
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import FingerprintJS from "@fingerprintjs/fingerprintjs";
|
import FingerprintJS from "@fingerprintjs/fingerprintjs";
|
||||||
import * as Sentry from "@sentry/browser";
|
|
||||||
import { notification } from "antd";
|
|
||||||
import axios from "axios";
|
|
||||||
import { setUserId, setUserProperties } from "@firebase/analytics";
|
import { setUserId, setUserProperties } from "@firebase/analytics";
|
||||||
import {
|
import {
|
||||||
checkActionCode,
|
checkActionCode,
|
||||||
@@ -12,6 +9,9 @@ import {
|
|||||||
} from "@firebase/auth";
|
} from "@firebase/auth";
|
||||||
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
||||||
import { getToken } from "@firebase/messaging";
|
import { getToken } from "@firebase/messaging";
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
|
import { notification } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import LogRocket from "logrocket";
|
import LogRocket from "logrocket";
|
||||||
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
||||||
@@ -351,7 +351,14 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
|||||||
});
|
});
|
||||||
payload.features?.allAccess === true
|
payload.features?.allAccess === true
|
||||||
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
||||||
: window.$crisp.push(["set", "session:segments", [["basic"]]]);
|
: (() => {
|
||||||
|
const featureKeys = Object.keys(payload.features).filter(
|
||||||
|
(key) =>
|
||||||
|
payload.features[key] === true ||
|
||||||
|
(typeof payload.features[key] === "string" && !isNaN(Date.parse(payload.features[key])))
|
||||||
|
);
|
||||||
|
window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]);
|
||||||
|
})();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Couldnt find $crisp.", error.message);
|
console.warn("Couldnt find $crisp.", error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,6 @@
|
|||||||
"intellipay_config": {
|
"intellipay_config": {
|
||||||
"cash_discount_percentage": "Cash Discount %",
|
"cash_discount_percentage": "Cash Discount %",
|
||||||
"enable_cash_discount": "Enable Cash Discounting",
|
"enable_cash_discount": "Enable Cash Discounting",
|
||||||
"payment_type": "Payment Type Map",
|
|
||||||
"payment_map": {
|
"payment_map": {
|
||||||
"amex": "American Express",
|
"amex": "American Express",
|
||||||
"disc": "Discover",
|
"disc": "Discover",
|
||||||
@@ -344,7 +343,8 @@
|
|||||||
"jcb": "JCB",
|
"jcb": "JCB",
|
||||||
"mast": "MasterCard",
|
"mast": "MasterCard",
|
||||||
"visa": "Visa"
|
"visa": "Visa"
|
||||||
}
|
},
|
||||||
|
"payment_type": "Payment Type Map"
|
||||||
},
|
},
|
||||||
"invoice_federal_tax_rate": "Invoices - Federal Tax Rate",
|
"invoice_federal_tax_rate": "Invoices - Federal Tax Rate",
|
||||||
"invoice_local_tax_rate": "Invoices - Local Tax Rate",
|
"invoice_local_tax_rate": "Invoices - Local Tax Rate",
|
||||||
@@ -601,7 +601,8 @@
|
|||||||
"templates": "Templates"
|
"templates": "Templates"
|
||||||
},
|
},
|
||||||
"ss_configuration": {
|
"ss_configuration": {
|
||||||
"dailyhrslimit": "Daily Incoming Hours Limit"
|
"dailyhrslimit": "Daily Incoming Hours Limit",
|
||||||
|
"nobusinessdays": "Include Weekends"
|
||||||
},
|
},
|
||||||
"ssbuckets": {
|
"ssbuckets": {
|
||||||
"color": "Job Color",
|
"color": "Job Color",
|
||||||
@@ -647,7 +648,12 @@
|
|||||||
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
|
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
|
||||||
"uselocalmediaserver": "Use Local Media Server?",
|
"uselocalmediaserver": "Use Local Media Server?",
|
||||||
"website": "Website",
|
"website": "Website",
|
||||||
"zip_post": "Zip/Postal Code"
|
"zip_post": "Zip/Postal Code",
|
||||||
|
"notifications": {
|
||||||
|
"description": "Select employees to automatically follow new jobs and receive notifications for job updates.",
|
||||||
|
"placeholder": "Search for employees",
|
||||||
|
"invalid_followers": "Invalid selection. Please select valid employees."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"2tiername": "Name => RO",
|
"2tiername": "Name => RO",
|
||||||
@@ -727,7 +733,10 @@
|
|||||||
"ssbuckets": "Job Size Definitions",
|
"ssbuckets": "Job Size Definitions",
|
||||||
"systemsettings": "System Settings",
|
"systemsettings": "System Settings",
|
||||||
"task-presets": "Task Presets",
|
"task-presets": "Task Presets",
|
||||||
"workingdays": "Working Days"
|
"workingdays": "Working Days",
|
||||||
|
"notifications": {
|
||||||
|
"followers": "Notifications"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
"contains": "Contains",
|
"contains": "Contains",
|
||||||
@@ -1235,6 +1244,7 @@
|
|||||||
"areyousure": "Are you sure?",
|
"areyousure": "Are you sure?",
|
||||||
"barcode": "Barcode",
|
"barcode": "Barcode",
|
||||||
"cancel": "Are you sure you want to cancel? Your changes will not be saved.",
|
"cancel": "Are you sure you want to cancel? Your changes will not be saved.",
|
||||||
|
"changelog": "Change Log",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"confirmpassword": "Confirm Password",
|
"confirmpassword": "Confirm Password",
|
||||||
"created_at": "Created At",
|
"created_at": "Created At",
|
||||||
@@ -1244,6 +1254,7 @@
|
|||||||
"errors": "Errors",
|
"errors": "Errors",
|
||||||
"excel": "Excel",
|
"excel": "Excel",
|
||||||
"exceptiontitle": "An error has occurred.",
|
"exceptiontitle": "An error has occurred.",
|
||||||
|
"feature-request": "Have a feature request?",
|
||||||
"friday": "Friday",
|
"friday": "Friday",
|
||||||
"globalsearch": "Global Search",
|
"globalsearch": "Global Search",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
@@ -1322,9 +1333,9 @@
|
|||||||
"notfoundtitle": "We couldn't find what you're looking for...",
|
"notfoundtitle": "We couldn't find what you're looking for...",
|
||||||
"partnernotrunning": "{{app}} has detected that the partner is not running. Please ensure it is running to enable full functionality.",
|
"partnernotrunning": "{{app}} has detected that the partner is not running. Please ensure it is running to enable full functionality.",
|
||||||
"rbacunauth": "You are not authorized to view this content. Please reach out to your shop manager to change your access level.",
|
"rbacunauth": "You are not authorized to view this content. Please reach out to your shop manager to change your access level.",
|
||||||
|
"submit-for-testing": "Submitted Job for testing successfully.",
|
||||||
"unsavedchanges": "You have unsaved changes.",
|
"unsavedchanges": "You have unsaved changes.",
|
||||||
"unsavedchangespopup": "You have unsaved changes. Are you sure you want to leave?",
|
"unsavedchangespopup": "You have unsaved changes. Are you sure you want to leave?"
|
||||||
"submit-for-testing": "Submitted Job for testing successfully."
|
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"dateRangeExceeded": "The date range has been exceeded.",
|
"dateRangeExceeded": "The date range has been exceeded.",
|
||||||
@@ -1635,9 +1646,12 @@
|
|||||||
"actual_completion": "Actual Completion",
|
"actual_completion": "Actual Completion",
|
||||||
"actual_delivery": "Actual Delivery",
|
"actual_delivery": "Actual Delivery",
|
||||||
"actual_in": "Actual In",
|
"actual_in": "Actual In",
|
||||||
|
"acv_amount": "ACV Amount",
|
||||||
"adjustment_bottom_line": "Adjustments",
|
"adjustment_bottom_line": "Adjustments",
|
||||||
"adjustmenthours": "Adjustment Hours",
|
"adjustmenthours": "Adjustment Hours",
|
||||||
"alt_transport": "Alt. Trans.",
|
"alt_transport": "Alt. Trans.",
|
||||||
|
"estimate_sent_approval": "Estimate Sent for Approval",
|
||||||
|
"estimate_approved": "Estimate Approved",
|
||||||
"area_of_damage_impact": {
|
"area_of_damage_impact": {
|
||||||
"10": "Left Front Side",
|
"10": "Left Front Side",
|
||||||
"11": "Left Front Corner",
|
"11": "Left Front Corner",
|
||||||
@@ -1760,9 +1774,10 @@
|
|||||||
"est_ct_ln": "Estimator Last Name",
|
"est_ct_ln": "Estimator Last Name",
|
||||||
"est_ea": "Estimator Email",
|
"est_ea": "Estimator Email",
|
||||||
"est_ph1": "Estimator Phone #",
|
"est_ph1": "Estimator Phone #",
|
||||||
"flat_rate_ats": "Flat Rate ATS?",
|
|
||||||
"federal_tax_payable": "Federal Tax Payable",
|
"federal_tax_payable": "Federal Tax Payable",
|
||||||
"federal_tax_rate": "Federal Tax Rate",
|
"federal_tax_rate": "Federal Tax Rate",
|
||||||
|
"flat_rate_ats": "Flat Rate ATS?",
|
||||||
|
"hit_and_run": "Hit and Run",
|
||||||
"ins_addr1": "Insurance Co. Address",
|
"ins_addr1": "Insurance Co. Address",
|
||||||
"ins_city": "Insurance Co. City",
|
"ins_city": "Insurance Co. City",
|
||||||
"ins_co_id": "Insurance Co. ID",
|
"ins_co_id": "Insurance Co. ID",
|
||||||
@@ -1942,6 +1957,8 @@
|
|||||||
"scheddates": "Schedule Dates"
|
"scheddates": "Schedule Dates"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"sent": "",
|
||||||
|
"approved": "",
|
||||||
"accountsreceivable": "Accounts Receivable",
|
"accountsreceivable": "Accounts Receivable",
|
||||||
"act_price_ppc": "New Part Price",
|
"act_price_ppc": "New Part Price",
|
||||||
"actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).",
|
"actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).",
|
||||||
@@ -2316,8 +2333,8 @@
|
|||||||
"duplicate": "Duplicate this Job",
|
"duplicate": "Duplicate this Job",
|
||||||
"duplicatenolines": "Duplicate this Job without Repair Data",
|
"duplicatenolines": "Duplicate this Job without Repair Data",
|
||||||
"newcccontract": "Create Courtesy Car Contract",
|
"newcccontract": "Create Courtesy Car Contract",
|
||||||
"void": "Void Job",
|
"submit-for-testing": "Submit for Testing",
|
||||||
"submit-for-testing": "Submit for Testing"
|
"void": "Void Job"
|
||||||
},
|
},
|
||||||
"jobsdetail": {
|
"jobsdetail": {
|
||||||
"claimdetail": "Claim Details",
|
"claimdetail": "Claim Details",
|
||||||
@@ -2423,6 +2440,66 @@
|
|||||||
"updated": "Note updated successfully."
|
"updated": "Note updated successfully."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"actions": {
|
||||||
|
"remove": "Remove"
|
||||||
|
},
|
||||||
|
"aria": {
|
||||||
|
"toggle": "Toggle Watching Job"
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"app": "App",
|
||||||
|
"email": "Email",
|
||||||
|
"fcm": "Push"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"auto-add": "Automatically watch Jobs I import",
|
||||||
|
"auto-add-success": "Auto watcher status successfully changed.",
|
||||||
|
"auto-add-failure": "Something went wrong updating your auto watcher status.",
|
||||||
|
"add-watchers": "Add Watchers",
|
||||||
|
"add-watchers-team": "Add Team Members",
|
||||||
|
"employee-search": "Search for an Employee",
|
||||||
|
"mark-all-read": "Mark All Read",
|
||||||
|
"new-notification-title": "New Notification:",
|
||||||
|
"no-watchers": "No Watchers",
|
||||||
|
"notification-center": "Notification Center",
|
||||||
|
"notification-popup-title": "Changes for Job #{{ro_number}}",
|
||||||
|
"notification-settings-failure": "Error saving Notification Settings. {{error}}",
|
||||||
|
"notification-settings-success": "Notification Settings saved successfully.",
|
||||||
|
"notificationscenarios": "Job Notification Scenarios",
|
||||||
|
"ro-number": "RO #{{ro_number}}",
|
||||||
|
"save": "Save Scenarios",
|
||||||
|
"scenario": "Scenario",
|
||||||
|
"show-unread-only": "Show Unread Only",
|
||||||
|
"teams-search": "Search for a Team",
|
||||||
|
"unwatch": "Unwatch",
|
||||||
|
"watch": "Watch",
|
||||||
|
"watching-issue": "Watching",
|
||||||
|
"employee-notification": "Notifications are disabled because you do not have an associated Employee record."
|
||||||
|
},
|
||||||
|
"scenarios": {
|
||||||
|
"alternate-transport-changed": "Alternate Transport Changed",
|
||||||
|
"bill-posted": "Bill Posted",
|
||||||
|
"critical-parts-status-changed": "Critical Parts Status Changed",
|
||||||
|
"intake-delivery-checklist-completed": "Intake or Delivery Checklist Completed",
|
||||||
|
"job-added-to-production": "Job Added to Production",
|
||||||
|
"job-assigned-to-me": "Job Assigned to Me",
|
||||||
|
"job-status-change": "Job Status Changed",
|
||||||
|
"new-media-added-reassigned": "New Media Added or Reassigned",
|
||||||
|
"new-note-added": "New Note Added",
|
||||||
|
"new-time-ticket-posted": "New Time Ticket Posted",
|
||||||
|
"part-marked-back-ordered": "Part Marked Back Ordered",
|
||||||
|
"payment-collected-completed": "Payment Collected / Completed",
|
||||||
|
"schedule-dates-changed": "Schedule Dates Changed",
|
||||||
|
"supplement-imported": "Supplement Imported",
|
||||||
|
"tasks-updated-created": "Tasks Updated / Created"
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"job-watchers": "Job Watchers",
|
||||||
|
"not-employee": "You need to be an employee to watch this job. Reach out to your admin to get set up!",
|
||||||
|
"not-employee-notifications": "You must be an employee to receive notifications"
|
||||||
|
}
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"labels": {
|
"labels": {
|
||||||
"noownerinfo": "No owner information."
|
"noownerinfo": "No owner information."
|
||||||
@@ -3419,6 +3496,7 @@
|
|||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"dms": "DMS Export",
|
"dms": "DMS Export",
|
||||||
"export-logs": "Export Logs",
|
"export-logs": "Export Logs",
|
||||||
|
"feature-request": "Feature Requet",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
"jobs": "Jobs",
|
"jobs": "Jobs",
|
||||||
"jobs-active": "Active Jobs",
|
"jobs-active": "Active Jobs",
|
||||||
@@ -3463,6 +3541,7 @@
|
|||||||
"dashboard": "Dashboard | {{app}}",
|
"dashboard": "Dashboard | {{app}}",
|
||||||
"dms": "DMS Export | {{app}}",
|
"dms": "DMS Export | {{app}}",
|
||||||
"export-logs": "Export Logs | {{app}}",
|
"export-logs": "Export Logs | {{app}}",
|
||||||
|
"feature-request": "Feature Request | {{app}}",
|
||||||
"imexonline": "ImEX Online",
|
"imexonline": "ImEX Online",
|
||||||
"inventory": "Inventory | {{app}}",
|
"inventory": "Inventory | {{app}}",
|
||||||
"jobs": "Active Jobs | {{app}}",
|
"jobs": "Active Jobs | {{app}}",
|
||||||
@@ -3680,10 +3759,10 @@
|
|||||||
"users": {
|
"users": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"signinerror": {
|
"signinerror": {
|
||||||
|
"auth/invalid-email": "A user with this email does not exist.",
|
||||||
"auth/user-disabled": "User account disabled. ",
|
"auth/user-disabled": "User account disabled. ",
|
||||||
"auth/user-not-found": "A user with this email does not exist.",
|
"auth/user-not-found": "A user with this email does not exist.",
|
||||||
"auth/wrong-password": "The email and password combination you provided is incorrect.",
|
"auth/wrong-password": "The email and password combination you provided is incorrect."
|
||||||
"auth/invalid-email": "A user with this email does not exist."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3783,60 +3862,6 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": "You must enter a unique vendor name."
|
"unique_vendor_name": "You must enter a unique vendor name."
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"labels": {
|
|
||||||
"notification-center": "Notification Center",
|
|
||||||
"scenario": "Scenario",
|
|
||||||
"notificationscenarios": "Job Notification Scenarios",
|
|
||||||
"save": "Save Scenarios",
|
|
||||||
"watching-issue": "Watching",
|
|
||||||
"add-watchers": "Add Watchers",
|
|
||||||
"employee-search": "Search for an Employee",
|
|
||||||
"teams-search": "Search for a Team",
|
|
||||||
"add-watchers-team": "Add Team Members",
|
|
||||||
"new-notification-title": "New Notification:",
|
|
||||||
"show-unread-only": "Show Unread Only",
|
|
||||||
"mark-all-read": "Mark All Read",
|
|
||||||
"notification-popup-title": "Changes for Job #{{ro_number}}",
|
|
||||||
"ro-number": "RO #{{ro_number}}",
|
|
||||||
"no-watchers": "No Watchers",
|
|
||||||
"notification-settings-success": "Notification Settings saved successfully.",
|
|
||||||
"notification-settings-failure": "Error saving Notification Settings. {{error}}",
|
|
||||||
"watch": "Watch",
|
|
||||||
"unwatch": "Unwatch"
|
|
||||||
},
|
|
||||||
"actions": {
|
|
||||||
"remove": "Remove"
|
|
||||||
},
|
|
||||||
"aria": {
|
|
||||||
"toggle": "Toggle Watching Job"
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"job-watchers": "Job Watchers"
|
|
||||||
},
|
|
||||||
"scenarios": {
|
|
||||||
"job-assigned-to-me": "Job Assigned to Me",
|
|
||||||
"bill-posted": "Bill Posted",
|
|
||||||
"critical-parts-status-changed": "Critical Parts Status Changed",
|
|
||||||
"part-marked-back-ordered": "Part Marked Back Ordered",
|
|
||||||
"new-note-added": "New Note Added",
|
|
||||||
"supplement-imported": "Supplement Imported",
|
|
||||||
"schedule-dates-changed": "Schedule Dates Changed",
|
|
||||||
"tasks-updated-created": "Tasks Updated / Created",
|
|
||||||
"new-media-added-reassigned": "New Media Added or Reassigned",
|
|
||||||
"new-time-ticket-posted": "New Time Ticket Posted",
|
|
||||||
"intake-delivery-checklist-completed": "Intake or Delivery Checklist Completed",
|
|
||||||
"job-added-to-production": "Job Added to Production",
|
|
||||||
"job-status-change": "Job Status Changed",
|
|
||||||
"payment-collected-completed": "Payment Collected / Completed",
|
|
||||||
"alternate-transport-changed": "Alternate Transport Changed"
|
|
||||||
},
|
|
||||||
"channels": {
|
|
||||||
"app": "App",
|
|
||||||
"email": "Email",
|
|
||||||
"fcm": "Push"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,6 @@
|
|||||||
"intellipay_config": {
|
"intellipay_config": {
|
||||||
"cash_discount_percentage": "",
|
"cash_discount_percentage": "",
|
||||||
"enable_cash_discount": "",
|
"enable_cash_discount": "",
|
||||||
"payment_type": "",
|
|
||||||
"payment_map": {
|
"payment_map": {
|
||||||
"amex": "American Express",
|
"amex": "American Express",
|
||||||
"disc": "Discover",
|
"disc": "Discover",
|
||||||
@@ -344,7 +343,8 @@
|
|||||||
"jcb": "JCB",
|
"jcb": "JCB",
|
||||||
"mast": "MasterCard",
|
"mast": "MasterCard",
|
||||||
"visa": "Visa"
|
"visa": "Visa"
|
||||||
}
|
},
|
||||||
|
"payment_type": ""
|
||||||
},
|
},
|
||||||
"invoice_federal_tax_rate": "",
|
"invoice_federal_tax_rate": "",
|
||||||
"invoice_local_tax_rate": "",
|
"invoice_local_tax_rate": "",
|
||||||
@@ -601,7 +601,8 @@
|
|||||||
"templates": ""
|
"templates": ""
|
||||||
},
|
},
|
||||||
"ss_configuration": {
|
"ss_configuration": {
|
||||||
"dailyhrslimit": ""
|
"dailyhrslimit": "",
|
||||||
|
"nobusinessdays": ""
|
||||||
},
|
},
|
||||||
"ssbuckets": {
|
"ssbuckets": {
|
||||||
"color": "",
|
"color": "",
|
||||||
@@ -647,7 +648,12 @@
|
|||||||
"use_paint_scale_data": "",
|
"use_paint_scale_data": "",
|
||||||
"uselocalmediaserver": "",
|
"uselocalmediaserver": "",
|
||||||
"website": "",
|
"website": "",
|
||||||
"zip_post": ""
|
"zip_post": "",
|
||||||
|
"notifications": {
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "",
|
||||||
|
"invalid_followers": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"2tiername": "",
|
"2tiername": "",
|
||||||
@@ -727,7 +733,10 @@
|
|||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
"task-presets": "",
|
"task-presets": "",
|
||||||
"workingdays": ""
|
"workingdays": "",
|
||||||
|
"notifications": {
|
||||||
|
"followers": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
"contains": "",
|
"contains": "",
|
||||||
@@ -1235,6 +1244,7 @@
|
|||||||
"areyousure": "",
|
"areyousure": "",
|
||||||
"barcode": "código de barras",
|
"barcode": "código de barras",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
|
"changelog": "",
|
||||||
"clear": "",
|
"clear": "",
|
||||||
"confirmpassword": "",
|
"confirmpassword": "",
|
||||||
"created_at": "",
|
"created_at": "",
|
||||||
@@ -1244,6 +1254,7 @@
|
|||||||
"errors": "",
|
"errors": "",
|
||||||
"excel": "",
|
"excel": "",
|
||||||
"exceptiontitle": "",
|
"exceptiontitle": "",
|
||||||
|
"feature-request": "",
|
||||||
"friday": "",
|
"friday": "",
|
||||||
"globalsearch": "",
|
"globalsearch": "",
|
||||||
"help": "",
|
"help": "",
|
||||||
@@ -1322,9 +1333,9 @@
|
|||||||
"notfoundtitle": "",
|
"notfoundtitle": "",
|
||||||
"partnernotrunning": "",
|
"partnernotrunning": "",
|
||||||
"rbacunauth": "",
|
"rbacunauth": "",
|
||||||
|
"submit-for-testing": "",
|
||||||
"unsavedchanges": "Usted tiene cambios no guardados.",
|
"unsavedchanges": "Usted tiene cambios no guardados.",
|
||||||
"unsavedchangespopup": "",
|
"unsavedchangespopup": ""
|
||||||
"submit-for-testing": ""
|
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"dateRangeExceeded": "",
|
"dateRangeExceeded": "",
|
||||||
@@ -1631,10 +1642,13 @@
|
|||||||
"voiding": ""
|
"voiding": ""
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"estimate_sent_approval": "",
|
||||||
|
"estimate_approved": "",
|
||||||
"active_tasks": "",
|
"active_tasks": "",
|
||||||
"actual_completion": "Realización real",
|
"actual_completion": "Realización real",
|
||||||
"actual_delivery": "Entrega real",
|
"actual_delivery": "Entrega real",
|
||||||
"actual_in": "Real en",
|
"actual_in": "Real en",
|
||||||
|
"acv_amount": "",
|
||||||
"adjustment_bottom_line": "Ajustes",
|
"adjustment_bottom_line": "Ajustes",
|
||||||
"adjustmenthours": "",
|
"adjustmenthours": "",
|
||||||
"alt_transport": "",
|
"alt_transport": "",
|
||||||
@@ -1760,9 +1774,10 @@
|
|||||||
"est_ct_ln": "Apellido del tasador",
|
"est_ct_ln": "Apellido del tasador",
|
||||||
"est_ea": "Correo electrónico del tasador",
|
"est_ea": "Correo electrónico del tasador",
|
||||||
"est_ph1": "Número de teléfono del tasador",
|
"est_ph1": "Número de teléfono del tasador",
|
||||||
"flat_rate_ats": "",
|
|
||||||
"federal_tax_payable": "Impuesto federal por pagar",
|
"federal_tax_payable": "Impuesto federal por pagar",
|
||||||
"federal_tax_rate": "",
|
"federal_tax_rate": "",
|
||||||
|
"flat_rate_ats": "",
|
||||||
|
"hit_and_run": "",
|
||||||
"ins_addr1": "Dirección de Insurance Co.",
|
"ins_addr1": "Dirección de Insurance Co.",
|
||||||
"ins_city": "Ciudad de seguros",
|
"ins_city": "Ciudad de seguros",
|
||||||
"ins_co_id": "ID de la compañía de seguros",
|
"ins_co_id": "ID de la compañía de seguros",
|
||||||
@@ -1942,6 +1957,8 @@
|
|||||||
"scheddates": ""
|
"scheddates": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"sent": "",
|
||||||
|
"approved": "",
|
||||||
"accountsreceivable": "",
|
"accountsreceivable": "",
|
||||||
"act_price_ppc": "",
|
"act_price_ppc": "",
|
||||||
"actual_completion_inferred": "",
|
"actual_completion_inferred": "",
|
||||||
@@ -2316,8 +2333,8 @@
|
|||||||
"duplicate": "",
|
"duplicate": "",
|
||||||
"duplicatenolines": "",
|
"duplicatenolines": "",
|
||||||
"newcccontract": "",
|
"newcccontract": "",
|
||||||
"void": "",
|
"submit-for-testing": "",
|
||||||
"submit-for-testing": ""
|
"void": ""
|
||||||
},
|
},
|
||||||
"jobsdetail": {
|
"jobsdetail": {
|
||||||
"claimdetail": "Detalles de la reclamación",
|
"claimdetail": "Detalles de la reclamación",
|
||||||
@@ -2423,6 +2440,67 @@
|
|||||||
"updated": "Nota actualizada con éxito."
|
"updated": "Nota actualizada con éxito."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"actions": {
|
||||||
|
"remove": ""
|
||||||
|
},
|
||||||
|
"aria": {
|
||||||
|
"toggle": ""
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"app": "",
|
||||||
|
"email": "",
|
||||||
|
"fcm": ""
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"auto-add-on": "",
|
||||||
|
"auto-add-off": "",
|
||||||
|
"auto-add-success": "",
|
||||||
|
"auto-add-failure": "",
|
||||||
|
"auto-add-description": "",
|
||||||
|
"add-watchers": "",
|
||||||
|
"add-watchers-team": "",
|
||||||
|
"employee-search": "",
|
||||||
|
"mark-all-read": "",
|
||||||
|
"new-notification-title": "",
|
||||||
|
"no-watchers": "",
|
||||||
|
"notification-center": "",
|
||||||
|
"notification-popup-title": "",
|
||||||
|
"notification-settings-failure": "",
|
||||||
|
"notification-settings-success": "",
|
||||||
|
"notificationscenarios": "",
|
||||||
|
"ro-number": "",
|
||||||
|
"save": "",
|
||||||
|
"scenario": "",
|
||||||
|
"show-unread-only": "",
|
||||||
|
"teams-search": "",
|
||||||
|
"unwatch": "",
|
||||||
|
"watch": "",
|
||||||
|
"watching-issue": "",
|
||||||
|
"employee-notification": ""
|
||||||
|
},
|
||||||
|
"scenarios": {
|
||||||
|
"alternate-transport-changed": "",
|
||||||
|
"bill-posted": "",
|
||||||
|
"critical-parts-status-changed": "",
|
||||||
|
"intake-delivery-checklist-completed": "",
|
||||||
|
"job-added-to-production": "",
|
||||||
|
"job-assigned-to-me": "",
|
||||||
|
"job-status-change": "",
|
||||||
|
"new-media-added-reassigned": "",
|
||||||
|
"new-note-added": "",
|
||||||
|
"new-time-ticket-posted": "",
|
||||||
|
"part-marked-back-ordered": "",
|
||||||
|
"payment-collected-completed": "",
|
||||||
|
"schedule-dates-changed": "",
|
||||||
|
"supplement-imported": "",
|
||||||
|
"tasks-updated-created": ""
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"job-watchers": "",
|
||||||
|
"not-employee": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"labels": {
|
"labels": {
|
||||||
"noownerinfo": ""
|
"noownerinfo": ""
|
||||||
@@ -3419,6 +3497,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"dms": "",
|
"dms": "",
|
||||||
"export-logs": "",
|
"export-logs": "",
|
||||||
|
"feature-request": "",
|
||||||
"inventory": "",
|
"inventory": "",
|
||||||
"jobs": "",
|
"jobs": "",
|
||||||
"jobs-active": "",
|
"jobs-active": "",
|
||||||
@@ -3463,6 +3542,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"dms": "",
|
"dms": "",
|
||||||
"export-logs": "",
|
"export-logs": "",
|
||||||
|
"feature-request": "",
|
||||||
"imexonline": "",
|
"imexonline": "",
|
||||||
"inventory": "",
|
"inventory": "",
|
||||||
"jobs": "Todos los trabajos | {{app}}",
|
"jobs": "Todos los trabajos | {{app}}",
|
||||||
@@ -3680,10 +3760,10 @@
|
|||||||
"users": {
|
"users": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"signinerror": {
|
"signinerror": {
|
||||||
|
"auth/invalid-email": "",
|
||||||
"auth/user-disabled": "",
|
"auth/user-disabled": "",
|
||||||
"auth/user-not-found": "",
|
"auth/user-not-found": "",
|
||||||
"auth/wrong-password": "",
|
"auth/wrong-password": ""
|
||||||
"auth/invalid-email": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3783,60 +3863,6 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": ""
|
"unique_vendor_name": ""
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"labels": {
|
|
||||||
"notification-center": "",
|
|
||||||
"scenario": "",
|
|
||||||
"notificationscenarios": "",
|
|
||||||
"save": "",
|
|
||||||
"watching-issue": "",
|
|
||||||
"add-watchers": "",
|
|
||||||
"employee-search": "",
|
|
||||||
"teams-search": "",
|
|
||||||
"add-watchers-team": "",
|
|
||||||
"new-notification-title": "",
|
|
||||||
"show-unread-only": "",
|
|
||||||
"mark-all-read": "",
|
|
||||||
"notification-popup-title": "",
|
|
||||||
"ro-number": "",
|
|
||||||
"no-watchers": "",
|
|
||||||
"notification-settings-success": "",
|
|
||||||
"notification-settings-failure": "",
|
|
||||||
"watch": "",
|
|
||||||
"unwatch": ""
|
|
||||||
},
|
|
||||||
"actions": {
|
|
||||||
"remove": ""
|
|
||||||
},
|
|
||||||
"aria": {
|
|
||||||
"toggle": ""
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"job-watchers": ""
|
|
||||||
},
|
|
||||||
"scenarios": {
|
|
||||||
"job-assigned-to-me": "",
|
|
||||||
"bill-posted": "",
|
|
||||||
"critical-parts-status-changed": "",
|
|
||||||
"part-marked-back-ordered": "",
|
|
||||||
"new-note-added": "",
|
|
||||||
"supplement-imported": "",
|
|
||||||
"schedule-dates-changed": "",
|
|
||||||
"tasks-updated-created": "",
|
|
||||||
"new-media-added-reassigned": "",
|
|
||||||
"new-time-ticket-posted": "",
|
|
||||||
"intake-delivery-checklist-completed": "",
|
|
||||||
"job-added-to-production": "",
|
|
||||||
"job-status-change": "",
|
|
||||||
"payment-collected-completed": "",
|
|
||||||
"alternate-transport-changed": ""
|
|
||||||
},
|
|
||||||
"channels": {
|
|
||||||
"app": "",
|
|
||||||
"email": "",
|
|
||||||
"fcm": ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,6 @@
|
|||||||
"intellipay_config": {
|
"intellipay_config": {
|
||||||
"cash_discount_percentage": "",
|
"cash_discount_percentage": "",
|
||||||
"enable_cash_discount": "",
|
"enable_cash_discount": "",
|
||||||
"payment_type": "",
|
|
||||||
"payment_map": {
|
"payment_map": {
|
||||||
"amex": "American Express",
|
"amex": "American Express",
|
||||||
"disc": "Discover",
|
"disc": "Discover",
|
||||||
@@ -344,7 +343,8 @@
|
|||||||
"jcb": "JCB",
|
"jcb": "JCB",
|
||||||
"mast": "MasterCard",
|
"mast": "MasterCard",
|
||||||
"visa": "Visa"
|
"visa": "Visa"
|
||||||
}
|
},
|
||||||
|
"payment_type": ""
|
||||||
},
|
},
|
||||||
"invoice_federal_tax_rate": "",
|
"invoice_federal_tax_rate": "",
|
||||||
"invoice_local_tax_rate": "",
|
"invoice_local_tax_rate": "",
|
||||||
@@ -601,7 +601,8 @@
|
|||||||
"templates": ""
|
"templates": ""
|
||||||
},
|
},
|
||||||
"ss_configuration": {
|
"ss_configuration": {
|
||||||
"dailyhrslimit": ""
|
"dailyhrslimit": "",
|
||||||
|
"nobusinessdays": ""
|
||||||
},
|
},
|
||||||
"ssbuckets": {
|
"ssbuckets": {
|
||||||
"color": "",
|
"color": "",
|
||||||
@@ -647,7 +648,12 @@
|
|||||||
"use_paint_scale_data": "",
|
"use_paint_scale_data": "",
|
||||||
"uselocalmediaserver": "",
|
"uselocalmediaserver": "",
|
||||||
"website": "",
|
"website": "",
|
||||||
"zip_post": ""
|
"zip_post": "",
|
||||||
|
"notifications": {
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "",
|
||||||
|
"invalid_followers": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"2tiername": "",
|
"2tiername": "",
|
||||||
@@ -727,7 +733,10 @@
|
|||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
"task-presets": "",
|
"task-presets": "",
|
||||||
"workingdays": ""
|
"workingdays": "",
|
||||||
|
"notifications": {
|
||||||
|
"followers": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
"contains": "",
|
"contains": "",
|
||||||
@@ -1235,6 +1244,7 @@
|
|||||||
"areyousure": "",
|
"areyousure": "",
|
||||||
"barcode": "code à barre",
|
"barcode": "code à barre",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
|
"changelog": "",
|
||||||
"clear": "",
|
"clear": "",
|
||||||
"confirmpassword": "",
|
"confirmpassword": "",
|
||||||
"created_at": "",
|
"created_at": "",
|
||||||
@@ -1244,6 +1254,7 @@
|
|||||||
"errors": "",
|
"errors": "",
|
||||||
"excel": "",
|
"excel": "",
|
||||||
"exceptiontitle": "",
|
"exceptiontitle": "",
|
||||||
|
"feature-request": "",
|
||||||
"friday": "",
|
"friday": "",
|
||||||
"globalsearch": "",
|
"globalsearch": "",
|
||||||
"help": "",
|
"help": "",
|
||||||
@@ -1322,10 +1333,9 @@
|
|||||||
"notfoundtitle": "",
|
"notfoundtitle": "",
|
||||||
"partnernotrunning": "",
|
"partnernotrunning": "",
|
||||||
"rbacunauth": "",
|
"rbacunauth": "",
|
||||||
|
"submit-for-testing": "",
|
||||||
"unsavedchanges": "Vous avez des changements non enregistrés.",
|
"unsavedchanges": "Vous avez des changements non enregistrés.",
|
||||||
"unsavedchangespopup": "",
|
"unsavedchangespopup": ""
|
||||||
"submit-for-testing": ""
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"dateRangeExceeded": "",
|
"dateRangeExceeded": "",
|
||||||
@@ -1632,10 +1642,13 @@
|
|||||||
"voiding": ""
|
"voiding": ""
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"estimate_sent_approval": "",
|
||||||
|
"estimate_approved": "",
|
||||||
"active_tasks": "",
|
"active_tasks": "",
|
||||||
"actual_completion": "Achèvement réel",
|
"actual_completion": "Achèvement réel",
|
||||||
"actual_delivery": "Livraison réelle",
|
"actual_delivery": "Livraison réelle",
|
||||||
"actual_in": "En réel",
|
"actual_in": "En réel",
|
||||||
|
"acv_amount": "",
|
||||||
"adjustment_bottom_line": "Ajustements",
|
"adjustment_bottom_line": "Ajustements",
|
||||||
"adjustmenthours": "",
|
"adjustmenthours": "",
|
||||||
"alt_transport": "",
|
"alt_transport": "",
|
||||||
@@ -1761,9 +1774,10 @@
|
|||||||
"est_ct_ln": "Nom de l'évaluateur",
|
"est_ct_ln": "Nom de l'évaluateur",
|
||||||
"est_ea": "Courriel de l'évaluateur",
|
"est_ea": "Courriel de l'évaluateur",
|
||||||
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
||||||
"flat_rate_ats": "",
|
|
||||||
"federal_tax_payable": "Impôt fédéral à payer",
|
"federal_tax_payable": "Impôt fédéral à payer",
|
||||||
"federal_tax_rate": "",
|
"federal_tax_rate": "",
|
||||||
|
"flat_rate_ats": "",
|
||||||
|
"hit_and_run": "",
|
||||||
"ins_addr1": "Adresse Insurance Co.",
|
"ins_addr1": "Adresse Insurance Co.",
|
||||||
"ins_city": "Insurance City",
|
"ins_city": "Insurance City",
|
||||||
"ins_co_id": "ID de la compagnie d'assurance",
|
"ins_co_id": "ID de la compagnie d'assurance",
|
||||||
@@ -1943,6 +1957,8 @@
|
|||||||
"scheddates": ""
|
"scheddates": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"sent": "",
|
||||||
|
"approved": "",
|
||||||
"accountsreceivable": "",
|
"accountsreceivable": "",
|
||||||
"act_price_ppc": "",
|
"act_price_ppc": "",
|
||||||
"actual_completion_inferred": "",
|
"actual_completion_inferred": "",
|
||||||
@@ -2317,8 +2333,8 @@
|
|||||||
"duplicate": "",
|
"duplicate": "",
|
||||||
"duplicatenolines": "",
|
"duplicatenolines": "",
|
||||||
"newcccontract": "",
|
"newcccontract": "",
|
||||||
"void": "",
|
"submit-for-testing": "",
|
||||||
"submit-for-testing": ""
|
"void": ""
|
||||||
},
|
},
|
||||||
"jobsdetail": {
|
"jobsdetail": {
|
||||||
"claimdetail": "Détails de la réclamation",
|
"claimdetail": "Détails de la réclamation",
|
||||||
@@ -2424,6 +2440,67 @@
|
|||||||
"updated": "Remarque mise à jour avec succès."
|
"updated": "Remarque mise à jour avec succès."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"actions": {
|
||||||
|
"remove": ""
|
||||||
|
},
|
||||||
|
"aria": {
|
||||||
|
"toggle": ""
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"app": "",
|
||||||
|
"email": "",
|
||||||
|
"fcm": ""
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"auto-add-on": "",
|
||||||
|
"auto-add-off": "",
|
||||||
|
"auto-add-success": "",
|
||||||
|
"auto-add-failure": "",
|
||||||
|
"auto-add-description": "",
|
||||||
|
"add-watchers": "",
|
||||||
|
"add-watchers-team": "",
|
||||||
|
"employee-search": "",
|
||||||
|
"mark-all-read": "",
|
||||||
|
"new-notification-title": "",
|
||||||
|
"no-watchers": "",
|
||||||
|
"notification-center": "",
|
||||||
|
"notification-popup-title": "",
|
||||||
|
"notification-settings-failure": "",
|
||||||
|
"notification-settings-success": "",
|
||||||
|
"notificationscenarios": "",
|
||||||
|
"ro-number": "",
|
||||||
|
"save": "",
|
||||||
|
"scenario": "",
|
||||||
|
"show-unread-only": "",
|
||||||
|
"teams-search": "",
|
||||||
|
"unwatch": "",
|
||||||
|
"watch": "",
|
||||||
|
"watching-issue": "",
|
||||||
|
"employee-notification": ""
|
||||||
|
},
|
||||||
|
"scenarios": {
|
||||||
|
"alternate-transport-changed": "",
|
||||||
|
"bill-posted": "",
|
||||||
|
"critical-parts-status-changed": "",
|
||||||
|
"intake-delivery-checklist-completed": "",
|
||||||
|
"job-added-to-production": "",
|
||||||
|
"job-assigned-to-me": "",
|
||||||
|
"job-status-change": "",
|
||||||
|
"new-media-added-reassigned": "",
|
||||||
|
"new-note-added": "",
|
||||||
|
"new-time-ticket-posted": "",
|
||||||
|
"part-marked-back-ordered": "",
|
||||||
|
"payment-collected-completed": "",
|
||||||
|
"schedule-dates-changed": "",
|
||||||
|
"supplement-imported": "",
|
||||||
|
"tasks-updated-created": ""
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"job-watchers": "",
|
||||||
|
"not-employee": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"labels": {
|
"labels": {
|
||||||
"noownerinfo": ""
|
"noownerinfo": ""
|
||||||
@@ -3420,6 +3497,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"dms": "",
|
"dms": "",
|
||||||
"export-logs": "",
|
"export-logs": "",
|
||||||
|
"feature-request": "",
|
||||||
"inventory": "",
|
"inventory": "",
|
||||||
"jobs": "",
|
"jobs": "",
|
||||||
"jobs-active": "",
|
"jobs-active": "",
|
||||||
@@ -3464,6 +3542,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"dms": "",
|
"dms": "",
|
||||||
"export-logs": "",
|
"export-logs": "",
|
||||||
|
"feature-request": "",
|
||||||
"imexonline": "",
|
"imexonline": "",
|
||||||
"inventory": "",
|
"inventory": "",
|
||||||
"jobs": "Tous les emplois | {{app}}",
|
"jobs": "Tous les emplois | {{app}}",
|
||||||
@@ -3681,10 +3760,10 @@
|
|||||||
"users": {
|
"users": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"signinerror": {
|
"signinerror": {
|
||||||
|
"auth/invalid-email": "",
|
||||||
"auth/user-disabled": "",
|
"auth/user-disabled": "",
|
||||||
"auth/user-not-found": "",
|
"auth/user-not-found": "",
|
||||||
"auth/wrong-password": "",
|
"auth/wrong-password": ""
|
||||||
"auth/invalid-email": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3784,60 +3863,6 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": ""
|
"unique_vendor_name": ""
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"labels": {
|
|
||||||
"notification-center": "",
|
|
||||||
"scenario": "",
|
|
||||||
"notificationscenarios": "",
|
|
||||||
"save": "",
|
|
||||||
"watching-issue": "",
|
|
||||||
"add-watchers": "",
|
|
||||||
"employee-search": "",
|
|
||||||
"teams-search": "",
|
|
||||||
"add-watchers-team": "",
|
|
||||||
"new-notification-title": "",
|
|
||||||
"show-unread-only": "",
|
|
||||||
"mark-all-read": "",
|
|
||||||
"notification-popup-title": "",
|
|
||||||
"ro-number": "",
|
|
||||||
"no-watchers": "",
|
|
||||||
"notification-settings-success": "",
|
|
||||||
"notification-settings-failure": "",
|
|
||||||
"watch": "",
|
|
||||||
"unwatch": ""
|
|
||||||
},
|
|
||||||
"actions": {
|
|
||||||
"remove": ""
|
|
||||||
},
|
|
||||||
"aria": {
|
|
||||||
"toggle": ""
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"job-watchers": ""
|
|
||||||
},
|
|
||||||
"scenarios": {
|
|
||||||
"job-assigned-to-me": "",
|
|
||||||
"bill-posted": "",
|
|
||||||
"critical-parts-status-changed": "",
|
|
||||||
"part-marked-back-ordered": "",
|
|
||||||
"new-note-added": "",
|
|
||||||
"supplement-imported": "",
|
|
||||||
"schedule-dates-changed": "",
|
|
||||||
"tasks-updated-created": "",
|
|
||||||
"new-media-added-reassigned": "",
|
|
||||||
"new-time-ticket-posted": "",
|
|
||||||
"intake-delivery-checklist-completed": "",
|
|
||||||
"job-added-to-production": "",
|
|
||||||
"job-status-change": "",
|
|
||||||
"payment-collected-completed": "",
|
|
||||||
"alternate-transport-changed": ""
|
|
||||||
},
|
|
||||||
"channels": {
|
|
||||||
"app": "",
|
|
||||||
"email": "",
|
|
||||||
"fcm": ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ const AuditTrailMapping = {
|
|||||||
jobchecklist: (type, inproduction, status) =>
|
jobchecklist: (type, inproduction, status) =>
|
||||||
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
|
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
|
||||||
jobconverted: (ro_number) => i18n.t("audit_trail.messages.jobconverted", { ro_number }),
|
jobconverted: (ro_number) => i18n.t("audit_trail.messages.jobconverted", { ro_number }),
|
||||||
jobintake: (status, email, scheduled_completion) =>
|
jobintake: (status, scheduled_completion) =>
|
||||||
i18n.t("audit_trail.messages.jobintake", { status, email, scheduled_completion }),
|
i18n.t("audit_trail.messages.jobintake", { status, scheduled_completion }),
|
||||||
jobdelivery: (status, email, actual_completion) =>
|
jobdelivery: (status, email, actual_completion) =>
|
||||||
i18n.t("audit_trail.messages.jobdelivery", { status, email, actual_completion }),
|
i18n.t("audit_trail.messages.jobdelivery", { status, email, actual_completion }),
|
||||||
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
|
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ const onServiceWorkerUpdate = (registration) => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
window.open(
|
window.open(
|
||||||
InstanceRenderManager({
|
`https://shopmanagement.canny.io/changelog`,
|
||||||
imex: "https://imex-online.noticeable.news/",
|
|
||||||
rome: "https://rome-online.noticeable.news/"
|
|
||||||
}),
|
|
||||||
"_blank"
|
"_blank"
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const currentDatePST = new Date()
|
|||||||
.reverse()
|
.reverse()
|
||||||
.join("-");
|
.join("-");
|
||||||
const sentryRelease =
|
const sentryRelease =
|
||||||
`${import.meta.env.VITE_APP_IS_TEST ? "test" : "production"}-${currentDatePST}-${process.env.VITE_GIT_COMMIT_HASH}`.trim();
|
`${import.meta.env.VITE_APP_IS_TEST ? "test" : "production"}-${currentDatePST}`.trim();
|
||||||
|
|
||||||
if (!import.meta.env.DEV) {
|
if (!import.meta.env.DEV) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
|
|||||||
19
client/src/utils/useIsEmployee.js
Normal file
19
client/src/utils/useIsEmployee.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user is an employee of the bodyshop
|
||||||
|
* @param bodyshop
|
||||||
|
* @param userOrEmail
|
||||||
|
* @returns {boolean|*}
|
||||||
|
*/
|
||||||
|
export function useIsEmployee(bodyshop, userOrEmail) {
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!bodyshop || !bodyshop.employees) return false;
|
||||||
|
|
||||||
|
// Handle both user object and email string
|
||||||
|
const email = typeof userOrEmail === "string" ? userOrEmail : userOrEmail?.email;
|
||||||
|
if (!email) return false;
|
||||||
|
|
||||||
|
return bodyshop.employees.some((employee) => employee.user_email === email);
|
||||||
|
}, [bodyshop, userOrEmail]);
|
||||||
|
}
|
||||||
@@ -216,6 +216,7 @@
|
|||||||
- id
|
- id
|
||||||
- kanban_settings
|
- kanban_settings
|
||||||
- notification_settings
|
- notification_settings
|
||||||
|
- notifications_autoadd
|
||||||
- qbo_realmId
|
- qbo_realmId
|
||||||
- shopid
|
- shopid
|
||||||
- useremail
|
- useremail
|
||||||
@@ -232,6 +233,7 @@
|
|||||||
- default_prod_list_view
|
- default_prod_list_view
|
||||||
- kanban_settings
|
- kanban_settings
|
||||||
- notification_settings
|
- notification_settings
|
||||||
|
- notifications_autoadd
|
||||||
- qbo_realmId
|
- qbo_realmId
|
||||||
filter:
|
filter:
|
||||||
user:
|
user:
|
||||||
@@ -1002,6 +1004,7 @@
|
|||||||
- md_tasks_presets
|
- md_tasks_presets
|
||||||
- md_to_emails
|
- md_to_emails
|
||||||
- messagingservicesid
|
- messagingservicesid
|
||||||
|
- notification_followers
|
||||||
- pbs_configuration
|
- pbs_configuration
|
||||||
- pbs_serialnumber
|
- pbs_serialnumber
|
||||||
- phone
|
- phone
|
||||||
@@ -1104,6 +1107,7 @@
|
|||||||
- md_ro_statuses
|
- md_ro_statuses
|
||||||
- md_tasks_presets
|
- md_tasks_presets
|
||||||
- md_to_emails
|
- md_to_emails
|
||||||
|
- notification_followers
|
||||||
- pbs_configuration
|
- pbs_configuration
|
||||||
- phone
|
- phone
|
||||||
- prodtargethrs
|
- prodtargethrs
|
||||||
@@ -3595,6 +3599,7 @@
|
|||||||
- actual_completion
|
- actual_completion
|
||||||
- actual_delivery
|
- actual_delivery
|
||||||
- actual_in
|
- actual_in
|
||||||
|
- acv_amount
|
||||||
- adj_g_disc
|
- adj_g_disc
|
||||||
- adj_strdis
|
- adj_strdis
|
||||||
- adj_towdis
|
- adj_towdis
|
||||||
@@ -3697,9 +3702,12 @@
|
|||||||
- est_ph1
|
- est_ph1
|
||||||
- est_st
|
- est_st
|
||||||
- est_zip
|
- est_zip
|
||||||
|
- estimate_approved
|
||||||
|
- estimate_sent_approval
|
||||||
- federal_tax_rate
|
- federal_tax_rate
|
||||||
- flat_rate_ats
|
- flat_rate_ats
|
||||||
- g_bett_amt
|
- g_bett_amt
|
||||||
|
- hit_and_run
|
||||||
- id
|
- id
|
||||||
- inproduction
|
- inproduction
|
||||||
- ins_addr1
|
- ins_addr1
|
||||||
@@ -3866,6 +3874,7 @@
|
|||||||
- actual_completion
|
- actual_completion
|
||||||
- actual_delivery
|
- actual_delivery
|
||||||
- actual_in
|
- actual_in
|
||||||
|
- acv_amount
|
||||||
- adj_g_disc
|
- adj_g_disc
|
||||||
- adj_strdis
|
- adj_strdis
|
||||||
- adj_towdis
|
- adj_towdis
|
||||||
@@ -3969,9 +3978,12 @@
|
|||||||
- est_ph1
|
- est_ph1
|
||||||
- est_st
|
- est_st
|
||||||
- est_zip
|
- est_zip
|
||||||
|
- estimate_approved
|
||||||
|
- estimate_sent_approval
|
||||||
- federal_tax_rate
|
- federal_tax_rate
|
||||||
- flat_rate_ats
|
- flat_rate_ats
|
||||||
- g_bett_amt
|
- g_bett_amt
|
||||||
|
- hit_and_run
|
||||||
- id
|
- id
|
||||||
- inproduction
|
- inproduction
|
||||||
- ins_addr1
|
- ins_addr1
|
||||||
@@ -4150,6 +4162,7 @@
|
|||||||
- actual_completion
|
- actual_completion
|
||||||
- actual_delivery
|
- actual_delivery
|
||||||
- actual_in
|
- actual_in
|
||||||
|
- acv_amount
|
||||||
- adj_g_disc
|
- adj_g_disc
|
||||||
- adj_strdis
|
- adj_strdis
|
||||||
- adj_towdis
|
- adj_towdis
|
||||||
@@ -4253,9 +4266,12 @@
|
|||||||
- est_ph1
|
- est_ph1
|
||||||
- est_st
|
- est_st
|
||||||
- est_zip
|
- est_zip
|
||||||
|
- estimate_approved
|
||||||
|
- estimate_sent_approval
|
||||||
- federal_tax_rate
|
- federal_tax_rate
|
||||||
- flat_rate_ats
|
- flat_rate_ats
|
||||||
- g_bett_amt
|
- g_bett_amt
|
||||||
|
- hit_and_run
|
||||||
- id
|
- id
|
||||||
- inproduction
|
- inproduction
|
||||||
- ins_addr1
|
- ins_addr1
|
||||||
@@ -4582,12 +4598,34 @@
|
|||||||
request_transform:
|
request_transform:
|
||||||
body:
|
body:
|
||||||
action: transform
|
action: transform
|
||||||
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.old.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.old.employee_prep}},\r\n \"clm_total\": {{$body.event.data.old.clm_total}},\r\n \"towin\": {{$body.event.data.old.towin}},\r\n \"employee_body\": {{$body.event.data.old.employee_body}},\r\n \"converted\": {{$body.event.data.old.converted}},\r\n \"scheduled_in\": {{$body.event.data.old.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.old.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.old.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.old.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.old.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.old.alt_transport}},\r\n \"date_exported\": {{$body.event.data.old.date_exported}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"employee_csr\": {{$body.event.data.old.employee_csr}},\r\n \"actual_in\": {{$body.event.data.old.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.old.deliverchecklist}},\r\n \"comment\": {{$body.event.data.old.comment}},\r\n \"employee_refinish\": {{$body.event.data.old.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.old.inproduction}},\r\n \"production_vars\": {{$body.event.data.old.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.old.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.old.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.old.date_invoiced}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.new.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.new.employee_prep}},\r\n \"clm_total\": {{$body.event.data.new.clm_total}},\r\n \"towin\": {{$body.event.data.new.towin}},\r\n \"employee_body\": {{$body.event.data.new.employee_body}},\r\n \"converted\": {{$body.event.data.new.converted}},\r\n \"scheduled_in\": {{$body.event.data.new.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.new.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.new.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.new.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.new.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.new.alt_transport}},\r\n \"date_exported\": {{$body.event.data.new.date_exported}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"employee_csr\": {{$body.event.data.new.employee_csr}},\r\n \"actual_in\": {{$body.event.data.new.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.new.deliverchecklist}},\r\n \"comment\": {{$body.event.data.new.comment}},\r\n \"employee_refinish\": {{$body.event.data.new.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.new.inproduction}},\r\n \"production_vars\": {{$body.event.data.new.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.new.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.new.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.new.date_invoiced}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n"
|
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": {{$body.event.op}},\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.old.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.old.employee_prep}},\r\n \"clm_total\": {{$body.event.data.old.clm_total}},\r\n \"towin\": {{$body.event.data.old.towin}},\r\n \"employee_body\": {{$body.event.data.old.employee_body}},\r\n \"converted\": {{$body.event.data.old.converted}},\r\n \"scheduled_in\": {{$body.event.data.old.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.old.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.old.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.old.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.old.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.old.alt_transport}},\r\n \"date_exported\": {{$body.event.data.old.date_exported}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"employee_csr\": {{$body.event.data.old.employee_csr}},\r\n \"actual_in\": {{$body.event.data.old.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.old.deliverchecklist}},\r\n \"comment\": {{$body.event.data.old.comment}},\r\n \"employee_refinish\": {{$body.event.data.old.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.old.inproduction}},\r\n \"production_vars\": {{$body.event.data.old.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.old.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.old.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.old.date_invoiced}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.new.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.new.employee_prep}},\r\n \"clm_total\": {{$body.event.data.new.clm_total}},\r\n \"towin\": {{$body.event.data.new.towin}},\r\n \"employee_body\": {{$body.event.data.new.employee_body}},\r\n \"converted\": {{$body.event.data.new.converted}},\r\n \"scheduled_in\": {{$body.event.data.new.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.new.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.new.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.new.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.new.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.new.alt_transport}},\r\n \"date_exported\": {{$body.event.data.new.date_exported}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"employee_csr\": {{$body.event.data.new.employee_csr}},\r\n \"actual_in\": {{$body.event.data.new.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.new.deliverchecklist}},\r\n \"comment\": {{$body.event.data.new.comment}},\r\n \"employee_refinish\": {{$body.event.data.new.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.new.inproduction}},\r\n \"production_vars\": {{$body.event.data.new.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.new.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.new.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.new.date_invoiced}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n"
|
||||||
method: POST
|
method: POST
|
||||||
query_params: {}
|
query_params: {}
|
||||||
template_engine: Kriti
|
template_engine: Kriti
|
||||||
url: '{{$base_url}}/notifications/events/handleJobsChange'
|
url: '{{$base_url}}/notifications/events/handleJobsChange'
|
||||||
version: 2
|
version: 2
|
||||||
|
- name: notifications_jobs_autoadd
|
||||||
|
definition:
|
||||||
|
enable_manual: false
|
||||||
|
insert:
|
||||||
|
columns: '*'
|
||||||
|
retry_conf:
|
||||||
|
interval_sec: 10
|
||||||
|
num_retries: 0
|
||||||
|
timeout_sec: 60
|
||||||
|
webhook_from_env: HASURA_API_URL
|
||||||
|
headers:
|
||||||
|
- name: event-secret
|
||||||
|
value_from_env: EVENT_SECRET
|
||||||
|
request_transform:
|
||||||
|
body:
|
||||||
|
action: transform
|
||||||
|
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": {{$body.event.op}},\r\n \"data\": {\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"shopid\": {{$body.event.data.new?.shopid}},\r\n \"ro_number\": {{$body.event.data.new?.ro_number}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs_autoadd\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n"
|
||||||
|
method: POST
|
||||||
|
query_params: {}
|
||||||
|
template_engine: Kriti
|
||||||
|
url: '{{$base_url}}/notifications/events/handleAutoAdd'
|
||||||
|
version: 2
|
||||||
- name: os_jobs
|
- name: os_jobs
|
||||||
definition:
|
definition:
|
||||||
delete:
|
delete:
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."jobs" add column "hit_and_run" boolean
|
||||||
|
-- null default 'false';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "hit_and_run" boolean
|
||||||
|
null default 'false';
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."jobs" add column "acv_amount" numeric
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "acv_amount" numeric
|
||||||
|
null;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."bodyshops" add column "notification_followers" json
|
||||||
|
-- null default json_build_object();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."bodyshops" add column "notification_followers" json
|
||||||
|
null default json_build_object();
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."associations" add column "notifications_autoadd" boolean
|
||||||
|
-- null default 'false';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."associations" add column "notifications_autoadd" boolean
|
||||||
|
null default 'false';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."bodyshops" alter column "notification_followers" set default json_build_object();
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."bodyshops" alter column "notification_followers" set default json_build_array();
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."jobs" add column "estimate_sent_approval" timestamptz
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "estimate_sent_approval" timestamptz
|
||||||
|
null;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."jobs" add column "estimate_approved" timestamptz
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "estimate_approved" timestamptz
|
||||||
|
null;
|
||||||
4990
package-lock.json
generated
4990
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0",
|
"node": ">=22.13.0",
|
||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -16,60 +16,56 @@
|
|||||||
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.782.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.808.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.782.0",
|
"@aws-sdk/client-elasticache": "^3.808.0",
|
||||||
"@aws-sdk/client-s3": "^3.782.0",
|
"@aws-sdk/client-s3": "^3.808.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.782.0",
|
"@aws-sdk/client-secrets-manager": "^3.808.0",
|
||||||
"@aws-sdk/client-ses": "^3.782.0",
|
"@aws-sdk/client-ses": "^3.808.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.782.0",
|
"@aws-sdk/credential-provider-node": "^3.808.0",
|
||||||
"@aws-sdk/lib-storage": "^3.782.0",
|
"@aws-sdk/lib-storage": "^3.808.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.782.0",
|
"@aws-sdk/s3-request-presigner": "^3.808.0",
|
||||||
"@opensearch-project/opensearch": "^2.13.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"aws4": "^1.13.2",
|
"aws4": "^1.13.2",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"bee-queue": "^1.7.1",
|
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bluebird": "^3.7.2",
|
"bullmq": "^5.52.2",
|
||||||
"bullmq": "^5.48.0",
|
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"cloudinary": "^2.6.0",
|
"cloudinary": "^2.6.1",
|
||||||
"compression": "^1.8.0",
|
"compression": "^1.8.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"crisp-status-reporter": "^1.2.2",
|
"crisp-status-reporter": "^1.2.2",
|
||||||
"csrf": "^3.1.0",
|
"dd-trace": "^5.51.0",
|
||||||
"dd-trace": "^5.45.0",
|
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"firebase-admin": "^13.2.0",
|
"firebase-admin": "^13.4.0",
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.11.0",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"inline-css": "^4.0.3",
|
|
||||||
"intuit-oauth": "^4.2.0",
|
"intuit-oauth": "^4.2.0",
|
||||||
"ioredis": "^5.6.0",
|
"ioredis": "^5.6.0",
|
||||||
"json-2-csv": "^5.5.9",
|
"json-2-csv": "^5.5.9",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"juice": "^11.0.1",
|
"juice": "^11.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.48",
|
"moment-timezone": "^0.5.48",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-mailjet": "^6.0.8",
|
|
||||||
"node-persist": "^4.0.4",
|
"node-persist": "^4.0.4",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
"phone": "^3.1.58",
|
"phone": "^3.1.58",
|
||||||
|
"query-string": "^9.1.2",
|
||||||
"recursive-diff": "^1.0.9",
|
"recursive-diff": "^1.0.9",
|
||||||
"redis": "^4.7.0",
|
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"skia-canvas": "^2.0.2",
|
"skia-canvas": "^2.0.2",
|
||||||
"soap": "^1.1.10",
|
"soap": "^1.1.10",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-adapter": "^2.5.5",
|
"socket.io-adapter": "^2.5.5",
|
||||||
"ssh2-sftp-client": "^11.0.0",
|
"ssh2-sftp-client": "^11.0.0",
|
||||||
"twilio": "^5.5.2",
|
"twilio": "^5.6.1",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-cloudwatch": "^6.3.0",
|
"winston-cloudwatch": "^6.3.0",
|
||||||
@@ -77,16 +73,14 @@
|
|||||||
"xmlbuilder2": "^3.1.1"
|
"xmlbuilder2": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.24.0",
|
"@eslint/js": "^9.26.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"eslint": "^9.26.0",
|
||||||
"eslint": "^9.24.0",
|
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"mock-require": "^3.0.3",
|
"mock-require": "^3.0.3",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"source-map-explorer": "^2.5.2",
|
"supertest": "^7.1.1",
|
||||||
"supertest": "^7.1.0",
|
"vitest": "^3.1.3"
|
||||||
"vitest": "^3.1.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
server.js
11
server.js
@@ -4,6 +4,7 @@ require("dotenv").config({
|
|||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Commented out due to stability issues
|
||||||
if (process.env.NODE_ENV) {
|
if (process.env.NODE_ENV) {
|
||||||
require("dd-trace").init({
|
require("dd-trace").init({
|
||||||
profiling: true,
|
profiling: true,
|
||||||
@@ -33,7 +34,6 @@ const {
|
|||||||
DescribeReplicationGroupsCommand
|
DescribeReplicationGroupsCommand
|
||||||
} = require("@aws-sdk/client-elasticache");
|
} = require("@aws-sdk/client-elasticache");
|
||||||
const { InstanceRegion } = require("./server/utils/instanceMgr");
|
const { InstanceRegion } = require("./server/utils/instanceMgr");
|
||||||
const StartStatusReporter = require("./server/utils/statusReporter");
|
|
||||||
const { registerCleanupTask, initializeCleanupManager } = require("./server/utils/cleanupManager");
|
const { registerCleanupTask, initializeCleanupManager } = require("./server/utils/cleanupManager");
|
||||||
|
|
||||||
const { loadEmailQueue } = require("./server/notifications/queues/emailQueue");
|
const { loadEmailQueue } = require("./server/notifications/queues/emailQueue");
|
||||||
@@ -117,6 +117,8 @@ const applyRoutes = ({ app }) => {
|
|||||||
app.use("/cdk", require("./server/routes/cdkRoutes"));
|
app.use("/cdk", require("./server/routes/cdkRoutes"));
|
||||||
app.use("/csi", require("./server/routes/csiRoutes"));
|
app.use("/csi", require("./server/routes/csiRoutes"));
|
||||||
app.use("/payroll", require("./server/routes/payrollRoutes"));
|
app.use("/payroll", require("./server/routes/payrollRoutes"));
|
||||||
|
app.use("/sso", require("./server/routes/ssoRoutes"));
|
||||||
|
app.use("/integrations", require("./server/routes/intergrationRoutes"));
|
||||||
|
|
||||||
// Default route for forbidden access
|
// Default route for forbidden access
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
@@ -390,13 +392,6 @@ const main = async () => {
|
|||||||
applyRoutes({ app });
|
applyRoutes({ app });
|
||||||
redisSocketEvents({ io: ioRedis, redisHelpers, ioHelpers, logger });
|
redisSocketEvents({ io: ioRedis, redisHelpers, ioHelpers, logger });
|
||||||
|
|
||||||
const StatusReporter = StartStatusReporter();
|
|
||||||
registerCleanupTask(async () => {
|
|
||||||
if (isFunction(StatusReporter?.end)) {
|
|
||||||
StatusReporter.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await server.listen(port);
|
await server.listen(port);
|
||||||
logger.log(`Server started on port ${port}`, "INFO", "api");
|
logger.log(`Server started on port ${port}`, "INFO", "api");
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ exports.PbsExportAp = async function (socket, { billids, txEnvelope }) {
|
|||||||
|
|
||||||
socket.emit("ap-export-success", billid);
|
socket.emit("ap-export-success", billid);
|
||||||
} else {
|
} else {
|
||||||
CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`);
|
CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
|
||||||
socket.emit("ap-export-failure", {
|
socket.emit("ap-export-failure", {
|
||||||
billid,
|
billid,
|
||||||
error: AccountPostingChange.Message
|
error: AccountPostingChange.Message
|
||||||
|
|||||||
@@ -105,14 +105,14 @@ exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selecte
|
|||||||
|
|
||||||
socket.emit("export-success", socket.JobData.id);
|
socket.emit("export-success", socket.JobData.id);
|
||||||
} else {
|
} else {
|
||||||
CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`);
|
CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
|
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
|
||||||
await InsertFailedExportLog(socket, error);
|
await InsertFailedExportLog(socket, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Was Successful
|
||||||
async function CheckForErrors(socket, response) {
|
async function CheckForErrors(socket, response) {
|
||||||
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
|
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
|
||||||
CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
|
CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ exports.default = async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "PRODUCTION") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
const moment = require("moment");
|
|
||||||
const { default: RenderInstanceManager } = require("../utils/instanceMgr");
|
|
||||||
const { header, end, start } = require("./html");
|
const { header, end, start } = require("./html");
|
||||||
|
|
||||||
// Required Strings
|
// Required Strings
|
||||||
@@ -7,19 +5,6 @@ const { header, end, start } = require("./html");
|
|||||||
// - subHeader - The subheader of the email
|
// - subHeader - The subheader of the email
|
||||||
// - body - The body of the email
|
// - body - The body of the email
|
||||||
|
|
||||||
// Optional Strings (Have default values)
|
|
||||||
// - footer - The footer of the email
|
|
||||||
// - dateLine - The date line of the email
|
|
||||||
|
|
||||||
const defaultFooter = () => {
|
|
||||||
return RenderInstanceManager({
|
|
||||||
imex: "ImEX Online Collision Repair Management System",
|
|
||||||
rome: "Rome Technologies"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const now = () => moment().format("MM/DD/YYYY @ hh:mm a");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate the email template
|
* Generate the email template
|
||||||
* @param strings
|
* @param strings
|
||||||
@@ -32,81 +17,48 @@ const generateEmailTemplate = (strings) => {
|
|||||||
header +
|
header +
|
||||||
start +
|
start +
|
||||||
`
|
`
|
||||||
<table class="row">
|
<!-- Report Title -->
|
||||||
<tbody>
|
${
|
||||||
<tr>
|
strings.header &&
|
||||||
<th class="small-12 large-12 columns first last">
|
`
|
||||||
<table>
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
<tbody>
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 8px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
<tr>
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
<td>
|
<h6 style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; color: inherit; word-wrap: normal; font-weight: normal; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 23px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; text-align: center;"><strong style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">${strings.header}</strong></h6>
|
||||||
<h6 style="text-align:left"><strong>${strings.header}</strong></h6>
|
</td></tr>
|
||||||
</td>
|
</tbody></table></th>
|
||||||
</tr>
|
</tr></tbody></table>
|
||||||
<tr>
|
`
|
||||||
<td>
|
}
|
||||||
<p style="font-size:90%">${strings.subHeader}</p>
|
${
|
||||||
</td>
|
strings.subHeader &&
|
||||||
</tr>
|
`
|
||||||
</tbody>
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
</table>
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
</th>
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
</tr>
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 95%;">${strings.subHeader}</p>
|
||||||
</tbody>
|
</td></tr>
|
||||||
</table>
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
`
|
||||||
|
}
|
||||||
<!-- End Report Title -->
|
<!-- End Report Title -->
|
||||||
<!-- Task Detail -->
|
${
|
||||||
<table class="row">
|
strings.body &&
|
||||||
<tbody>
|
`
|
||||||
<tr>
|
<!-- Report Detail -->
|
||||||
<th class="small-12 large-12 columns first last">
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
<table>
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
<tbody>
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
<tr>
|
${strings.body}
|
||||||
<td>${strings.body}</td>
|
</td></tr>
|
||||||
</tr>
|
</tbody></table></th>
|
||||||
</tbody>
|
</tr></tbody></table>
|
||||||
</table>
|
<!-- End Report Detail -->
|
||||||
</th>
|
`
|
||||||
</tr>
|
}
|
||||||
</tbody>
|
` +
|
||||||
</table>
|
end(strings.dateLine)
|
||||||
<!-- End Task Detail -->
|
|
||||||
<!-- Footer -->
|
|
||||||
<table class="row collapsed footer" id="non-printable">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th class="small-3 large-3 columns first">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><p style="font-size:70%; padding-right:10px">${strings?.dateLine || now()}</p></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</th>
|
|
||||||
<th class="small-6 large-6 columns">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><p style="font-size:70%; text-align:center">${strings?.footer || defaultFooter()}</p></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</th>
|
|
||||||
<th class="small-3 large-3 columns last">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><p style="font-size:70%"> </p></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>` +
|
|
||||||
end
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
2765
server/email/html.js
2765
server/email/html.js
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user