diff --git a/backend/src/controllers/MessageController.ts b/backend/src/controllers/MessageController.ts index 4c33aef..b2080d8 100644 --- a/backend/src/controllers/MessageController.ts +++ b/backend/src/controllers/MessageController.ts @@ -66,7 +66,7 @@ export const store = async (req: Request, res: Response): Promise => { }); const io = getIO(); - io.to(ticketId).to("notification").emit("appMessage", { + io.to(ticketId).to("notification").to(ticket.status).emit("appMessage", { action: "create", message, ticket, diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index ef30029..9795df3 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -18,6 +18,7 @@ type IndexQuery = { interface TicketData { contactId: number; status: string; + userId: number; } export const index = async (req: Request, res: Response): Promise => { @@ -46,12 +47,12 @@ export const index = async (req: Request, res: Response): Promise => { }; export const store = async (req: Request, res: Response): Promise => { - const { contactId, status }: TicketData = req.body; + const { contactId, status, userId }: TicketData = req.body; - const ticket = await CreateTicketService({ contactId, status }); + const ticket = await CreateTicketService({ contactId, status, userId }); const io = getIO(); - io.to("notification").emit("ticket", { + io.to(ticket.status).emit("ticket", { action: "create", ticket }); @@ -66,10 +67,21 @@ export const update = async ( const { ticketId } = req.params; const ticketData: TicketData = req.body; - const ticket = await UpdateTicketService({ ticketData, ticketId }); + const { ticket, oldStatus } = await UpdateTicketService({ + ticketData, + ticketId + }); const io = getIO(); - io.to("notification").emit("ticket", { + + if (ticket.status !== oldStatus) { + io.to(oldStatus).emit("ticket", { + action: "delete", + ticketId: ticket.id + }); + } + + io.to(ticket.status).emit("ticket", { action: "updateStatus", ticket }); @@ -83,13 +95,15 @@ export const remove = async ( ): Promise => { const { ticketId } = req.params; - await DeleteTicketService(ticketId); + const ticket = await DeleteTicketService(ticketId); const io = getIO(); - io.to("notification").emit("ticket", { - action: "delete", - ticketId: +ticketId - }); + io.to(ticket.status) + .to("notification") + .emit("ticket", { + action: "delete", + ticketId: +ticketId + }); return res.status(200).json({ message: "ticket deleted" }); }; diff --git a/backend/src/helpers/SetTicketMessagesAsRead.ts b/backend/src/helpers/SetTicketMessagesAsRead.ts index c6bbff9..fea43e3 100644 --- a/backend/src/helpers/SetTicketMessagesAsRead.ts +++ b/backend/src/helpers/SetTicketMessagesAsRead.ts @@ -24,7 +24,7 @@ const SetTicketMessagesAsRead = async (ticket: Ticket): Promise => { } const io = getIO(); - io.to("notification").emit("ticket", { + io.to(ticket.status).to("notification").emit("ticket", { action: "updateUnread", ticketId: ticket.id }); diff --git a/backend/src/libs/socket.ts b/backend/src/libs/socket.ts index cdeacc1..9e983ad 100644 --- a/backend/src/libs/socket.ts +++ b/backend/src/libs/socket.ts @@ -18,6 +18,11 @@ export const initIO = (httpServer: Server): SocketIO => { socket.join("notification"); }); + socket.on("joinTickets", status => { + console.log(`A client joined to ${status} tickets channel.`); + socket.join(status); + }); + socket.on("disconnect", () => { console.log("Client disconnected"); }); diff --git a/backend/src/services/TicketServices/CreateTicketService.ts b/backend/src/services/TicketServices/CreateTicketService.ts index 6dfefff..9131669 100644 --- a/backend/src/services/TicketServices/CreateTicketService.ts +++ b/backend/src/services/TicketServices/CreateTicketService.ts @@ -4,12 +4,14 @@ import Ticket from "../../models/Ticket"; interface Request { contactId: number; - status?: string; + status: string; + userId: number; } const CreateTicketService = async ({ contactId, - status + status, + userId }: Request): Promise => { const defaultWhatsapp = await GetDefaultWhatsApp(); @@ -19,7 +21,8 @@ const CreateTicketService = async ({ const { id }: Ticket = await defaultWhatsapp.$create("ticket", { contactId, - status + status, + userId }); const ticket = await Ticket.findByPk(id, { include: ["contact"] }); diff --git a/backend/src/services/TicketServices/DeleteTicketService.ts b/backend/src/services/TicketServices/DeleteTicketService.ts index 9e1f579..1906f08 100644 --- a/backend/src/services/TicketServices/DeleteTicketService.ts +++ b/backend/src/services/TicketServices/DeleteTicketService.ts @@ -1,7 +1,7 @@ import Ticket from "../../models/Ticket"; import AppError from "../../errors/AppError"; -const DeleteTicketService = async (id: string): Promise => { +const DeleteTicketService = async (id: string): Promise => { const ticket = await Ticket.findOne({ where: { id } }); @@ -11,6 +11,8 @@ const DeleteTicketService = async (id: string): Promise => { } await ticket.destroy(); + + return ticket; }; export default DeleteTicketService; diff --git a/backend/src/services/TicketServices/UpdateTicketService.ts b/backend/src/services/TicketServices/UpdateTicketService.ts index f7b893d..0753d52 100644 --- a/backend/src/services/TicketServices/UpdateTicketService.ts +++ b/backend/src/services/TicketServices/UpdateTicketService.ts @@ -12,10 +12,15 @@ interface Request { ticketId: string; } +interface Response { + ticket: Ticket; + oldStatus: string; +} + const UpdateTicketService = async ({ ticketData, ticketId -}: Request): Promise => { +}: Request): Promise => { const { status, userId } = ticketData; const ticket = await Ticket.findOne({ @@ -33,12 +38,14 @@ const UpdateTicketService = async ({ throw new AppError("No ticket found with this ID.", 404); } + const oldStatus = ticket.status; + await ticket.update({ status, userId }); - return ticket; + return { ticket, oldStatus }; }; export default UpdateTicketService; diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 8fc37e2..6c7d218 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -138,12 +138,15 @@ const handleMessage = async ( } const io = getIO(); - io.to(ticket.id.toString()).to("notification").emit("appMessage", { - action: "create", - message: newMessage, - ticket, - contact - }); + io.to(ticket.id.toString()) + .to(ticket.status) + .to("notification") + .emit("appMessage", { + action: "create", + message: newMessage, + ticket, + contact + }); }; const isValidMsg = (msg: WbotMessage): boolean => { diff --git a/frontend/src/components/TicketsList/index.js b/frontend/src/components/TicketsList/index.js index d34b9bd..ed922e6 100644 --- a/frontend/src/components/TicketsList/index.js +++ b/frontend/src/components/TicketsList/index.js @@ -1,4 +1,5 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useReducer } from "react"; +import openSocket from "socket.io-client"; import { makeStyles } from "@material-ui/core/styles"; import List from "@material-ui/core/List"; @@ -69,11 +70,84 @@ const useStyles = makeStyles(theme => ({ }, })); +const reducer = (state, action) => { + if (action.type === "LOAD_TICKETS") { + const newTickets = action.payload; + + newTickets.forEach(ticket => { + const ticketIndex = state.findIndex(t => t.id === ticket.id); + if (ticketIndex !== -1) { + state[ticketIndex] = ticket; + if (ticket.unreadMessages > 0) { + state.unshift(state.splice(ticketIndex, 1)[0]); + } + } else { + state.push(ticket); + } + }); + + return [...state]; + } + + if (action.type === "RESET_UNREAD") { + const ticketId = action.payload; + + const ticketIndex = state.findIndex(t => t.id === ticketId); + if (ticketIndex !== -1) { + state[ticketIndex].unreadMessages = 0; + } + return [...state]; + } + + if (action.type === "UPDATE_TICKET") { + const ticket = action.payload; + + const ticketIndex = state.findIndex(t => t.id === ticket.id); + if (ticketIndex !== -1) { + state[ticketIndex] = ticket; + } else { + state.unshift(ticket); + } + return [...state]; + } + + if (action.type === "UPDATE_TICKET_MESSAGES_COUNT") { + const ticket = action.payload; + + const ticketIndex = state.findIndex(t => t.id === ticket.id); + if (ticketIndex !== -1) { + state[ticketIndex] = ticket; + state.unshift(state.splice(ticketIndex, 1)[0]); + } + return [...state]; + } + + if (action.type === "DELETE_TICKET") { + const ticketId = action.payload; + const ticketIndex = state.findIndex(t => t.id === ticketId); + if (ticketIndex !== -1) { + state.splice(ticketIndex, 1); + } + return [...state]; + } + + if (action.type === "RESET") { + return []; + } +}; + const TicketsList = ({ status, searchParam, showAll }) => { + const userId = +localStorage.getItem("userId"); const classes = useStyles(); const [pageNumber, setPageNumber] = useState(1); + const [ticketsList, dispatch] = useReducer(reducer, []); - const { tickets, hasMore, loading, dispatch } = useTickets({ + useEffect(() => { + dispatch({ type: "RESET" }); + setPageNumber(1); + }, [status, searchParam, dispatch, showAll]); + + const { tickets, hasMore, loading } = useTickets({ pageNumber, searchParam, status, @@ -81,9 +155,52 @@ const TicketsList = ({ status, searchParam, showAll }) => { }); useEffect(() => { - dispatch({ type: "RESET" }); - setPageNumber(1); - }, [status, searchParam, dispatch, showAll]); + dispatch({ + type: "LOAD_TICKETS", + payload: tickets, + }); + }, [tickets]); + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + socket.emit("joinTickets", status); + + socket.on("ticket", data => { + if (data.action === "updateUnread") { + dispatch({ + type: "RESET_UNREAD", + payload: data.ticketId, + }); + } + + if ( + (data.action === "updateStatus" || data.action === "create") && + (!data.ticket.userId || data.ticket.userId === userId || showAll) + ) { + dispatch({ + type: "UPDATE_TICKET", + payload: data.ticket, + }); + } + + if (data.action === "delete") { + dispatch({ type: "DELETE_TICKET", payload: data.ticketId }); + } + }); + + socket.on("appMessage", data => { + if (data.action === "create") { + dispatch({ + type: "UPDATE_TICKET_MESSAGES_COUNT", + payload: data.ticket, + }); + } + }); + + return () => { + socket.disconnect(); + }; + }, [status, showAll, userId]); const loadMore = () => { setPageNumber(prevState => prevState + 1); @@ -91,7 +208,9 @@ const TicketsList = ({ status, searchParam, showAll }) => { const handleScroll = e => { if (!hasMore || loading) return; + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + if (scrollHeight - (scrollTop + 100) < clientHeight) { loadMore(); } @@ -110,16 +229,16 @@ const TicketsList = ({ status, searchParam, showAll }) => { {status === "open" && (
{i18n.t("ticketsList.assignedHeader")} - {tickets.length} + {ticketsList.length}
)} {status === "pending" && (
{i18n.t("ticketsList.pendingHeader")} - {tickets.length} + {ticketsList.length}
)} - {tickets.length === 0 && !loading ? ( + {ticketsList.length === 0 && !loading ? (
{i18n.t("ticketsList.noTicketsTitle")} @@ -130,7 +249,7 @@ const TicketsList = ({ status, searchParam, showAll }) => {
) : ( <> - {tickets.map(ticket => ( + {ticketsList.map(ticket => ( ))} diff --git a/frontend/src/hooks/useTickets/index.js b/frontend/src/hooks/useTickets/index.js index 0090b6b..b913c09 100644 --- a/frontend/src/hooks/useTickets/index.js +++ b/frontend/src/hooks/useTickets/index.js @@ -1,80 +1,8 @@ -import { useState, useEffect, useReducer } from "react"; -import openSocket from "socket.io-client"; +import { useState, useEffect } from "react"; import { toast } from "react-toastify"; import api from "../../services/api"; -const reducer = (state, action) => { - if (action.type === "LOAD_TICKETS") { - const newTickets = action.payload; - - newTickets.forEach(ticket => { - const ticketIndex = state.findIndex(t => t.id === ticket.id); - if (ticketIndex !== -1) { - state[ticketIndex] = ticket; - if (ticket.unreadMessages > 0) { - state.unshift(state.splice(ticketIndex, 1)[0]); - } - } else { - state.push(ticket); - } - }); - - return [...state]; - } - - if (action.type === "UPDATE_TICKETS") { - const { ticket, status, loggedUser, withUnreadMessages } = action.payload; - - const ticketIndex = state.findIndex(t => t.id === ticket.id); - if (ticketIndex !== -1) { - if (ticket.status !== state[ticketIndex].status) { - state.splice(ticketIndex, 1); - } else { - state[ticketIndex] = ticket; - state.unshift(state.splice(ticketIndex, 1)[0]); - } - } else if ( - ticket.status === status && - (ticket.userId === loggedUser || - !ticket.userId || - ticket.status === "closed") - ) { - state.unshift(ticket); - } else if (withUnreadMessages) { - state.unshift(ticket); - } - return [...state]; - } - - if (action.type === "DELETE_TICKET") { - const ticketId = action.payload; - - const ticketIndex = state.findIndex(t => t.id === ticketId); - if (ticketIndex !== -1) { - state.splice(ticketIndex, 1); - } - return [...state]; - } - - if (action.type === "RESET_UNREAD") { - const { ticketId, withUnreadMessages } = action.payload; - - const ticketIndex = state.findIndex(t => t.id === ticketId); - if (ticketIndex !== -1) { - state[ticketIndex].unreadMessages = 0; - if (withUnreadMessages) { - state.splice(ticketIndex, 1); - } - } - return [...state]; - } - - if (action.type === "RESET") { - return []; - } -}; - const useTickets = ({ searchParam, pageNumber, @@ -83,10 +11,9 @@ const useTickets = ({ showAll, withUnreadMessages, }) => { - const userId = +localStorage.getItem("userId"); const [loading, setLoading] = useState(true); const [hasMore, setHasMore] = useState(false); - const [tickets, dispatch] = useReducer(reducer, []); + const [tickets, setTickets] = useState([]); useEffect(() => { setLoading(true); @@ -103,10 +30,7 @@ const useTickets = ({ withUnreadMessages, }, }); - dispatch({ - type: "LOAD_TICKETS", - payload: data.tickets, - }); + setTickets(data.tickets); setHasMore(data.hasMore); setLoading(false); } catch (err) { @@ -121,57 +45,7 @@ const useTickets = ({ return () => clearTimeout(delayDebounceFn); }, [searchParam, pageNumber, status, date, showAll, withUnreadMessages]); - useEffect(() => { - const socket = openSocket(process.env.REACT_APP_BACKEND_URL); - socket.emit("joinNotification"); - - socket.on("ticket", data => { - if (data.action === "updateUnread") { - dispatch({ - type: "RESET_UNREAD", - payload: { - ticketId: data.ticketId, - withUnreadMessages: withUnreadMessages, - }, - }); - } - - if (data.action === "updateStatus" || data.action === "create") { - dispatch({ - type: "UPDATE_TICKETS", - payload: { - ticket: data.ticket, - status: status, - loggedUser: userId, - }, - }); - } - - if (data.action === "delete") { - dispatch({ type: "DELETE_TICKET", payload: data.ticketId }); - } - }); - - socket.on("appMessage", data => { - if (data.action === "create") { - dispatch({ - type: "UPDATE_TICKETS", - payload: { - ticket: data.ticket, - status: status, - withUnreadMessages: withUnreadMessages, - loggedUser: userId, - }, - }); - } - }); - - return () => { - socket.disconnect(); - }; - }, [status, withUnreadMessages, userId]); - - return { loading, tickets, hasMore, dispatch }; + return { tickets, loading, hasMore }; }; export default useTickets;