// noinspection RegExpAnonymousGroup import { BellFilled } from "@ant-design/icons"; import { useQuery } from "@apollo/client"; import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { Badge, Layout, Menu, Spin, Tooltip } from "antd"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { FaTasks } from "react-icons/fa"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { TASKS_CENTER_POLL_INTERVAL, useSocket } from "../../contexts/SocketIO/useSocket.js"; import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js"; import { QUERY_MY_TASKS_COUNT } from "../../graphql/tasks.queries.js"; import { selectDarkMode, 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 day from "../../utils/day.js"; import { useIsEmployee } from "../../utils/useIsEmployee.js"; import NotificationCenterContainer from "../notification-center/notification-center.container.jsx"; import TaskCenterContainer from "../task-center/task-center.container.jsx"; import buildAccountingChildren from "./buildAccountingChildren.jsx"; import buildLeftMenuItems from "./buildLeftMenuItems.jsx"; import { toggleDarkMode } from "../../redux/application/application.actions"; // --- Redux mappings --- const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, recentItems: selectRecentItems, selectedHeader: selectSelectedHeader, bodyshop: selectBodyshop, darkMode: selectDarkMode }); const mapDispatchToProps = (dispatch) => ({ 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, modal: "cardPayment" })), setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" })), toggleDarkMode: () => dispatch(toggleDarkMode()) }); // --- Utility Hooks --- function useUnreadNotifications(userAssociationId, isConnected, scenarioNotificationsOn) { const { data: unreadData, refetch: refetchUnread, loading: unreadLoading } = useQuery(GET_UNREAD_COUNT, { variables: { associationid: userAssociationId }, fetchPolicy: "network-only", pollInterval: isConnected ? 0 : day.duration(60, "seconds").asMilliseconds(), skip: !userAssociationId || !scenarioNotificationsOn }); const unreadCount = unreadData?.notifications_aggregate?.aggregate?.count ?? 0; useEffect(() => { if (userAssociationId) { refetchUnread().catch((e) => console.error(`Error fetching unread notifications: ${e?.message}`)); } }, [refetchUnread, userAssociationId]); useEffect(() => { if (!isConnected && !unreadLoading && userAssociationId) { refetchUnread().catch((e) => console.error(`Error fetching unread notifications: ${e?.message}`)); } }, [isConnected, unreadLoading, refetchUnread, userAssociationId]); return { unreadCount, unreadLoading }; } function useIncompleteTaskCount(assignedToId, bodyshopId, isEmployee, isConnected) { const { data: taskCountData, loading: taskCountLoading } = useQuery(QUERY_MY_TASKS_COUNT, { variables: { assigned_to: assignedToId, bodyshopid: bodyshopId }, skip: !assignedToId || !bodyshopId || !isEmployee, fetchPolicy: "network-only", pollInterval: isConnected ? 0 : TASKS_CENTER_POLL_INTERVAL }); const incompleteTaskCount = taskCountData?.tasks_aggregate?.aggregate?.count ?? 0; return { incompleteTaskCount, taskCountLoading }; } // --- Main Component --- function Header({ handleMenuClick, currentUser, bodyshop, selectedHeader, signOutStart, setBillEnterContext, setTimeTicketContext, setPaymentContext, setReportCenterContext, recentItems, setCardPaymentContext, setTaskUpsertContext, toggleDarkMode, darkMode }) { // Feature flags const { treatments: { ImEXPay, DmsAp, Simple_Inventory } } = useSplitTreatments({ attributes: {}, names: ["ImEXPay", "DmsAp", "Simple_Inventory"], splitKey: bodyshop && bodyshop.imexshopid }); // Contexts and hooks const { t } = useTranslation(); const { isConnected, scenarioNotificationsOn } = useSocket(); const [notificationVisible, setNotificationVisible] = useState(false); const [taskCenterVisible, setTaskCenterVisible] = useState(false); const baseTitleRef = useRef(document.title || ""); const lastSetTitleRef = useRef(""); const taskCenterRef = useRef(null); const notificationRef = useRef(null); const userAssociationId = bodyshop?.associations?.[0]?.id; const isEmployee = useIsEmployee(bodyshop, currentUser); // Data hooks const { unreadCount, unreadLoading } = useUnreadNotifications( userAssociationId, isConnected, scenarioNotificationsOn ); const assignedToId = bodyshop?.employees?.find((e) => e.user_email === currentUser.email)?.id; const { incompleteTaskCount, taskCountLoading } = useIncompleteTaskCount( assignedToId, bodyshop?.id, isEmployee, isConnected ); // --- Effects --- // Update document title with unread count useEffect(() => { const updateTitle = () => { const currentTitle = document.title; if (currentTitle !== lastSetTitleRef.current) { const baseTitleMatch = currentTitle.match(/^\(\d+\)\s*(.*)$/); baseTitleRef.current = baseTitleMatch ? baseTitleMatch[1] : currentTitle; } const newTitle = unreadCount > 0 ? `(${unreadCount}) ${baseTitleRef.current}` : baseTitleRef.current; if (document.title !== newTitle) { document.title = newTitle; lastSetTitleRef.current = newTitle; } }; updateTitle(); const interval = setInterval(updateTitle, 100); return () => { clearInterval(interval); document.title = baseTitleRef.current; }; }, [unreadCount]); // Handle outside clicks for popovers useEffect(() => { const handleClickOutside = (event) => { const isNotificationClick = event.target.closest("#header-notifications"); const isTaskCenterClick = event.target.closest("#header-taskcenter"); if (isNotificationClick && scenarioNotificationsOn) { setTaskCenterVisible(false); // Close task center return; } if (isTaskCenterClick) { setNotificationVisible(scenarioNotificationsOn ? false : notificationVisible); // Close notification center if enabled return; } if (taskCenterVisible && taskCenterRef.current && !taskCenterRef.current.contains(event.target)) { setTaskCenterVisible(false); } if ( scenarioNotificationsOn && notificationVisible && notificationRef.current && !notificationRef.current.contains(event.target) ) { setNotificationVisible(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [taskCenterVisible, notificationVisible, scenarioNotificationsOn]); // --- Event Handlers --- const handleTaskCenterClick = useCallback( (e) => { setTaskCenterVisible((prev) => { if (prev) return false; return true; }); if (handleMenuClick) handleMenuClick(e); }, [handleMenuClick] ); const handleNotificationClick = useCallback( (e) => { setNotificationVisible((prev) => { if (prev) return false; return true; }); if (handleMenuClick) handleMenuClick(e); }, [handleMenuClick] ); const handleDarkModeToggle = useCallback(() => { toggleDarkMode(); }, [toggleDarkMode]); // --- Menu Items --- // built externally to keep the component clean, but on this level to prevent unnecessary re-renders const accountingChildren = useMemo( () => buildAccountingChildren({ t, bodyshop, currentUser, setBillEnterContext, setPaymentContext, setCardPaymentContext, setTimeTicketContext, ImEXPay, DmsAp, Simple_Inventory }), [ t, bodyshop, currentUser, setBillEnterContext, setPaymentContext, setCardPaymentContext, setTimeTicketContext, ImEXPay, DmsAp, Simple_Inventory ] ); // Built externally to keep the component clean const leftMenuItems = useMemo( () => buildLeftMenuItems({ t, bodyshop, recentItems, setTaskUpsertContext, setReportCenterContext, signOutStart, accountingChildren, darkMode, handleDarkModeToggle }), [ t, bodyshop, recentItems, setTaskUpsertContext, setReportCenterContext, signOutStart, accountingChildren, darkMode, handleDarkModeToggle ] ); const rightMenuItems = useMemo(() => { const items = []; if (scenarioNotificationsOn) { items.push({ key: "notifications", id: "header-notifications", icon: unreadLoading ? ( ) : ( ), onClick: handleNotificationClick }); } items.push({ key: "taskcenter", id: "header-taskcenter", icon: taskCountLoading ? ( ) : ( 0 ? incompleteTaskCount : 0} showZero={false}> ), onClick: handleTaskCenterClick }); return items; }, [ scenarioNotificationsOn, unreadLoading, unreadCount, taskCountLoading, incompleteTaskCount, isEmployee, handleNotificationClick, handleTaskCenterClick, t ]); // --- Render --- return ( {scenarioNotificationsOn && ( setNotificationVisible(false)} unreadCount={unreadCount} /> )} setTaskCenterVisible(false)} /> ); } export default connect(mapStateToProps, mapDispatchToProps)(Header);