diff --git a/client/src/components/production-board-kanban/production-board-kanban.component.jsx b/client/src/components/production-board-kanban/production-board-kanban.component.jsx index 0c515132b..4d10d9369 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.component.jsx +++ b/client/src/components/production-board-kanban/production-board-kanban.component.jsx @@ -3,7 +3,7 @@ import { useApolloClient } from "@apollo/client"; import Board from "./trello-board/index"; import { Button, notification, Skeleton, Space } from "antd"; import { PageHeader } from "@ant-design/pro-layout"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -23,6 +23,7 @@ import cloneDeep from "lodash/cloneDeep"; import isEqual from "lodash/isEqual"; import { defaultFilters, mergeWithDefaults } from "./settings/defaultKanbanSettings.js"; import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop @@ -45,10 +46,118 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr const [loading, setLoading] = useState(true); const [isMoving, setIsMoving] = useState(false); const [orientation, setOrientation] = useState("vertical"); + const { socket } = useContext(SocketContext); // Access socket from the context const { t } = useTranslation(); const client = useApolloClient(); + // Update the lanes or cards based on job update event + const handleJobUpdated = useCallback( + (updatedJob) => { + setBoardLanes((prevBoardLanes) => { + const updatedLanes = cloneDeep(prevBoardLanes.lanes); + + // Find the lane containing the card with the updated job ID + const sourceLane = updatedLanes.find((lane) => lane.cards.some((card) => card.id === updatedJob.id)); + + if (!sourceLane) { + console.error("Source lane not found for the updated job."); + return prevBoardLanes; // Return the previous state if no source lane is found + } + + // Find the card in the source lane + const cardIndex = sourceLane.cards.findIndex((card) => card.id === updatedJob.id); + const currentCard = sourceLane.cards[cardIndex]; + + if (!currentCard) { + console.error("Card not found for the updated job."); + return prevBoardLanes; // Return the previous state if the card is not found + } + + // Iterate through the properties of updatedJob and update the corresponding values in currentCard.metadata + Object.keys(updatedJob).forEach((key) => { + // Ensure the field exists in currentCard.metadata before updating it + if (key in currentCard.metadata && currentCard.metadata[key] !== updatedJob[key]) { + console.log(`Updating ${key} from ${currentCard.metadata[key]} to ${updatedJob[key]}`); + currentCard.metadata[key] = updatedJob[key]; + } + }); + + // Mark that data has been changed if any field was updated + const isDataChanged = !isEqual(currentCard.metadata, updatedJob); + + // Check if the lane (status) has changed + const isLaneChanged = updatedJob.status !== sourceLane.id; + + // Case 1: Both data and lane have changed + if (isDataChanged && isLaneChanged) { + console.log("Case 1: Data and Lane Changed"); + + // Remove the card from the source lane + const [cardToMove] = sourceLane.cards.splice(cardIndex, 1); + + // Find the target lane based on the new status + const targetLane = updatedLanes.find((lane) => lane.id === updatedJob.status); + if (targetLane) { + // Move the card to the target lane and update its data + targetLane.cards.push({ ...cardToMove, metadata: { ...currentCard.metadata } }); + + // Update the lane titles with the new card count + sourceLane.title = `${sourceLane.title.split(" ")[0]} (${sourceLane.cards.length})`; + targetLane.title = `${targetLane.title.split(" ")[0]} (${targetLane.cards.length})`; + } else { + console.error("Target lane not found for the updated job."); + } + } + // Case 2: Only data has changed + else if (isDataChanged && !isLaneChanged) { + console.log("Case 2: Only Data Changed"); + + // Update the card's metadata in place + sourceLane.cards[cardIndex] = { ...currentCard, metadata: { ...currentCard.metadata } }; + + // Force a shallow change in the source lane to trigger re-render + sourceLane.cards = [...sourceLane.cards]; + } + // Case 3: Only the lane has changed + else if (!isDataChanged && isLaneChanged) { + console.log("Case 3: Only Lane Changed"); + + // Remove the card from the source lane + const [cardToMove] = sourceLane.cards.splice(cardIndex, 1); + + // Find the target lane based on the new status + const targetLane = updatedLanes.find((lane) => lane.id === updatedJob.status); + if (targetLane) { + // Move the card to the new lane without changing its data + targetLane.cards.push(cardToMove); + + // Update the lane titles with the new card count + sourceLane.title = `${sourceLane.title.split(" ")[0]} (${sourceLane.cards.length})`; + targetLane.title = `${targetLane.title.split(" ")[0]} (${targetLane.cards.length})`; + } else { + console.error("Target lane not found for the updated job."); + } + } + + // Return the updated lanes + return { lanes: updatedLanes }; + }); + }, + [setBoardLanes] + ); + + useEffect(() => { + // Listen for the job-updated event from the socket + if (socket) { + socket.on("job-updated", handleJobUpdated); + + return () => { + socket.off("job-updated", handleJobUpdated); + }; + } + }, [socket, handleJobUpdated]); + useEffect(() => { if (associationSettings) { setLoading(true); diff --git a/client/src/components/production-board-kanban/production-board-kanban.container.jsx b/client/src/components/production-board-kanban/production-board-kanban.container.jsx index bf5940cb9..a42406dc9 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.container.jsx +++ b/client/src/components/production-board-kanban/production-board-kanban.container.jsx @@ -28,9 +28,9 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) { onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`) }); - const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, { - onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`) - }); + // const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, { + // onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`) + // }); const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, { variables: { email: currentUser.email }, @@ -40,11 +40,11 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) { // This provides us the current version of the Lanes from the Redux store // const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {})); - useEffect(() => { - if (updatedJobs && data) { - refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`)); - } - }, [updatedJobs, data, refetch]); + // useEffect(() => { + // if (updatedJobs && data) { + // refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`)); + // } + // }, [updatedJobs, data, refetch]); const filteredAssociationSettings = useMemo(() => { return associationSettings?.associations[0] || null; diff --git a/client/src/contexts/SocketIO/useSocket.js b/client/src/contexts/SocketIO/useSocket.js index b7e69d321..f60f1b95f 100644 --- a/client/src/contexts/SocketIO/useSocket.js +++ b/client/src/contexts/SocketIO/useSocket.js @@ -7,6 +7,10 @@ const useSocket = (bodyshop) => { const [clientId, setClientId] = useState(null); // State to store unique identifier useEffect(() => { + const handleBodyshopMessage = (message) => { + console.log(`Received message for bodyshop ${bodyshop.id}:`, message); + }; + if (bodyshop && bodyshop.id) { const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "https://localhost:3000"; @@ -27,6 +31,11 @@ const useSocket = (bodyshop) => { socketInstance.on("connect", () => { console.log("Socket connected:", socketInstance.id); setClientId(socketInstance.id); + + if (bodyshop.id) { + socketInstance.on("bodyshop-message", handleBodyshopMessage); + socketInstance.emit("join-bodyshop-room", bodyshop.id); + } }); socketInstance.on("reconnect", (attempt) => { @@ -41,6 +50,14 @@ const useSocket = (bodyshop) => { console.log("Socket disconnected"); }); + if (bodyshop?.id) { + return () => { + socketInstance.emit("leave-bodyshop-room", bodyshop.id); + socketInstance.off("bodyshop-message", handleBodyshopMessage); + socketInstance.disconnect(); + }; + } + return () => { socketInstance.disconnect(); }; @@ -52,3 +69,24 @@ const useSocket = (bodyshop) => { }; export default useSocket; + +// 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]); diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index b91e61d61..b49d4bdba 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -120,27 +120,6 @@ 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 = ( { const { io, logger } = req; + + logger.log("job-update", "INFO", req.user?.email, null, { + message: `Job updated event received from Hasura`, + body: req?.body + }); + if (!req?.body?.event?.data?.new || !isObject(req?.body?.event?.data?.new)) { logger.log("job-update-error", "ERROR", req.user?.email, null, { message: `Malformed Job Update request sent from Hasura`, @@ -13,9 +19,14 @@ const jobUpdated = async (req, res) => { message: `Malformed Job Update request sent from Hasura` }); } - const bodyshopID = req.body.event.data.new.shopid; - return res.json({ message: "Job updated" }); + const updatedJob = req.body.event.data.new; + const bodyshopID = updatedJob.shopid; + + // Emit the job-updated event only to the room corresponding to the bodyshop + io.to(bodyshopID).emit("job-updated", updatedJob); + + return res.json({ message: "Job updated and event emitted" }); }; module.exports = jobUpdated;