Compare commits

..

22 Commits

Author SHA1 Message Date
Allan Carr
831802f5af IO-3235 FeatureAccess on VisualBoard for SmartSchedule Option of Color Cards
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-05-20 15:25:29 -07:00
Allan Carr
6005eaee6a Merged in feature/IO-3217-OTSL-Labor-Type (pull request #2319)
IO-3217 OTSL Labor Type
2025-05-19 21:02:01 +00:00
Patrick Fic
c069600cfd Merged in hotfix/2025-05-15 (pull request #2317)
Hotfix/2025 05 15 IO-3217 IO-3066 IO-3210 IO-2328
2025-05-15 22:47:26 +00:00
Patrick Fic
186cbf2c97 Merge branch 'feature/IO-3066-ems-upload' into hotfix/2025-05-15 2025-05-15 15:47:04 -07:00
Patrick Fic
392988ae11 Io-3066 resolve typo. 2025-05-15 15:46:45 -07:00
Allan Carr
2e33b79eb9 Merged in feature/IO-3210-Podium-Datapump (pull request #2315)
IO-3210 Podium Datapump

Approved-by: Patrick Fic
2025-05-15 22:39:39 +00:00
Patrick Fic
fa99ef7b37 Merge branch 'feature/IO-3066-ems-upload' into hotfix/2025-05-15 2025-05-15 15:36:44 -07:00
Patrick Fic
c4aff1b516 Merge branch 'feature/IO-2328-intellipay-querystring-package' into hotfix/2025-05-15 2025-05-15 15:36:31 -07:00
Patrick Fic
61276bb2d1 IO-2328 change querystring versions. 2025-05-15 15:35:56 -07:00
Allan Carr
8b89e2eb9d IO-3210 Podium Datapump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-05-15 15:27:59 -07:00
Patrick Fic
9ab41308e7 IO-3066 add EMS upload functionality. 2025-05-15 12:53:41 -07:00
Allan Carr
f76052ec9b Merge branch 'master-AIO' into feature/IO-3210-Podium-Datapump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-05-15 12:41:09 -07:00
Patrick Fic
b8841e3ded Merged in release/2025-05-09 (pull request #2312)
IO-3190 Add nulll coalesce and check for non-intake events.

Approved-by: Patrick Fic
2025-05-15 00:57:44 +00:00
Patrick Fic
a49b3f6496 IO-3190 Add nulll coalesce and check for non-intake events. 2025-05-14 17:56:15 -07:00
Dave Richer
3e17ec3cf8 Merged in release/2025-05-09 (pull request #2310)
[DO NOT MERGE ] Release/2025-05-09 into master-AIO
2025-05-14 20:10:07 +00:00
Dave Richer
76c0c7c41e release/2025-05-09 - Bump Deps 2025-05-13 10:43:15 -04:00
Dave Richer
025b986f60 Merged in feature/IO-3228-Notifications-1.6-and-Deprecations (pull request #2308)
feature/IO-3228-Notifications-1.6-and-Deprecations
2025-05-09 14:39:59 +00:00
Dave Richer
6e6addd62f feature/IO-3228-Notifications-1.6-and-Deprecations
- See Ticket for full details (Notifications restrictions, AntD deprecations)
2025-05-09 10:38:19 -04:00
Dave Richer
266c3acf34 Merged in feature/IO-3214-Job-Status-Card-Extension (pull request #2306)
feature/IO-3214-Job-Status-Card-Extension - PR Notes/Package Updates
2025-05-08 15:27:53 +00:00
Dave Richer
ca18291425 Merged in feature/IO-3214-Job-Status-Card-Extension (pull request #2304)
feature/IO-3214-Job-Status-Card-Extension - Complete
2025-05-06 21:10:52 +00:00
Dave Richer
a9814c1eb1 Merged in release/2025-04-25 (pull request #2287)
Release/2025-04-25 into Master-AIO -  IO-2282, IO-3066, IO-3164, IO-3187, IO-3190, IO-3200, IO-3210, IO-3212, IO-3213, IO-3215, IO-3220, IO-3223
2025-04-26 00:43:22 +00:00
Allan Carr
12c87ed689 IO-3217 OTSL Labor Type
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-23 18:48:35 -07:00
49 changed files with 667 additions and 529 deletions

110
client/package-lock.json generated
View File

@@ -21,13 +21,13 @@
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.8.1",
"@sentry/cli": "^2.45.0",
"@sentry/react": "^9.17.0",
"@sentry/react": "^9.18.0",
"@sentry/vite-plugin": "^3.4.0",
"@splitsoftware/splitio-react": "^2.1.1",
"@tanem/react-nprogress": "^5.0.53",
"antd": "^5.25.0",
"antd": "^5.25.1",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^4.2.0",
"apollo-link-sentry": "^4.3.0",
"autosize": "^6.0.1",
"axios": "^1.8.4",
"classnames": "^2.5.1",
@@ -78,7 +78,7 @@
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.86.3",
"sass": "^1.88.0",
"socket.io-client": "^4.8.1",
"styled-components": "^6.1.18",
"subscriptions-transport-ws": "^0.11.0",
@@ -90,7 +90,7 @@
"@ant-design/icons": "^6.0.0",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.27.1",
"@dotenvx/dotenvx": "^1.43.0",
"@dotenvx/dotenvx": "^1.44.0",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@eslint/js": "^9.26.0",
@@ -2587,9 +2587,9 @@
}
},
"node_modules/@dotenvx/dotenvx": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.43.0.tgz",
"integrity": "sha512-Z8XjM75aWZ/ekUzBjlr/OqQsLWtJY4nVtruxopAt+FlYHfY0/gKl85nD16aEqbTkU53kJcm5psID0L2/sQMmuw==",
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.44.0.tgz",
"integrity": "sha512-18Aa+7KP/L2Kj9lxmT4EJZnsCq/xGIHgzU26rdzsKMhjpeT3YY+qin/dNAnIaVHPZnee7kXpZL55M9htd30r7Q==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -4458,50 +4458,50 @@
"license": "MIT"
},
"node_modules/@sentry-internal/browser-utils": {
"version": "9.17.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.17.0.tgz",
"integrity": "sha512-37n6NXtkUfdK7YiP3L5DJvhA/iusOmnjHQdX1e2VwI6a29xHCl/vRqLR3XNr5K4m+49al+3fWo2ltcKsfV+0xw==",
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.18.0.tgz",
"integrity": "sha512-TwSlmgYpHhe55JpOcVApkM0XcXZh1/cYuEPKPFgeaaPD8BrQrLJJvwKxnonSWXOhdnkJxi4GgK7j7mw57PS4aA==",
"license": "MIT",
"dependencies": {
"@sentry/core": "9.17.0"
"@sentry/core": "9.18.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "9.17.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.17.0.tgz",
"integrity": "sha512-C2jBlGgYVGm8eXK38wlYQyd6NsHKaQlENg5fx8TDFMKWMNmLf6BmnPZ+y73OsFwcUtBz04CwZteybYB2GgYrvQ==",
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.18.0.tgz",
"integrity": "sha512-QlrB8oQK+5bfhbgK6yHF6rLwLNJ9XuGblTc51yVkm4d4jn4W/HDyaNqMfQF+JXdTiFatl8oz2xdKR8kGK8kXyg==",
"license": "MIT",
"dependencies": {
"@sentry/core": "9.17.0"
"@sentry/core": "9.18.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "9.17.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.17.0.tgz",
"integrity": "sha512-oH4NolXkEpe73eRP9r3K6WpERYItZisYQudsNrtkUBQL5M/uENiE7YTOvL5osD8AWmU0hCKY3Oua+qDi2lB+8g==",
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.18.0.tgz",
"integrity": "sha512-2A32FFwrlZtdpBruvpcLEfucu6BpyqOk3F4Bo5smM/5q7u0pa7q5d9FSY5l3nwKEAFAoLGv3hcCb+8wxMm50xA==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "9.17.0",
"@sentry/core": "9.17.0"
"@sentry-internal/browser-utils": "9.18.0",
"@sentry/core": "9.18.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "9.17.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.17.0.tgz",
"integrity": "sha512-w9AxBJIa+MbxDngvwnqouoJ/ezb7wNjxzFXtmaVtGp7hbC4yme/TOTNtFYg2J/ceQf3GMc8AfW5tsP6zU0R7gg==",
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.18.0.tgz",
"integrity": "sha512-3DEyQLmHcYgcwJ8n8eMhI6bhhawPuMc2xTT+Az8gXMqCO/X9ZACpipAmhXFjYP9Ptl+w0Vh3nllJw+gXc/DOsg==",
"license": "MIT",
"dependencies": {
"@sentry-internal/replay": "9.17.0",
"@sentry/core": "9.17.0"
"@sentry-internal/replay": "9.18.0",
"@sentry/core": "9.18.0"
},
"engines": {
"node": ">=18"
@@ -4517,16 +4517,16 @@
}
},
"node_modules/@sentry/browser": {
"version": "9.17.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.17.0.tgz",
"integrity": "sha512-3e/Q5bv06Q+XYV2cKmUgfMfnJtBY8MZKufpcwQ2ab2eMrastqau9KjYeWXapskDm179oPLfzLcDCSlDSTcvqpQ==",
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.18.0.tgz",
"integrity": "sha512-0SWfp4J2+mH4lZOcHfyIwt9VoGD7yCGQE1cm0BPcLwKnrVQeXHtUXNYNy8HTHSjTGyoFDhEAYelE/tdA3OLcWQ==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "9.17.0",
"@sentry-internal/feedback": "9.17.0",
"@sentry-internal/replay": "9.17.0",
"@sentry-internal/replay-canvas": "9.17.0",
"@sentry/core": "9.17.0"
"@sentry-internal/browser-utils": "9.18.0",
"@sentry-internal/feedback": "9.18.0",
"@sentry-internal/replay": "9.18.0",
"@sentry-internal/replay-canvas": "9.18.0",
"@sentry/core": "9.18.0"
},
"engines": {
"node": ">=18"
@@ -4899,22 +4899,22 @@
}
},
"node_modules/@sentry/core": {
"version": "9.17.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.17.0.tgz",
"integrity": "sha512-9f1A93/kY9lLH06L1thPx94IhyLjEP3aRxYAtjtBfzId8UtubSpwP92sbxgslodD73R4tURwWJj7nYZ9HLYBUg==",
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.18.0.tgz",
"integrity": "sha512-kRVH8BqMiaU2FTHYa68zNlAloS43jl4XtIEHkLKVH/7gUtwRmM4Gqj8P7RTrZdO1Lo7ksYnGj+AG05Z09CRbOw==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/react": {
"version": "9.17.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.17.0.tgz",
"integrity": "sha512-hJOVUheFoUKr5e4vHxyKiu72FRgqmMTFIUG9myim8PH8mJYDqab7Z7cOt4dsBR86soKanaRB5PJq5jGFuipLfg==",
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.18.0.tgz",
"integrity": "sha512-1cCLYZrZ2gu6H8eE83DC47kLf+pzD1Rim3dDoOEvwt1F5cD3K/DBeIhoHZaXqBeQxuVyHXOOLXSAC/CIuas5Aw==",
"license": "MIT",
"dependencies": {
"@sentry/browser": "9.17.0",
"@sentry/core": "9.17.0",
"@sentry/browser": "9.18.0",
"@sentry/core": "9.18.0",
"hoist-non-react-statics": "^3.3.2"
},
"engines": {
@@ -6088,9 +6088,9 @@
}
},
"node_modules/antd": {
"version": "5.25.0",
"resolved": "https://registry.npmjs.org/antd/-/antd-5.25.0.tgz",
"integrity": "sha512-p9d8Kuj/bipjNdg9NrTu1VmTrhcwIhURu2NfK6qaBMbb+LRyFdAUoseT+7J4a+5z3jNVjxH5zaYv/45Zf8Coyg==",
"version": "5.25.1",
"resolved": "https://registry.npmjs.org/antd/-/antd-5.25.1.tgz",
"integrity": "sha512-4KC7KuPCjr0z3Vuw9DsF+ceqJaPLbuUI3lOX1sY8ix25ceamp+P8yxOmk3Y2JHCD2ZAhq+5IQ/DTJRN2adWYKQ==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^7.2.0",
@@ -6128,7 +6128,7 @@
"rc-rate": "~2.13.1",
"rc-resize-observer": "^1.4.3",
"rc-segmented": "~2.7.0",
"rc-select": "~14.16.6",
"rc-select": "~14.16.7",
"rc-slider": "~11.1.8",
"rc-steps": "~6.0.1",
"rc-switch": "~4.1.0",
@@ -6232,9 +6232,9 @@
}
},
"node_modules/apollo-link-sentry": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/apollo-link-sentry/-/apollo-link-sentry-4.2.0.tgz",
"integrity": "sha512-w8EUM4aEw1/VxIB3KOP11T8qz44oWRcbXRd2vJq/qHnfRMKS5HkMerSIYwKN2e8k9H8ubfkwBvStH51CVf4wwg==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/apollo-link-sentry/-/apollo-link-sentry-4.3.0.tgz",
"integrity": "sha512-C3WK4iwIzW5vC5BoY3VPdKjm16P6ca/LGKFnxg6PvUuboxPlqs7LHQCYvEsdAxBkoY+8kRXd8Q3+3oU+HHUceA==",
"license": "MIT",
"dependencies": {
"deepmerge": "^4.2.2",
@@ -14024,9 +14024,9 @@
}
},
"node_modules/rc-select": {
"version": "14.16.6",
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.6.tgz",
"integrity": "sha512-YPMtRPqfZWOm2XGTbx5/YVr1HT0vn//8QS77At0Gjb3Lv+Lbut0IORJPKLWu1hQ3u4GsA0SrDzs7nI8JG7Zmyg==",
"version": "14.16.7",
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.7.tgz",
"integrity": "sha512-lT9kO5gFHQdJzu9a0btcOtNaJHkhenSl8H5mcpgXN9VIMXP59rnkpbdHmPrteixWs1D5zFOTyoTYX3b7joADIQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.10.1",
@@ -15375,9 +15375,9 @@
"license": "MIT"
},
"node_modules/sass": {
"version": "1.87.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz",
"integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==",
"version": "1.88.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz",
"integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",

View File

@@ -20,13 +20,13 @@
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.8.1",
"@sentry/cli": "^2.45.0",
"@sentry/react": "^9.17.0",
"@sentry/react": "^9.18.0",
"@sentry/vite-plugin": "^3.4.0",
"@splitsoftware/splitio-react": "^2.1.1",
"@tanem/react-nprogress": "^5.0.53",
"antd": "^5.25.0",
"antd": "^5.25.1",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^4.2.0",
"apollo-link-sentry": "^4.3.0",
"autosize": "^6.0.1",
"axios": "^1.8.4",
"classnames": "^2.5.1",
@@ -77,7 +77,7 @@
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.86.3",
"sass": "^1.88.0",
"socket.io-client": "^4.8.1",
"styled-components": "^6.1.18",
"subscriptions-transport-ws": "^0.11.0",
@@ -130,7 +130,7 @@
"@ant-design/icons": "^6.0.0",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.27.1",
"@dotenvx/dotenvx": "^1.43.0",
"@dotenvx/dotenvx": "^1.44.0",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@eslint/js": "^9.26.0",

View File

@@ -14,8 +14,21 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
setPartsOrderContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "partsOrder"
})
),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
});
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
@@ -69,7 +82,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, b
<Modal
open={open}
onCancel={() => setOpen(false)}
destroyOnClose
destroyOnHidden
title={t("bills.actions.return")}
onOk={() => form.submit()}
>

View File

@@ -29,7 +29,7 @@ export default function BillDetailEditcontainer() {
delete search.billid;
history({ search: queryString.stringify(search) });
}}
destroyOnClose
destroyOnHidden
open={search.billid}
>
<BillDetailEditComponent />

View File

@@ -412,7 +412,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
)}
</Space>
}
destroyOnClose
destroyOnHidden
>
<Form
onFinish={handleFinish}

View File

@@ -75,7 +75,7 @@ export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisi
title={t("payments.labels.findermodal")}
onCancel={() => toggleModalVisible()}
onOk={() => toggleModalVisible()}
destroyOnClose
destroyOnHidden
forceRender
>
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>

View File

@@ -40,7 +40,7 @@ function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodys
</Button>
]}
width="80%"
destroyOnClose
destroyOnHidden
>
<CardPaymentModalComponent />
</Modal>

View File

@@ -63,7 +63,7 @@ export function ContractsFindModalContainer({
title={t("contracts.labels.findermodal")}
onCancel={() => toggleModalVisible()}
onOk={() => toggleModalVisible()}
destroyOnClose
destroyOnHidden
forceRender
>
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>

View File

@@ -152,7 +152,7 @@ export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOv
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<Modal
destroyOnClose={true}
destroyOnHidden
open={modalVisible}
maskClosable={false}
width={"80%"}

View File

@@ -51,6 +51,7 @@ import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { useIsEmployee } from "../../utils/useIsEmployee.js";
// Redux mappings
const mapStateToProps = createStructuredSelector({
@@ -98,6 +99,7 @@ function Header({
const baseTitleRef = useRef(document.title || "");
const lastSetTitleRef = useRef("");
const userAssociationId = bodyshop?.associations?.[0]?.id;
const isEmployee = useIsEmployee(bodyshop, currentUser);
const {
data: unreadData,
@@ -682,7 +684,7 @@ function Header({
icon: unreadLoading ? (
<Spin size="small" />
) : (
<Badge offset={[8, 0]} size="small" count={unreadCount}>
<Badge offset={[8, 0]} size="small" count={isEmployee ? unreadCount : 0}>
<BellFilled />
</Badge>
),

View File

@@ -98,7 +98,7 @@ export function InventoryUpsertModalContainer({ currentUser, bodyshop, inventory
onCancel={() => {
toggleModalVisible();
}}
destroyOnClose
destroyOnHidden
>
<Form form={form} onFinish={handleFinish} layout="vertical">
<InventoryUpsertModal form={form} />

View File

@@ -66,7 +66,7 @@ export function ScheduleEventComponent({
const [popOverVisible, setPopOverVisible] = useState(false);
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
variables: { id: event.job.id },
variables: { id: event.job?.id },
onCompleted: (data) => {
if (data?.jobs_by_pk) {
const totalHours =
@@ -83,6 +83,7 @@ export function ScheduleEventComponent({
});
}
},
fetchPolicy: "network-only"
});
@@ -409,8 +410,10 @@ export function ScheduleEventComponent({
open={popOverVisible}
onOpenChange={setPopOverVisible}
onClick={(e) => {
getJobDetails();
e.stopPropagation();
if (event.job?.id) {
e.stopPropagation();
getJobDetails();
}
}}
getPopupContainer={(trigger) => trigger.parentNode}
trigger="click"

View File

@@ -49,7 +49,7 @@ export function JobCostingModalContainer({ jobCostingModal, toggleModalVisible }
}}
cancelButtonProps={{ style: { display: "none" } }}
width="90%"
destroyOnClose
destroyOnHidden
>
{!costingData ? (
<LoadingSpinner loading={true} />

View File

@@ -32,7 +32,13 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })),
setPrintCenterContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "printCenter"
})
),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
@@ -87,7 +93,7 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTra
};
return (
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{data ? (

View File

@@ -44,7 +44,7 @@ function JobReconciliationModalContainer({ reconciliationModal, toggleModalVisib
onOk={handleCancel}
onCancel={handleCancel}
cancelButtonProps={{ display: "none" }}
destroyOnClose
destroyOnHidden
className="imex-reconciliation-modal"
>
{loading && <LoadingSpinner loading={loading} />}

View File

@@ -24,7 +24,8 @@ export default function JobWatcherToggleComponent({
handleToggleSelf,
handleRemoveWatcher,
handleWatcherSelect,
handleTeamSelect
handleTeamSelect,
isEmployee
}) {
const { t } = useTranslation();
@@ -66,22 +67,32 @@ export default function JobWatcherToggleComponent({
<List>
<List.Item
actions={[
<Button
type={isWatching ? "primary" : "default"}
danger={!isWatching}
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
size="medium"
onClick={handleToggleSelf}
loading={adding || removing}
>
{isWatching ? t("notifications.labels.unwatch") : t("notifications.labels.watch")}
</Button>
<Tooltip title={!isEmployee ? t("notifications.tooltips.not-employee") : ""} placement="top">
<span>
<Button
type={isWatching ? "primary" : "default"}
danger={!isWatching}
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
size="medium"
onClick={handleToggleSelf}
loading={adding || removing}
disabled={!isEmployee || adding || removing}
>
{isWatching ? t("notifications.labels.unwatch") : t("notifications.labels.watch")}
</Button>
</span>
</Tooltip>
]}
>
<List.Item.Meta>
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
{t("notifications.labels.watching-issue")}
</Text>
{!isEmployee && (
<Text type="danger" style={{ marginBottom: 8, display: "block" }}>
{t("notifications.tooltips.not-employee")}
</Text>
)}
</List.Item.Meta>
</List.Item>
</List>
@@ -98,8 +109,11 @@ export default function JobWatcherToggleComponent({
<EmployeeSearchSelectComponent
style={{ minWidth: "100%" }}
options={
bodyshop?.employees?.filter((e) =>
jobWatchers.every((w) => w.user_email !== e.user_email && e.active && e.user_email)
bodyshop?.employees?.filter(
(e) =>
e.user_email && // Ensure user_email is not null or undefined
e.active && // Ensure employee is active
jobWatchers.every((w) => w.user_email !== e.user_email) // Ensure not already a watcher
) || []
}
placeholder={t("notifications.labels.employee-search")}

View File

@@ -6,6 +6,7 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
import { useIsEmployee } from "../../utils/useIsEmployee.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -21,13 +22,14 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
splitKey: bodyshop && bodyshop.imexshopid
});
const userEmail = currentUser.email;
const jobid = job.id;
const isEmployee = useIsEmployee(bodyshop, currentUser);
const [open, setOpen] = useState(false);
const [selectedWatcher, setSelectedWatcher] = useState(null);
const [selectedTeam, setSelectedTeam] = useState(null);
const userEmail = currentUser.email;
const jobid = job.id;
// Fetch current watchers with refetch capability
const {
data: watcherData,
@@ -139,13 +141,13 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
});
const handleToggleSelf = useCallback(async () => {
if (adding || removing) return;
if (adding || removing || !isEmployee) return;
if (isWatching) {
await removeWatcher({ variables: { jobid, userEmail } });
} else {
await addWatcher({ variables: { jobid, userEmail } });
}
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing]);
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing, isEmployee]);
const handleRemoveWatcher = useCallback(
async (email) => {
@@ -187,7 +189,16 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
setSelectedTeam(null);
return;
}
await Promise.all(newWatchers.map((email) => addWatcher({ variables: { jobid, userEmail: email } })));
await Promise.all(
newWatchers.map((email) =>
addWatcher({
variables: {
jobid,
userEmail: email
}
})
)
);
},
[jobWatchers, addWatcher, jobid, adding]
);
@@ -212,6 +223,7 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
handleWatcherSelect={handleWatcherSelect}
handleTeamSelect={handleTeamSelect}
currentUser={currentUser}
isEmployee={isEmployee} // Pass isEmployee to the component
/>
);
}

View File

@@ -4,11 +4,12 @@ import { Col, Row } from "antd";
import Axios from "axios";
import _ from "lodash";
import queryString from "query-string";
import React, { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
DELETE_AVAILABLE_JOB,
@@ -33,7 +34,6 @@ import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.contai
import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
import HeaderFields from "./jobs-available-supplement.headerfields";
import JobsAvailableTableComponent from "./jobs-available-table.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -195,7 +195,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
await deleteJob({
variables: { id: estData.id }
}).then((r) => {
}).then(() => {
refetch();
setInsertLoading(false);
});
@@ -315,7 +315,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
deleteJob({
variables: { id: estData.id }
}).then((r) => {
}).then(() => {
refetch();
setInsertLoading(false);
});
@@ -372,7 +372,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
loadEstData({ variables: { id: record.id } });
modalSearchState[1](record.clm_no);
setJobModalVisible(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line
}, []);
useEffect(() => {
@@ -456,7 +456,7 @@ function replaceEmpty(someObj, replaceValue = null) {
return JSON.parse(temp);
}
async function CheckTaxRatesUSA(estData, bodyshop) {
async function CheckTaxRatesUSA(estData) {
if (!estData.parts_tax_rates?.PAM) {
estData.parts_tax_rates.PAM = estData.parts_tax_rates.PAC;
}
@@ -568,7 +568,7 @@ async function CheckTaxRates(estData, bodyshop) {
});
//}
}
function ResolveCCCLineIssues(estData, bodyshop) {
function ResolveCCCLineIssues(estData) {
//Find all misc amounts, populate them to the act price.
//This needs to be done before cleansing unq_seq since some misc prices could move over.
estData.joblines.data.forEach((line) => {
@@ -585,6 +585,9 @@ function ResolveCCCLineIssues(estData, bodyshop) {
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
line.mod_lbr_ty = "LAR";
}
if (line.mod_lbr_ty === "OTSL") {
line.mod_lbr_ty = line.mod_lbr_hrs === 0 ? null : "LAB";
}
}
});
});

View File

@@ -65,7 +65,7 @@ export default connect(
<Modal
title={t("jobs.labels.existing_jobs")}
width={"80%"}
destroyOnClose
destroyOnHidden
okButtonProps={{ disabled: selectedJob ? false : true }}
{...modalProps}
>

View File

@@ -20,7 +20,14 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
});
export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleModalVisible, insertAuditTrail }) {
@@ -123,7 +130,7 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
onCancel={() => {
toggleModalVisible();
}}
destroyOnClose
destroyOnHidden
>
<Form form={form} onFinish={handleFinish} layout="vertical">
<NoteUpsertModalComponent form={form} />

View File

@@ -1,11 +1,11 @@
import { Virtuoso } from "react-virtuoso";
import { Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
import { Alert, Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import "./notification-center.styles.scss";
import day from "../../utils/day.js";
import { forwardRef, useRef, useEffect } from "react";
import { forwardRef, useEffect, useRef } from "react";
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
const { Text, Title } = Typography;
@@ -26,7 +26,8 @@ const NotificationCenterComponent = forwardRef(
markAllRead,
loadMore,
onNotificationClick,
unreadCount
unreadCount,
isEmployee
},
ref
) => {
@@ -93,7 +94,12 @@ const NotificationCenterComponent = forwardRef(
) : (
<EyeOutlined className="notification-toggle-icon" />
)}
<Switch checked={showUnreadOnly} onChange={(checked) => toggleUnreadOnly(checked)} size="small" />
<Switch
checked={showUnreadOnly}
onChange={(checked) => toggleUnreadOnly(checked)}
size="small"
disabled={!isEmployee}
/>
</Space>
</Tooltip>
<Tooltip title={t("notifications.labels.mark-all-read")}>
@@ -106,14 +112,20 @@ const NotificationCenterComponent = forwardRef(
</Tooltip>
</div>
</div>
<Virtuoso
ref={virtuosoRef}
style={{ height: "400px", width: "100%" }}
data={notifications}
totalCount={notifications.length}
endReached={loadMore}
itemContent={renderNotification}
/>
{!isEmployee ? (
<div style={{ padding: 10 }}>
<Alert message={t("notifications.labels.employee-notification")} type="warning" />
</div>
) : (
<Virtuoso
ref={virtuosoRef}
style={{ height: "400px", width: "100%" }}
data={notifications}
totalCount={notifications.length}
endReached={loadMore}
itemContent={renderNotification}
/>
)}
</div>
);
}

View File

@@ -4,9 +4,10 @@ import { connect } from "react-redux";
import NotificationCenterComponent from "./notification-center.component";
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
import day from "../../utils/day.js";
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js";
import { useIsEmployee } from "../../utils/useIsEmployee.js";
// This will be used to poll for notifications when the socket is disconnected
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
@@ -17,17 +18,18 @@ const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
* @param onClose
* @param bodyshop
* @param unreadCount
* @param currentUser
* @returns {JSX.Element}
* @constructor
*/
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount, currentUser }) => {
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
const [notifications, setNotifications] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
const notificationRef = useRef(null);
const userAssociationId = bodyshop?.associations?.[0]?.id;
const isEmployee = useIsEmployee(bodyshop, currentUser);
const baseWhereClause = useMemo(() => {
return { associationid: { _eq: userAssociationId } };
@@ -51,7 +53,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
fetchPolicy: "cache-and-network",
notifyOnNetworkStatusChange: true,
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
skip: !userAssociationId,
skip: !userAssociationId || !isEmployee,
onError: (err) => {
console.error(`Error polling Notifications: ${err?.message || ""}`);
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
@@ -71,7 +73,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
}, [visible, onClose]);
useEffect(() => {
if (data?.notifications) {
if (data?.notifications && isEmployee) {
const processedNotifications = data.notifications
.map((notif) => {
let scenarioText;
@@ -101,11 +103,13 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
})
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
setNotifications(processedNotifications);
} else if (!isEmployee) {
setNotifications([]); // Clear notifications if not an employee
}
}, [data]);
}, [data, isEmployee]);
const loadMore = useCallback(() => {
if (!queryLoading && data?.notifications.length) {
if (!queryLoading && data?.notifications.length && isEmployee) {
setIsLoading(true); // Show spinner during fetchMore
fetchMore({
variables: { offset: data.notifications.length, where: whereClause },
@@ -121,13 +125,14 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
})
.finally(() => setIsLoading(false)); // Hide spinner when done
}
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause]);
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause, isEmployee]);
const handleToggleUnreadOnly = (value) => {
setShowUnreadOnly(value);
};
const handleMarkAllRead = useCallback(() => {
if (!isEmployee) return; // Do nothing if not an employee
setIsLoading(true);
markAllNotificationsRead()
.then(() => {
@@ -147,7 +152,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
})
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
.finally(() => setIsLoading(false));
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly]);
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly, isEmployee]);
const handleNotificationClick = useCallback(
(notificationId) => {
@@ -170,17 +175,18 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
);
useEffect(() => {
if (visible && !isConnected) {
if (visible && !isConnected && isEmployee) {
setIsLoading(true);
refetch()
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
.finally(() => setIsLoading(false));
}
}, [visible, isConnected, refetch]);
}, [visible, isConnected, refetch, isEmployee]);
return (
<NotificationCenterComponent
ref={notificationRef}
isEmployee={isEmployee}
visible={visible}
onClose={onClose}
notifications={notifications}
@@ -196,7 +202,8 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
};
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
bodyshop: selectBodyshop,
currentUser: selectCurrentUser
});
export default connect(mapStateToProps, null)(NotificationCenterContainer);

View File

@@ -1,10 +1,10 @@
import { useMutation, useQuery } from "@apollo/client";
import { useEffect, useState } from "react";
import { Button, Card, Checkbox, Divider, Form, Space, Switch, Table, Typography } from "antd";
import { Alert, Button, Card, Checkbox, Divider, Form, Space, Switch, Table, Typography } from "antd";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import {
QUERY_NOTIFICATION_SETTINGS,
@@ -16,14 +16,16 @@ import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
import PropTypes from "prop-types";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
import { useIsEmployee } from "../../utils/useIsEmployee.js";
/**
* Notifications Settings Form
* @param currentUser
* @param bodyshop
* @returns {JSX.Element}
* @constructor
*/
const NotificationSettingsForm = ({ currentUser }) => {
const NotificationSettingsForm = ({ currentUser, bodyshop }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [initialValues, setInitialValues] = useState({});
@@ -31,6 +33,7 @@ const NotificationSettingsForm = ({ currentUser }) => {
const [autoAddEnabled, setAutoAddEnabled] = useState(false);
const [initialAutoAdd, setInitialAutoAdd] = useState(false);
const notification = useNotification();
const isEmployee = useIsEmployee(bodyshop, currentUser);
// Fetch notification settings and notifications_autoadd
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
@@ -199,6 +202,11 @@ const NotificationSettingsForm = ({ currentUser }) => {
</Space>
}
>
{!isEmployee && (
<div style={{ width: "100%", marginBottom: "10px" }}>
<Alert message={t("notifications.labels.employee-notification")} type="warning" />
</div>
)}
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
<Divider />
</Card>
@@ -209,11 +217,13 @@ const NotificationSettingsForm = ({ currentUser }) => {
NotificationSettingsForm.propTypes = {
currentUser: PropTypes.shape({
email: PropTypes.string.isRequired
}).isRequired
}).isRequired,
bodyshop: PropTypes.object.isRequired
};
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
currentUser: selectCurrentUser,
bodyshop: selectBodyshop
});
export default connect(mapStateToProps)(NotificationSettingsForm);

View File

@@ -333,7 +333,7 @@ export function PartsOrderModalContainer({
onOk={() => form.submit()}
okButtonProps={{ loading: saving }}
cancelButtonProps={{ loading: saving }}
destroyOnClose
destroyOnHidden
width="75%"
forceRender
>

View File

@@ -46,7 +46,7 @@ export default function PartsQueueDetailCard() {
};
return (
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{data ? (

View File

@@ -90,7 +90,7 @@ export function PartsReceiveModalContainer({ partsReceiveModal, toggleModalVisib
onCancel={() => toggleModalVisible()}
onOk={() => form.submit()}
okButtonProps={{ loading: loading }}
destroyOnClose
destroyOnHidden
forceRender
width="50%"
>

View File

@@ -134,7 +134,7 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop }) {
<Modal
title={!context || (context && !context.id) ? t("payments.labels.new") : t("payments.labels.edit")}
open={open}
destroyOnClose
destroyOnHidden
okText={t("general.actions.save")}
onOk={() => form.submit()}
width="50%"

View File

@@ -32,7 +32,7 @@ export function PrintCenterModalContainer({ printCenterModal, toggleModalVisible
okText={t("general.actions.close")}
width="90%"
title={t("printcenter.labels.title")}
destroyOnClose
destroyOnHidden
>
<PrintCenterModalComponent context={context} />
</Modal>

View File

@@ -1,7 +1,15 @@
import { Card, Col, Form, Radio, Row } from "antd";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../../redux/user/user.selectors";
import { HasFeatureAccess } from "../../feature-wrapper/feature-wrapper.component";
const LayoutSettings = ({ t }) => (
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const LayoutSettings = ({ t, bodyshop }) => (
<Card title={t("production.settings.layout")} style={{ maxWidth: "100%", overflowX: "auto" }}>
<Row gutter={[16, 16]}>
{[
@@ -30,14 +38,18 @@ const LayoutSettings = ({ t }) => (
{ value: false, label: t("production.labels.wide") }
]
},
{
name: "cardcolor",
label: t("production.labels.cardcolor"),
options: [
{ value: true, label: t("production.labels.on") },
{ value: false, label: t("production.labels.off") }
]
},
...(HasFeatureAccess({ bodyshop, featureName: "smartscheduling" })
? [
{
name: "cardcolor",
label: t("production.labels.cardcolor"),
options: [
{ value: true, label: t("production.labels.on") },
{ value: false, label: t("production.labels.off") }
]
}
]
: []),
{
name: "kiosk",
label: t("production.labels.kiosk_mode"),
@@ -67,4 +79,4 @@ LayoutSettings.propTypes = {
t: PropTypes.func.isRequired
};
export default LayoutSettings;
export default connect(mapStateToProps)(LayoutSettings);

View File

@@ -28,7 +28,7 @@ export function ReportCenterModalContainer({ reportCenterModal, toggleModalVisib
onOk={() => toggleModalVisible()}
onCancel={() => toggleModalVisible()}
cancelButtonProps={{ style: { display: "none" } }}
destroyOnClose
destroyOnHidden
width="80%"
>
<RbacWrapperComponent action="shop:reportcenter">

View File

@@ -209,7 +209,7 @@ export function ScheduleJobModalContainer({
onOk={() => form.submit()}
width={"90%"}
maskClosable={false}
destroyOnClose
destroyOnHidden
okButtonProps={{
loading: loading
}}

View File

@@ -106,7 +106,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
<>
<Modal
open={state.open}
destroyOnClose
destroyOnHidden
width="80%"
closable={false}
cancelButtonProps={{ style: { display: "none" } }}

View File

@@ -8,7 +8,7 @@ export default function ShopInfoNotificationsAutoadd({ bodyshop }) {
const { t } = useTranslation();
// Filter employee options to ensure active employees with valid IDs
const employeeOptions = bodyshop?.employees?.filter((e) => e.active && e.id && typeof e.id === "string") || [];
const employeeOptions = bodyshop?.employees?.filter((e) => e.active && e.user_email && e.id) || [];
return (
<div>

View File

@@ -275,7 +275,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
toggleModalVisible();
}}
okButtonProps={{ disabled: !isTouched }}
destroyOnClose
destroyOnHidden
>
<Form
form={form}

View File

@@ -70,7 +70,7 @@ export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) {
};
return (
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{data ? (

View File

@@ -39,7 +39,7 @@ export function TimeTicketListTeamPay({ bodyshop, context, actions }) {
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 }}>
<LayoutFormRow grow noDivider>
<Form.Item shouldUpdate>

View File

@@ -181,7 +181,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
)}
</Space>
}
destroyOnClose
destroyOnHidden
id="time-ticket-modal"
>
<Form

View File

@@ -119,7 +119,7 @@ export function TimeTickeTaskModalContainer({
return (
<Modal
destroyOnClose
destroyOnHidden
open={open}
onCancel={() => {
toggleModalVisible();

View File

@@ -2474,7 +2474,8 @@
"teams-search": "Search for a Team",
"unwatch": "Unwatch",
"watch": "Watch",
"watching-issue": "Watching"
"watching-issue": "Watching",
"employee-notification": "Notifications are disabled because you do not have an associated Employee record."
},
"scenarios": {
"alternate-transport-changed": "Alternate Transport Changed",
@@ -2494,7 +2495,9 @@
"tasks-updated-created": "Tasks Updated / Created"
},
"tooltips": {
"job-watchers": "Job Watchers"
"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": {

View File

@@ -2476,7 +2476,8 @@
"teams-search": "",
"unwatch": "",
"watch": "",
"watching-issue": ""
"watching-issue": "",
"employee-notification": ""
},
"scenarios": {
"alternate-transport-changed": "",
@@ -2496,7 +2497,8 @@
"tasks-updated-created": ""
},
"tooltips": {
"job-watchers": ""
"job-watchers": "",
"not-employee": ""
}
},
"owner": {

View File

@@ -2476,7 +2476,8 @@
"teams-search": "",
"unwatch": "",
"watch": "",
"watching-issue": ""
"watching-issue": "",
"employee-notification": ""
},
"scenarios": {
"alternate-transport-changed": "",
@@ -2496,7 +2497,8 @@
"tasks-updated-created": ""
},
"tooltips": {
"job-watchers": ""
"job-watchers": "",
"not-employee": ""
}
},
"owner": {

View 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]);
}

667
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,14 +16,14 @@
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
},
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.804.0",
"@aws-sdk/client-elasticache": "^3.804.0",
"@aws-sdk/client-s3": "^3.804.0",
"@aws-sdk/client-secrets-manager": "^3.804.0",
"@aws-sdk/client-ses": "^3.804.0",
"@aws-sdk/credential-provider-node": "^3.804.0",
"@aws-sdk/lib-storage": "^3.804.0",
"@aws-sdk/s3-request-presigner": "^3.804.0",
"@aws-sdk/client-cloudwatch-logs": "^3.808.0",
"@aws-sdk/client-elasticache": "^3.808.0",
"@aws-sdk/client-s3": "^3.808.0",
"@aws-sdk/client-secrets-manager": "^3.808.0",
"@aws-sdk/client-ses": "^3.808.0",
"@aws-sdk/credential-provider-node": "^3.808.0",
"@aws-sdk/lib-storage": "^3.808.0",
"@aws-sdk/s3-request-presigner": "^3.808.0",
"@opensearch-project/opensearch": "^2.13.0",
"@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0",
@@ -42,7 +42,7 @@
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"firebase-admin": "^13.2.0",
"firebase-admin": "^13.4.0",
"graphql": "^16.11.0",
"graphql-request": "^6.1.0",
"intuit-oauth": "^4.2.0",
@@ -57,7 +57,7 @@
"node-persist": "^4.0.4",
"nodemailer": "^6.10.0",
"phone": "^3.1.58",
"query-string": "^9.1.2",
"query-string": "7.1.3",
"recursive-diff": "^1.0.9",
"rimraf": "^6.0.1",
"skia-canvas": "^2.0.2",
@@ -65,7 +65,7 @@
"socket.io": "^4.8.1",
"socket.io-adapter": "^2.5.5",
"ssh2-sftp-client": "^11.0.0",
"twilio": "^5.6.0",
"twilio": "^5.6.1",
"uuid": "^11.1.0",
"winston": "^3.17.0",
"winston-cloudwatch": "^6.3.0",
@@ -80,7 +80,7 @@
"mock-require": "^3.0.3",
"p-limit": "^3.1.0",
"prettier": "^3.5.3",
"supertest": "^7.1.0",
"supertest": "^7.1.1",
"vitest": "^3.1.3"
}
}

View File

@@ -4,4 +4,5 @@ exports.chatter = require("./chatter").default;
exports.claimscorp = require("./claimscorp").default;
exports.kaizen = require("./kaizen").default;
exports.usageReport = require("./usageReport").default;
exports.podium = require("./podium").default;
exports.podium = require("./podium").default;
exports.emsUpload = require("./emsUpload").default;

22
server/data/emsUpload.js Normal file
View File

@@ -0,0 +1,22 @@
const moment = require("moment-timezone");
const logger = require("../utils/logger");
const s3Client = require("../utils/s3"); // Using the S3 client utilities with LocalStack support
const emsUpload = async (req, res) => {
try {
const { bodyshopid, ciecaid, clm_no, ownr_ln } = req.body;
const presignedUrl = await s3Client.getPresignedUrl({
bucketName: process.env.S3_EMS_UPLOAD_BUCKET,
key: `${bodyshopid}/${ciecaid}-${clm_no}-${ownr_ln}-${moment().format("YYYY-MM-DD--HH-mm-ss")}.zip`
});
res.status(200).json({ presignedUrl });
} catch (error) {
logger.log("ems-upload-presign-error", "ERROR", req?.user?.email, null, {
error: error.message,
stack: error.stack
});
res.status(500).json({ error: error.message, stack: error.stack });
}
};
exports.default = emsUpload;

View File

@@ -185,7 +185,7 @@ async function uploadViaSFTP(csvObj) {
await sftp.connect(ftpSetup);
try {
csvObj.result = await sftp.put(Buffer.from(csvObj.xml), `${csvObj.filename}`);
csvObj.result = await sftp.put(Buffer.from(csvObj.csv), `${csvObj.filename}`);
logger.log("podium-sftp-upload", "DEBUG", "api", csvObj.bodyshopid, {
imexshopid: csvObj.imexshopid,
filename: csvObj.filename,

View File

@@ -138,6 +138,9 @@ router.post("/canvastest", validateFirebaseIdTokenMiddleware, canvastest);
// Alert Check
router.post("/alertcheck", eventAuthorizationMiddleware, alertCheck);
//EMS Upload
router.post("/emsupload", validateFirebaseIdTokenMiddleware, data.emsUpload);
// Redis Cache Routes
router.post("/bodyshop-cache", eventAuthorizationMiddleware, updateBodyshopCache);

View File

@@ -9,6 +9,7 @@ const {
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
const { InstanceRegion } = require("./instanceMgr");
const { isString, isEmpty } = require("lodash");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
const createS3Client = () => {
const S3Options = {
@@ -95,6 +96,17 @@ const createS3Client = () => {
throw error;
}
};
const getPresignedUrl = async ({ bucketName, key }) => {
const command = new PutObjectCommand({
Bucket: bucketName,
Key: key,
StorageClass: "INTELLIGENT_TIERING"
});
const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 360 });
return presignedUrl;
}
return {
uploadFileToS3,
downloadFileFromS3,
@@ -102,8 +114,12 @@ const createS3Client = () => {
deleteFileFromS3,
copyFileInS3,
fileExistsInS3,
getPresignedUrl,
...s3Client
};
};
module.exports = createS3Client();