diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index 2a0b679c6..4cf5b5d35 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -21,6 +21,7 @@ import "./App.styles.scss"; import Eula from "../components/eula/eula.component"; import InstanceRenderMgr from "../utils/instanceRenderMgr"; import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx"; +import { SocketProvider } from "../contexts/SocketIO/socketContext.jsx"; const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component")); const ManagePage = lazy(() => import("../pages/manage/manage.page.container")); @@ -201,7 +202,9 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline path="/manage/*" element={ - + + + } > diff --git a/client/src/contexts/SocketIO/socketContext.jsx b/client/src/contexts/SocketIO/socketContext.jsx new file mode 100644 index 000000000..e0f0e61fc --- /dev/null +++ b/client/src/contexts/SocketIO/socketContext.jsx @@ -0,0 +1,13 @@ +import React, { createContext } from "react"; +import useSocket from "./useSocket"; // Import the custom hook + +// Create the SocketContext +const SocketContext = createContext(null); + +export const SocketProvider = ({ children, bodyshop }) => { + const { socket, clientId } = useSocket(bodyshop); + + return {children}; +}; + +export default SocketContext; diff --git a/client/src/contexts/SocketIO/useSocket.js b/client/src/contexts/SocketIO/useSocket.js new file mode 100644 index 000000000..ebaf11885 --- /dev/null +++ b/client/src/contexts/SocketIO/useSocket.js @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; +import SocketIO from "socket.io-client"; +import { auth } from "../../firebase/firebase.utils"; + +const useSocket = (bodyshop) => { + const [socket, setSocket] = useState(null); + const [clientId, setClientId] = useState(null); // State to store unique identifier + + useEffect(() => { + if (bodyshop && bodyshop.id) { + const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "https://localhost:3000"; // Use Vite proxy in development + + const socketInstance = SocketIO(endpoint, { + path: "/ws", // Ensure this matches the Vite proxy and backend path + withCredentials: true, + auth: async (callback) => { + const token = auth.currentUser && (await auth.currentUser.getIdToken()); + callback({ token }); + }, + reconnectionAttempts: Infinity, // Try reconnecting forever + reconnectionDelay: 2000, // How long to wait between reconnection attempts + reconnectionDelayMax: 10000 // Maximum delay between attempts + }); + + setSocket(socketInstance); + + socketInstance.on("connect", () => { + console.log("Socket connected:", socketInstance.id); + setClientId(socketInstance.id); + }); + + socketInstance.on("reconnect", (attempt) => { + console.log(`Socket reconnected after ${attempt} attempts`); + }); + + socketInstance.on("connect_error", (err) => { + console.error("Socket connection error:", err); + }); + + socketInstance.on("disconnect", () => { + console.log("Socket disconnected"); + }); + + return () => { + socketInstance.disconnect(); + }; + } + }, [bodyshop]); + + // Return both socket and clientId + return { socket, clientId }; +}; + +export default useSocket; diff --git a/client/src/pages/dms-payables/dms-payables.container.jsx b/client/src/pages/dms-payables/dms-payables.container.jsx index f0c0e6c33..d41c6f65d 100644 --- a/client/src/pages/dms-payables/dms-payables.container.jsx +++ b/client/src/pages/dms-payables/dms-payables.container.jsx @@ -1,16 +1,15 @@ import { Button, Card, Col, notification, Row, Select, Space } from "antd"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState, useContext } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { useLocation, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; -import SocketIO from "socket.io-client"; import DmsAllocationsSummaryApComponent from "../../components/dms-allocations-summary-ap/dms-allocations-summary-ap.component"; import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component"; -import { auth } from "../../firebase/firebase.utils"; import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import SocketContext from "../../contexts/SocketIO/socketContext"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop @@ -23,20 +22,9 @@ const mapDispatchToProps = (dispatch) => ({ export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer); -export const socket = SocketIO( - import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "https://localhost:3000", - { - path: "/ws", - withCredentials: true, - auth: async (callback) => { - const token = auth.currentUser && (await auth.currentUser.getIdToken()); - callback({ token }); - } - } -); - export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { const { t } = useTranslation(); + const { socket } = useContext(SocketContext); const [logLevel, setLogLevel] = useState("DEBUG"); const history = useNavigate(); const [logs, setLogs] = useState([]); @@ -67,40 +55,43 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { }, [t, setBreadcrumbs, setSelectedHeader]); useEffect(() => { - socket.on("connect", () => socket.emit("set-log-level", logLevel)); - socket.on("reconnect", () => { - setLogs((logs) => { - return [ + if (socket) { + const handleConnect = () => socket.emit("set-log-level", logLevel); + const handleReconnect = () => { + setLogs((logs) => [ ...logs, { timestamp: new Date(), level: "WARNING", message: "Reconnected to CDK Export Service" } - ]; - }); - }); + ]); + }; + const handleLogEvent = (payload) => { + setLogs((logs) => [...logs, payload]); + }; + const handleExportComplete = () => { + notification.open({ + type: "success", + message: t("jobs.labels.dms.apexported") + }); + }; - socket.on("log-event", (payload) => { - setLogs((logs) => { - return [...logs, payload]; - }); - }); + socket.on("connect", handleConnect); + socket.on("reconnect", handleReconnect); + socket.on("log-event", handleLogEvent); + socket.on("ap-export-complete", handleExportComplete); - socket.on("ap-export-complete", (payload) => { - notification.open({ - type: "success", - message: t("jobs.labels.dms.apexported") - }); - }); + if (socket.disconnected) socket.connect(); - if (socket.disconnected) socket.connect(); - return () => { - socket.removeAllListeners(); - socket.disconnect(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + return () => { + socket.off("connect", handleConnect); + socket.off("reconnect", handleReconnect); + socket.off("log-event", handleLogEvent); + socket.off("ap-export-complete", handleExportComplete); + }; + } + }, [socket, logLevel, t]); if (!state?.billids) { history(`/manage/accounting/payables`); @@ -133,16 +124,15 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { ERROR - + {/* {*/} + {/* setLogs([]);*/} + {/* socket.disconnect();*/} + {/* socket.connect();*/} + {/* }}*/} + {/*>*/} + {/* Reconnect*/} + {/**/} } > diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx index 056ddf08e..4c02ec399 100644 --- a/client/src/pages/dms/dms.container.jsx +++ b/client/src/pages/dms/dms.container.jsx @@ -1,12 +1,11 @@ import { useQuery } from "@apollo/client"; import { Button, Card, Col, notification, Result, Row, Select, Space } from "antd"; import queryString from "query-string"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useContext, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link, useLocation, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; -import SocketIO from "socket.io-client"; import AlertComponent from "../../components/alert/alert.component"; import DmsAllocationsSummary from "../../components/dms-allocations-summary/dms-allocations-summary.component"; import DmsCustomerSelector from "../../components/dms-customer-selector/dms-customer-selector.component"; @@ -14,12 +13,12 @@ import DmsLogEvents from "../../components/dms-log-events/dms-log-events.compone import DmsPostForm from "../../components/dms-post-form/dms-post-form.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import { OwnerNameDisplayFunction } from "../../components/owner-name-display/owner-name-display.component"; -import { auth } from "../../firebase/firebase.utils"; import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries"; import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; +import SocketContext from "../../contexts/SocketIO/socketContext"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop @@ -28,25 +27,21 @@ const mapStateToProps = createStructuredSelector({ const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), - insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })) + insertAuditTrail: ({ jobid, operation, type }) => + dispatch( + insertAuditTrail({ + jobid, + operation, + type + }) + ) }); export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer); -export const socket = SocketIO( - import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "https://localhost:3000", // for dev testing, - { - path: "/ws", - withCredentials: true, - auth: async (callback) => { - const token = auth.currentUser && (await auth.currentUser.getIdToken()); - callback({ token }); - } - } -); - export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, insertAuditTrail }) { const { t } = useTranslation(); + const { socket, clientId } = useContext(SocketContext); const [logLevel, setLogLevel] = useState("DEBUG"); const history = useNavigate(); const [logs, setLogs] = useState([]); @@ -83,47 +78,58 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse }, [t, setBreadcrumbs, setSelectedHeader]); useEffect(() => { - socket.on("connect", () => socket.emit("set-log-level", logLevel)); - socket.on("reconnect", () => { - setLogs((logs) => { - return [ + if (socket) { + const handleConnect = () => { + socket.emit("set-log-level", logLevel); + }; + + const handleReconnect = () => { + setLogs((logs) => [ ...logs, { timestamp: new Date(), level: "WARNING", message: "Reconnected to CDK Export Service" } - ]; - }); - }); - socket.on("connect_error", (err) => { - console.log(`connect_error due to ${err}`, err); - notification.error({ message: err.message }); - }); - socket.on("log-event", (payload) => { - setLogs((logs) => { - return [...logs, payload]; - }); - }); - socket.on("export-success", (payload) => { - notification.success({ - message: t("jobs.successes.exported") - }); - insertAuditTrail({ - jobid: payload, - operation: AuditTrailMapping.jobexported(), - type: "jobexported" - }); - history("/manage/accounting/receivables"); - }); + ]); + }; - if (socket.disconnected) socket.connect(); - return () => { - socket.removeAllListeners(); - socket.disconnect(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const handleConnectError = (err) => { + console.log(`connect_error due to ${err}`, err); + notification.error({ message: err.message }); + }; + + const handleLogEvent = (payload) => { + setLogs((logs) => [...logs, payload]); + }; + + const handleExportSuccess = (payload) => { + notification.success({ + message: t("jobs.successes.exported") + }); + insertAuditTrail({ + jobid: payload, + operation: AuditTrailMapping.jobexported(), + type: "jobexported" + }); + history("/manage/accounting/receivables"); + }; + + socket.on("connect", handleConnect); + socket.on("reconnect", handleReconnect); + socket.on("connect_error", handleConnectError); + socket.on("log-event", handleLogEvent); + socket.on("export-success", handleExportSuccess); + + return () => { + socket.off("connect", handleConnect); + socket.off("reconnect", handleReconnect); + socket.off("connect_error", handleConnectError); + socket.off("log-event", handleLogEvent); + socket.off("export-success", handleExportSuccess); + }; + } + }, [socket, logLevel, t, insertAuditTrail, history]); if (loading) return ; if (error) return ; @@ -180,15 +186,15 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse ERROR - + {/* {*/} + {/* setLogs([]);*/} + {/* socket.disconnect();*/} + {/* socket.connect();*/} + {/* }}*/} + {/*>*/} + {/* Reconnect*/} + {/**/} } > diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index d2f4c65d0..b91e61d61 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -1,6 +1,6 @@ import { FloatButton, Layout, Spin } from "antd"; // import preval from "preval.macro"; -import React, { lazy, Suspense, useEffect, useState } from "react"; +import React, { lazy, Suspense, useContext, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link, Route, Routes } from "react-router-dom"; @@ -18,13 +18,12 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com import PartnerPingComponent from "../../components/partner-ping/partner-ping.component"; import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container"; import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component"; -import { auth } from "../../firebase/firebase.utils"; import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors"; import UpdateAlert from "../../components/update-alert/update-alert.component"; import InstanceRenderManager from "../../utils/instanceRenderMgr.js"; import "./manage.page.styles.scss"; -import SocketIO from "socket.io-client"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; const JobsPage = lazy(() => import("../jobs/jobs.page")); @@ -111,47 +110,7 @@ const mapDispatchToProps = (dispatch) => ({}); export function Manage({ conflict, bodyshop }) { const { t } = useTranslation(); const [chatVisible] = useState(false); - const [socket, setSocket] = useState(null); // State for Socket.IO connection - - useEffect(() => { - if (bodyshop && bodyshop.id) { - const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "https://localhost:3000"; // Use Vite proxy in development - - const socketInstance = SocketIO(endpoint, { - path: "/ws", // Ensure this matches the Vite proxy and backend path - withCredentials: true, - auth: async (callback) => { - const token = auth.currentUser && (await auth.currentUser.getIdToken()); - callback({ token }); - } - }); - - setSocket(socketInstance); - - socketInstance.on("connect", () => { - console.log("Socket connected:", socketInstance.id); - socketInstance.emit("join-bodyshop-room", bodyshop.id); - }); - - socketInstance.on("bodyshop-message", (message) => { - console.log(`Received message for bodyshop ${bodyshop.id}:`, message); - }); - - socketInstance.on("connect_error", (err) => { - console.error("Socket connection error:", err); - }); - - socketInstance.on("disconnect", () => { - console.log("Socket disconnected"); - }); - - return () => { - socketInstance.emit("leave-bodyshop-room", bodyshop.id); - socketInstance.off("bodyshop-message"); - socketInstance.disconnect(); - }; - } - }, [bodyshop]); + const { socket, clientId } = useContext(SocketContext); useEffect(() => { document.title = InstanceRenderManager({ @@ -161,6 +120,27 @@ export function Manage({ conflict, bodyshop }) { }); }, [t]); + useEffect(() => { + if (socket && bodyshop && bodyshop.id) { + const handleConnect = () => { + socket.emit("join-bodyshop-room", bodyshop.id); + }; + + const handleBodyshopMessage = (message) => { + console.log(`Received message for bodyshop ${bodyshop.id}:`, message); + }; + + socket.on("connect", handleConnect); + socket.on("bodyshop-message", handleBodyshopMessage); + + return () => { + socket.emit("leave-bodyshop-room", bodyshop.id); + socket.off("connect", handleConnect); + socket.off("bodyshop-message", handleBodyshopMessage); + }; + } + }, [socket, bodyshop]); + const AppRouteTable = ( { if (socket && bodyshop && bodyshop.id) { - socket.emit("broadcast-to-bodyshop", bodyshop.id, "Hello"); - console.log(`Broadcasting message to bodyshop ${bodyshop.id}: ${"hello"}`); + console.log(`Broadcasting message to bodyshop ${bodyshop.id}:`); + socket.emit("broadcast-to-bodyshop", bodyshop.id, `Hello from ${clientId}`); } };