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.