diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx
index 0de1be784..175e4785e 100644
--- a/client/src/components/header/header.component.jsx
+++ b/client/src/components/header/header.component.jsx
@@ -1,4 +1,16 @@
-import Icon, {
+import { Badge, Layout, Menu, Spin } from "antd";
+import { useTranslation } from "react-i18next";
+import { useEffect, useState } from "react";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { Link } from "react-router-dom";
+import { useQuery } from "@apollo/client";
+import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
+import { useSplitTreatments } from "@splitsoftware/splitio-react";
+import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
+import LockWrapper from "../lock-wrapper/lock-wrapper.component";
+import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
+import {
BankFilled,
BarChartOutlined,
BellFilled,
@@ -26,35 +38,21 @@ import Icon, {
UnorderedListOutlined,
UserOutlined
} from "@ant-design/icons";
-import { useSplitTreatments } from "@splitsoftware/splitio-react";
-import { Badge, Layout, Menu, Space, Spin } from "antd";
-import { useTranslation } from "react-i18next";
import { BsKanban } from "react-icons/bs";
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
import { FiLogOut } from "react-icons/fi";
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
import { IoBusinessOutline } from "react-icons/io5";
import { RiSurveyLine } from "react-icons/ri";
-import { connect } from "react-redux";
-import { Link } from "react-router-dom";
-import { createStructuredSelector } from "reselect";
+import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
import { signOutStart } from "../../redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
-import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
-import LockWrapper from "../lock-wrapper/lock-wrapper.component";
-import { useState, useEffect } from "react";
-import { debounce } from "lodash";
-import { useQuery } from "@apollo/client";
-import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
-import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
-import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
-
-// Used to Determine if the Header is in Mobile Mode, and to toggle the multiple menus
-const HEADER_MOBILE_BREAKPOINT = 576;
+import day from "../../utils/day.js";
+// Redux mappings
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
recentItems: selectRecentItems,
@@ -63,43 +61,13 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
- setBillEnterContext: (context) =>
- dispatch(
- setModalContext({
- context: context,
- modal: "billEnter"
- })
- ),
- setTimeTicketContext: (context) =>
- dispatch(
- setModalContext({
- context: context,
- modal: "timeTicket"
- })
- ),
- setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })),
- setReportCenterContext: (context) =>
- dispatch(
- setModalContext({
- context: context,
- modal: "reportCenter"
- })
- ),
+ setBillEnterContext: (context) => dispatch(setModalContext({ context, modal: "billEnter" })),
+ setTimeTicketContext: (context) => dispatch(setModalContext({ context, modal: "timeTicket" })),
+ setPaymentContext: (context) => dispatch(setModalContext({ context, modal: "payment" })),
+ setReportCenterContext: (context) => dispatch(setModalContext({ context, modal: "reportCenter" })),
signOutStart: () => dispatch(signOutStart()),
- setCardPaymentContext: (context) =>
- dispatch(
- setModalContext({
- context: context,
- modal: "cardPayment"
- })
- ),
- setTaskUpsertContext: (context) =>
- dispatch(
- setModalContext({
- context: context,
- modal: "taskUpsert"
- })
- )
+ setCardPaymentContext: (context) => dispatch(setModalContext({ context, modal: "cardPayment" })),
+ setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
});
function Header({
@@ -125,12 +93,10 @@ function Header({
});
const { t } = useTranslation();
-
const { isConnected } = useSocket();
const [notificationVisible, setNotificationVisible] = useState(false);
const userAssociationId = bodyshop?.associations?.[0]?.id;
-
const {
data: unreadData,
refetch: refetchUnread,
@@ -138,22 +104,21 @@ function Header({
} = useQuery(GET_UNREAD_COUNT, {
variables: { associationid: userAssociationId },
fetchPolicy: "network-only",
- pollInterval: isConnected ? 0 : 30000, // Poll only if socket is down
- skip: !userAssociationId // Skip query if no userAssociationId
+ pollInterval: isConnected ? 0 : day.duration(60, "seconds").asMilliseconds(),
+ skip: !userAssociationId
});
const unreadCount = unreadData?.notifications_aggregate?.aggregate?.count ?? 0;
- // Initial fetch and socket status handling
useEffect(() => {
if (userAssociationId) {
- refetchUnread().catch((e) => console.error(`Something went wrong fetching unread notifications: ${e?.message}`));
+ refetchUnread().catch((e) => console.error(`Error fetching unread notifications: ${e?.message}`));
}
}, [refetchUnread, userAssociationId]);
useEffect(() => {
if (!isConnected && !unreadLoading && userAssociationId) {
- refetchUnread().catch((e) => console.error(`Something went wrong fetching unread notifications: ${e?.message}`));
+ refetchUnread().catch((e) => console.error(`Error fetching unread notifications: ${e?.message}`));
}
}, [isConnected, unreadLoading, refetchUnread, userAssociationId]);
@@ -162,33 +127,11 @@ function Header({
if (handleMenuClick) handleMenuClick(e);
};
- const [isMobile, setIsMobile] = useState(() => {
- const effectiveWidth = window.innerWidth / (window.devicePixelRatio || 1);
- return effectiveWidth <= HEADER_MOBILE_BREAKPOINT;
- });
-
- const handleResize = debounce(() => {
- const effectiveWidth = window.innerWidth / (window.devicePixelRatio || 1);
- setIsMobile(effectiveWidth <= HEADER_MOBILE_BREAKPOINT);
- }, 200);
-
- useEffect(() => {
- window.addEventListener("resize", handleResize);
- window.addEventListener("orientationchange", handleResize);
- return () => {
- window.removeEventListener("resize", handleResize);
- window.removeEventListener("orientationchange", handleResize);
- handleResize.cancel(); // Cancel any pending debounced calls on cleanup
- };
- }, [handleResize]);
-
- // Accounting children setup (unchanged)
- const accountingChildren = [];
- accountingChildren.push(
+ const accountingChildren = [
{
key: "bills",
id: "header-accounting-bills",
- icon: ,
+ icon: ,
label: (
@@ -200,42 +143,31 @@ function Header({
{
key: "enterbills",
id: "header-accounting-enterbills",
- icon: ,
+ icon: ,
label: (
-
-
- {t("menus.header.enterbills")}
-
-
+
+ {t("menus.header.enterbills")}
+
),
- onClick: () => {
+ onClick: () =>
HasFeatureAccess({ featureName: "bills", bodyshop }) &&
- setBillEnterContext({
- actions: {},
- context: {}
- });
- }
- }
- );
-
- if (Simple_Inventory.treatment === "on") {
- accountingChildren.push(
- {
- type: "divider"
- },
- {
- key: "inventory",
- id: "header-accounting-inventory",
- icon: ,
- label: {t("menus.header.inventory")}
- }
- );
- }
-
- accountingChildren.push(
- {
- type: "divider"
+ setBillEnterContext({
+ actions: {},
+ context: {}
+ })
},
+ ...(Simple_Inventory.treatment === "on"
+ ? [
+ { type: "divider" },
+ {
+ key: "inventory",
+ id: "header-accounting-inventory",
+ icon: ,
+ label: {t("menus.header.inventory")}
+ }
+ ]
+ : []),
+ { type: "divider" },
{
key: "allpayments",
id: "header-accounting-allpayments",
@@ -251,41 +183,31 @@ function Header({
{
key: "enterpayments",
id: "header-accounting-enterpayments",
- icon: ,
+ icon: ,
label: (
{t("menus.header.enterpayment")}
),
- onClick: () => {
+ onClick: () =>
HasFeatureAccess({ featureName: "payments", bodyshop }) &&
- setPaymentContext({
- actions: {},
- context: null
- });
- }
- }
- );
-
- if (ImEXPay.treatment === "on") {
- accountingChildren.push({
- key: "entercardpayments",
- id: "header-accounting-entercardpayments",
- icon: ,
- label: t("menus.header.entercardpayment"),
- onClick: () => {
- setCardPaymentContext({
+ setPaymentContext({
actions: {},
- context: {}
- });
- }
- });
- }
-
- accountingChildren.push(
- {
- type: "divider"
+ context: null
+ })
},
+ ...(ImEXPay.treatment === "on"
+ ? [
+ {
+ key: "entercardpayments",
+ id: "header-accounting-entercardpayments",
+ icon: ,
+ label: t("menus.header.entercardpayment"),
+ onClick: () => setCardPaymentContext({ actions: {}, context: {} })
+ }
+ ]
+ : []),
+ { type: "divider" },
{
key: "timetickets",
id: "header-accounting-timetickets",
@@ -297,133 +219,124 @@ function Header({
)
- }
- );
-
- if (bodyshop?.md_tasks_presets?.use_approvals) {
- accountingChildren.push({
- key: "ttapprovals",
- id: "header-accounting-ttapprovals",
- icon: ,
- label: {t("menus.header.ttapprovals")}
- });
- }
- accountingChildren.push(
+ },
+ ...(bodyshop?.md_tasks_presets?.use_approvals
+ ? [
+ {
+ key: "ttapprovals",
+ id: "header-accounting-ttapprovals",
+ icon: ,
+ label: {t("menus.header.ttapprovals")}
+ }
+ ]
+ : []),
{
key: "entertimetickets",
- icon: ,
+ id: "header-accounting-entertimetickets",
+ icon: ,
label: (
{t("menus.header.entertimeticket")}
),
- id: "header-accounting-entertimetickets",
- onClick: () => {
+ onClick: () =>
HasFeatureAccess({ featureName: "timetickets", bodyshop }) &&
- setTimeTicketContext({
- actions: {},
- context: {
- created_by: currentUser.displayName
- ? currentUser.email.concat(" | ", currentUser.displayName)
- : currentUser.email
- }
- });
- }
+ setTimeTicketContext({
+ actions: {},
+ context: {
+ created_by: currentUser.displayName
+ ? `${currentUser.email} | ${currentUser.displayName}`
+ : currentUser.email
+ }
+ })
},
+ { type: "divider" },
{
- type: "divider"
- }
- );
-
- const accountingExportChildren = [
- {
- key: "receivables",
- id: "header-accounting-receivables",
+ key: "accountingexport",
+ id: "header-accounting-export",
+ icon: ,
label: (
-
-
- {t("menus.header.accounting-receivables")}
-
-
- )
+
+ {t("menus.header.export")}
+
+ ),
+ children: [
+ {
+ key: "receivables",
+ id: "header-accounting-receivables",
+ label: (
+
+
+ {t("menus.header.accounting-receivables")}
+
+
+ )
+ },
+ ...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) ||
+ DmsAp.treatment === "on"
+ ? [
+ {
+ key: "payables",
+ id: "header-accounting-payables",
+ label: (
+
+
+ {t("menus.header.accounting-payables")}
+
+
+ )
+ }
+ ]
+ : []),
+ ...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))
+ ? [
+ {
+ key: "payments",
+ id: "header-accounting-payments",
+ label: (
+
+
+ {t("menus.header.accounting-payments")}
+
+
+ )
+ }
+ ]
+ : []),
+ { type: "divider" },
+ {
+ key: "exportlogs",
+ id: "header-accounting-exportlogs",
+ label: (
+
+
+ {t("menus.header.export-logs")}
+
+
+ )
+ }
+ ]
}
];
- if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on") {
- accountingExportChildren.push({
- key: "payables",
- id: "header-accounting-payables",
- label: (
-
-
- {t("menus.header.accounting-payables")}
-
-
- )
- });
- }
-
- if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))) {
- accountingExportChildren.push({
- key: "payments",
- id: "header-accounting-payments",
- label: (
-
-
- {t("menus.header.accounting-payments")}
-
-
- )
- });
- }
-
- accountingExportChildren.push(
- {
- type: "divider"
- },
- {
- key: "exportlogs",
- id: "header-accounting-exportlogs",
- label: (
-
-
- {t("menus.header.export-logs")}
-
-
- )
- }
- );
-
- accountingChildren.push({
- key: "accountingexport",
- id: "header-accounting-export",
- icon: ,
- label: (
-
- {t("menus.header.export")}
-
- ),
- children: accountingExportChildren
- });
-
- // Define all menu items
- const menuItems = [
+ // Left menu items (includes original navigation items)
+ const leftMenuItems = [
{
key: "home",
- icon: ,
id: "header-home",
+ icon: ,
label: {t("menus.header.home")}
},
{
key: "schedule",
id: "header-schedule",
- icon: ,
+ icon: ,
label: {t("menus.header.schedule")}
},
{
key: "jobssubmenu",
id: "header-jobs",
- icon: ,
+ icon: ,
label: t("menus.header.jobs"),
children: [
{
@@ -456,20 +369,14 @@ function Header({
icon: ,
label: {t("menus.header.newjob")}
},
- {
- type: "divider",
- id: "header-jobs-divider"
- },
+ { type: "divider" },
{
key: "alljobs",
id: "header-all-jobs",
icon: ,
label: {t("menus.header.alljobs")}
},
- {
- type: "divider",
- id: "header-jobs-divider2"
- },
+ { type: "divider" },
{
key: "productionlist",
id: "header-production-list",
@@ -479,7 +386,7 @@ function Header({
{
key: "productionboard",
id: "header-production-board",
- icon: ,
+ icon: ,
label: (
@@ -488,10 +395,7 @@ function Header({
)
},
- {
- type: "divider",
- id: "header-jobs-divider3"
- },
+ { type: "divider" },
{
key: "scoreboard",
id: "header-scoreboard",
@@ -508,8 +412,8 @@ function Header({
},
{
key: "customers",
- icon: ,
id: "header-customers",
+ icon: ,
label: t("menus.header.customers"),
children: [
{
@@ -614,12 +518,7 @@ function Header({
id: "header-create-task",
icon: ,
label: t("menus.header.create_task"),
- onClick: () => {
- setTaskUpsertContext({
- actions: {},
- context: {}
- });
- }
+ onClick: () => setTaskUpsertContext({ actions: {}, context: {} })
},
{
key: "mytasks",
@@ -644,7 +543,7 @@ function Header({
{
key: "shop",
id: "header-shop",
- icon: ,
+ icon: ,
label: {t("menus.header.shop_config")}
},
{
@@ -662,23 +561,18 @@ function Header({
id: "header-reportcenter",
icon: ,
label: t("menus.header.reportcenter"),
- onClick: () => {
- setReportCenterContext({
- actions: {},
- context: {}
- });
- }
+ onClick: () => setReportCenterContext({ actions: {}, context: {} })
},
{
key: "shop-vendors",
id: "header-shop-vendors",
- icon: ,
+ icon: ,
label: {t("menus.header.shop_vendors")}
},
{
key: "shop-csi",
id: "header-shop-csi",
- icon: ,
+ icon: ,
label: (
@@ -689,23 +583,10 @@ function Header({
}
]
},
- // Right-aligned items on desktop, merged on mobile
- {
- key: "notifications",
- icon: unreadLoading ? (
-
- ) : (
-
-
-
- ),
- id: "header-notifications",
- onClick: handleNotificationClick
- },
{
key: "recent",
- icon: ,
id: "header-recent",
+ icon: ,
children: recentItems.map((i, idx) => ({
key: idx,
id: `header-recent-${idx}`,
@@ -714,13 +595,13 @@ function Header({
},
{
key: "user",
+ id: "header-user",
icon: ,
- // label: currentUser.displayName || currentUser.email || t("general.labels.unknown"),
children: [
{
key: "signout",
id: "header-signout",
- icon: ,
+ icon: ,
danger: true,
label: t("user.actions.signout"),
onClick: () => signOutStart()
@@ -728,32 +609,25 @@ function Header({
{
key: "help",
id: "header-help",
- icon: ,
+ icon: ,
label: t("menus.header.help"),
- onClick: () => {
- window.open("https://help.imex.online/", "_blank");
- }
+ onClick: () => window.open("https://help.imex.online/", "_blank")
},
- ...(InstanceRenderManager({
- imex: true,
- rome: false
- })
+ ...(InstanceRenderManager({ imex: true, rome: false })
? [
{
key: "rescue",
id: "header-rescue",
- icon: ,
+ icon: ,
label: t("menus.header.rescueme"),
- onClick: () => {
- window.open("https://imexrescue.com/", "_blank");
- }
+ onClick: () => window.open("https://imexrescue.com/", "_blank")
}
]
: []),
{
key: "shiftclock",
id: "header-shiftclock",
- icon: ,
+ icon: ,
label: (
@@ -772,58 +646,59 @@ function Header({
}
];
+ // Notifications item (always on the right)
+ const notificationItem = [
+ {
+ key: "notifications",
+ id: "header-notifications",
+ icon: unreadLoading ? (
+
+ ) : (
+
+
+
+ ),
+ onClick: handleNotificationClick
+ }
+ ];
+
return (
-
- {isMobile ? (
+
+
- ) : (
-
-
-
-
-
setNotificationVisible(false)} />
-
- )}
+ />
+
+
+ setNotificationVisible(false)} />
);
}
diff --git a/server/notifications/scenarioParser.js b/server/notifications/scenarioParser.js
index 5f40f910e..83497d357 100644
--- a/server/notifications/scenarioParser.js
+++ b/server/notifications/scenarioParser.js
@@ -16,7 +16,7 @@ const { dispatchEmailsToQueue } = require("./queues/emailQueue");
const { dispatchAppsToQueue } = require("./queues/appQueue");
// If true, the user who commits the action will NOT receive notifications; if false, they will.
-const FILTER_SELF_FROM_WATCHERS = (() => process.env.NODE_ENV === "production")();
+const FILTER_SELF_FROM_WATCHERS = process.env?.FILTER_SELF_FROM_WATCHERS === "true";
/**
* Parses an event and determines matching scenarios for notifications.