From 1440a6022847a80877a5f84859e54664aebf4a23 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 12 Nov 2024 12:31:46 -0800 Subject: [PATCH] feature/IO-3026-Enhanced-Notifications - Initial commit Signed-off-by: Dave Richer --- client/src/contexts/SocketIO/useSocket.js | 21 ++- .../pages/manage/manage.page.component.jsx | 120 +++++++++++++++++- .../redux/application/application.actions.js | 11 ++ .../redux/application/application.reducer.js | 23 +++- .../application/application.selectors.js | 1 + .../redux/application/application.types.js | 4 +- 6 files changed, 167 insertions(+), 13 deletions(-) diff --git a/client/src/contexts/SocketIO/useSocket.js b/client/src/contexts/SocketIO/useSocket.js index 11577c906..1c5a058fc 100644 --- a/client/src/contexts/SocketIO/useSocket.js +++ b/client/src/contexts/SocketIO/useSocket.js @@ -1,8 +1,9 @@ -import { useEffect, useState, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import SocketIO from "socket.io-client"; import { auth } from "../../firebase/firebase.utils"; import { store } from "../../redux/store"; -import { setWssStatus } from "../../redux/application/application.actions"; +import { addAlerts, setWssStatus } from "../../redux/application/application.actions"; + const useSocket = (bodyshop) => { const socketRef = useRef(null); const [clientId, setClientId] = useState(null); @@ -31,6 +32,14 @@ const useSocket = (bodyshop) => { socketRef.current = socketInstance; const handleBodyshopMessage = (message) => { + if (!message || !message?.type) return; + + switch (message.type) { + case "alert-update": + store.dispatch(addAlerts(message.payload)); + break; + } + if (!import.meta.env.DEV) return; console.log(`Received message for bodyshop ${bodyshop.id}:`, message); }; @@ -39,22 +48,22 @@ const useSocket = (bodyshop) => { console.log("Socket connected:", socketInstance.id); socketInstance.emit("join-bodyshop-room", bodyshop.id); setClientId(socketInstance.id); - store.dispatch(setWssStatus("connected")) + store.dispatch(setWssStatus("connected")); }; const handleReconnect = (attempt) => { console.log(`Socket reconnected after ${attempt} attempts`); - store.dispatch(setWssStatus("connected")) + store.dispatch(setWssStatus("connected")); }; const handleConnectionError = (err) => { console.error("Socket connection error:", err); - store.dispatch(setWssStatus("error")) + store.dispatch(setWssStatus("error")); }; const handleDisconnect = () => { console.log("Socket disconnected"); - store.dispatch(setWssStatus("disconnected")) + store.dispatch(setWssStatus("disconnected")); }; socketInstance.on("connect", handleConnect); diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index 46d66016f..a6ef08753 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -1,4 +1,4 @@ -import { FloatButton, Layout, Spin } from "antd"; +import { FloatButton, Layout, notification, Spin } from "antd"; // import preval from "preval.macro"; import React, { lazy, Suspense, useContext, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -21,11 +21,12 @@ import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-st import { requestForToken } from "../../firebase/firebase.utils"; import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; 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 WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx"; +import { selectAlerts } from "../../redux/application/application.selectors.js"; +import { addAlerts } from "../../redux/application/application.actions.js"; const JobsPage = lazy(() => import("../jobs/jobs.page")); @@ -104,16 +105,125 @@ const { Content, Footer } = Layout; const mapStateToProps = createStructuredSelector({ conflict: selectInstanceConflict, - bodyshop: selectBodyshop + bodyshop: selectBodyshop, + alerts: selectAlerts }); -const mapDispatchToProps = (dispatch) => ({}); +// images.imex.online/alerts/alerts.json +const ALERT_FILE_URL = "http://localhost:5000/alerts.json"; -export function Manage({ conflict, bodyshop }) { +const mapDispatchToProps = (dispatch) => ({ + setAlerts: (alerts) => dispatch(addAlerts(alerts)) +}); + +export function Manage({ conflict, bodyshop, alerts, setAlerts }) { const { t } = useTranslation(); const [chatVisible] = useState(false); const { socket, clientId } = useContext(SocketContext); + // State to track displayed alerts + const [displayedAlertIds, setDisplayedAlertIds] = useState([]); + + // Fetch displayed alerts from localStorage on mount + useEffect(() => { + const displayedAlerts = JSON.parse(localStorage.getItem("displayedAlerts") || "[]"); + setDisplayedAlertIds(displayedAlerts); + }, []); + + // Fetch alerts from the JSON file and dispatch to Redux store + useEffect(() => { + const fetchAlerts = async () => { + try { + const response = await fetch(ALERT_FILE_URL); + const fetchedAlerts = await response.json(); + setAlerts(fetchedAlerts); + } catch (error) { + console.error("Error fetching alerts:", error); + } + }; + + fetchAlerts(); + }, []); + + useEffect(() => { + console.log("Alerts in Manage component:", alerts); + if (alerts && Object.keys(alerts).length > 0) { + // Convert the alerts object into an array + const alertArray = Object.values(alerts); + + // Filter out alerts that have already been displayed + const newAlerts = alertArray.filter((alert) => !displayedAlertIds.includes(alert.id)); + + console.log("New alerts to display:", newAlerts); + + newAlerts.forEach((alert) => { + // Display the notification + notification.open({ + key: "notification-alerts-" + alert.id, + message: alert.message, + description: alert.description, + type: alert.type || "info", + duration: 0, + placement: "bottomRight", + closable: true + }); + + // Update displayed alerts state and localStorage + setDisplayedAlertIds((prevIds) => { + const updatedIds = [...prevIds, alert.id]; + localStorage.setItem("displayedAlerts", JSON.stringify(updatedIds)); + return updatedIds; + }); + }); + } + }, [alerts]); + + // useEffect(() => { + // const fetchAlerts = async () => { + // try { + // const response = await fetch(ALERT_FILE_URL); + // + // // Check if the response is OK (status in the range 200-299) + // if (!response.ok) { + // console.error(`Network response was not ok: ${response.status} ${response.statusText}`); + // return; // Exit the function early since we can't proceed + // } + // + // const alerts = await response.json(); + // + // // Check if alerts is an array + // if (!Array.isArray(alerts)) { + // console.error("Alerts data is not an array"); + // return; + // } + // + // const displayedAlerts = JSON.parse(localStorage.getItem("displayedAlerts") || "[]"); + // const alertsNotDisplayed = alerts.filter((alert) => !displayedAlerts.includes(alert.id)); + // + // // Display notifications for alerts not yet displayed + // alertsNotDisplayed.forEach((alert) => { + // // Update localStorage immediately to prevent duplicate notifications + // displayedAlerts.push(alert.id); + // localStorage.setItem("displayedAlerts", JSON.stringify(displayedAlerts)); + // + // notification.open({ + // key: "notification-alerts-" + alert.id, + // message: alert.message, + // description: alert.description, + // type: alert.type || "info", + // duration: 0, + // placement: "bottomRight", + // closable: true + // }); + // }); + // } catch (error) { + // console.error("Error fetching alerts:", error); + // } + // }; + // + // fetchAlerts(); + // }, []); + useEffect(() => { const widgetId = InstanceRenderManager({ imex: "IABVNO4scRKY11XBQkNr", diff --git a/client/src/redux/application/application.actions.js b/client/src/redux/application/application.actions.js index c8246022b..04f880b9a 100644 --- a/client/src/redux/application/application.actions.js +++ b/client/src/redux/application/application.actions.js @@ -67,6 +67,17 @@ export const setUpdateAvailable = (isUpdateAvailable) => ({ type: ApplicationActionTypes.SET_UPDATE_AVAILABLE, payload: isUpdateAvailable }); + +export const setAlerts = (alerts) => ({ + type: ApplicationActionTypes.SET_ALERTS, + payload: alerts +}); + +export const addAlerts = (alerts) => ({ + type: ApplicationActionTypes.ADD_ALERTS, + payload: alerts +}); + export const setWssStatus = (status) => ({ type: ApplicationActionTypes.SET_WSS_STATUS, payload: status diff --git a/client/src/redux/application/application.reducer.js b/client/src/redux/application/application.reducer.js index 21878e52a..56327a0c0 100644 --- a/client/src/redux/application/application.reducer.js +++ b/client/src/redux/application/application.reducer.js @@ -15,7 +15,8 @@ const INITIAL_STATE = { error: null }, jobReadOnly: false, - partnerVersion: null + partnerVersion: null, + alerts: {} }; const applicationReducer = (state = INITIAL_STATE, action) => { @@ -91,6 +92,26 @@ const applicationReducer = (state = INITIAL_STATE, action) => { case ApplicationActionTypes.SET_WSS_STATUS: { return { ...state, wssStatus: action.payload }; } + case ApplicationActionTypes.SET_ALERTS: { + const alertsMap = {}; + action.payload.forEach((alert) => { + alertsMap[alert.id] = alert; + }); + return { + ...state, + alerts: alertsMap + }; + } + case ApplicationActionTypes.ADD_ALERTS: { + const newAlertsMap = { ...state.alerts }; + action.payload.forEach((alert) => { + newAlertsMap[alert.id] = alert; + }); + return { + ...state, + alerts: newAlertsMap + }; + } default: return state; } diff --git a/client/src/redux/application/application.selectors.js b/client/src/redux/application/application.selectors.js index a4f434cfe..6b0d1c2c4 100644 --- a/client/src/redux/application/application.selectors.js +++ b/client/src/redux/application/application.selectors.js @@ -23,3 +23,4 @@ export const selectOnline = createSelector([selectApplication], (application) => export const selectProblemJobs = createSelector([selectApplication], (application) => application.problemJobs); export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable); export const selectWssStatus = createSelector([selectApplication], (application) => application.wssStatus); +export const selectAlerts = createSelector([selectApplication], (application) => application.alerts); diff --git a/client/src/redux/application/application.types.js b/client/src/redux/application/application.types.js index 1672cda0b..3665d6193 100644 --- a/client/src/redux/application/application.types.js +++ b/client/src/redux/application/application.types.js @@ -13,6 +13,8 @@ const ApplicationActionTypes = { INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL", SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS", SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE", - SET_WSS_STATUS: "SET_WSS_STATUS" + SET_WSS_STATUS: "SET_WSS_STATUS", + SET_ALERTS: "SET_ALERTS", + ADD_ALERTS: "ADD_ALERTS" }; export default ApplicationActionTypes;