feature/IO-3228-Notifications-1.6-and-Deprecations

- See Ticket for full details (Notifications restrictions, AntD deprecations)
This commit is contained in:
Dave Richer
2025-05-09 10:38:19 -04:00
parent 266c3acf34
commit 6e6addd62f
41 changed files with 486 additions and 377 deletions

View File

@@ -25,9 +25,9 @@
"@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",
@@ -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",

View File

@@ -24,9 +24,9 @@
"@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",

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

@@ -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

@@ -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

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

538
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.806.0",
"@aws-sdk/client-elasticache": "^3.806.0",
"@aws-sdk/client-s3": "^3.806.0",
"@aws-sdk/client-secrets-manager": "^3.806.0",
"@aws-sdk/client-ses": "^3.806.0",
"@aws-sdk/credential-provider-node": "^3.806.0",
"@aws-sdk/lib-storage": "^3.806.0",
"@aws-sdk/s3-request-presigner": "^3.806.0",
"@opensearch-project/opensearch": "^2.13.0",
"@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0",