diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b23d587 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "pwa-chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/backend/.sequelizerc b/backend/.sequelizerc index 23abc5c..e5241e2 100644 --- a/backend/.sequelizerc +++ b/backend/.sequelizerc @@ -4,4 +4,5 @@ module.exports = { config: resolve(__dirname, "src", "config", "database.js"), "modules-path": resolve(__dirname, "src", "models"), "migrations-path": resolve(__dirname, "src", "database", "migrations"), + "seeders-path": resolve(__dirname, "src", "database", "seeds"), }; diff --git a/backend/src/controllers/MessageController.js b/backend/src/controllers/MessageController.js index 2c962fe..064a2d1 100644 --- a/backend/src/controllers/MessageController.js +++ b/backend/src/controllers/MessageController.js @@ -126,7 +126,6 @@ exports.store = async (req, res, next) => { }); if (media) { - console.log(media); const newMedia = MessageMedia.fromFilePath(req.file.path); message.mediaUrl = req.file.filename; diff --git a/backend/src/controllers/TicketController.js b/backend/src/controllers/TicketController.js index d707a77..e0ff97b 100644 --- a/backend/src/controllers/TicketController.js +++ b/backend/src/controllers/TicketController.js @@ -27,14 +27,16 @@ exports.index = async (req, res) => { }, ]; - if (status === "open") { + if (status) { whereCondition = { ...whereCondition, - status: { [Sequelize.Op.or]: ["pending", "open"] }, + status: status, }; - } else if (status === "closed") { - whereCondition = { ...whereCondition, status: "closed" }; - } else if (searchParam) { + } + // else if (status === "closed") { + // whereCondition = { ...whereCondition, status: "closed" }; + // } + else if (searchParam) { includeCondition = [ ...includeCondition, { diff --git a/backend/src/database/seeds/20200824172424-create-contacts.js b/backend/src/database/seeds/20200824172424-create-contacts.js new file mode 100644 index 0000000..54a55cd --- /dev/null +++ b/backend/src/database/seeds/20200824172424-create-contacts.js @@ -0,0 +1,48 @@ +"use strict"; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.bulkInsert( + "Contacts", + [ + { + name: "Joana Doe", + profilePicUrl: + "https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1834&q=80", + number: 5512345678, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + name: "John Rulles", + profilePicUrl: + "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80", + number: 5512345679, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + name: "Jonas Jhones", + profilePicUrl: + "https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80", + number: 5512345680, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + name: "Julie June", + profilePicUrl: + "https://images.unsplash.com/photo-1493666438817-866a91353ca9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1049&q=80", + number: 5512345681, + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + {} + ); + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.bulkDelete("Contacts", null, {}); + }, +}; diff --git a/backend/src/database/seeds/20200824173654-create-tickets.js b/backend/src/database/seeds/20200824173654-create-tickets.js new file mode 100644 index 0000000..83fd634 --- /dev/null +++ b/backend/src/database/seeds/20200824173654-create-tickets.js @@ -0,0 +1,261 @@ +"use strict"; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.bulkInsert( + "Tickets", + [ + { + status: "pending", + lastMessage: "hello!", + contactId: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 2, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 2, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 2, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 2, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 2, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 2, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 3, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 3, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 3, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 3, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 3, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 3, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 3, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 4, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 4, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 4, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 4, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 4, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 4, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 4, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 4, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 2, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 2, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 2, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 2, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 2, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + status: "pending", + lastMessage: "hello!", + contactId: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + {} + ); + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.bulkDelete("Tickets", null, {}); + }, +}; diff --git a/backend/src/database/seeds/20200824174824-create-messages.js b/backend/src/database/seeds/20200824174824-create-messages.js new file mode 100644 index 0000000..0037136 --- /dev/null +++ b/backend/src/database/seeds/20200824174824-create-messages.js @@ -0,0 +1,148 @@ +"use strict"; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.bulkInsert( + "Messages", + [ + { + id: "12312321342", + body: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ack: 0, + ticketId: 1, + fromMe: false, + read: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "12312321313", + body: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ack: 3, + ticketId: 1, + fromMe: true, + read: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "12312321314", + body: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ack: 3, + ticketId: 1, + fromMe: true, + read: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "12312321315", + body: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ack: 0, + ticketId: 1, + fromMe: false, + read: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "12312321316", + body: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ack: 0, + ticketId: 5, + fromMe: false, + read: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "12312321355", + body: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ack: 3, + ticketId: 5, + fromMe: true, + read: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "12312321318", + body: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ack: 3, + ticketId: 5, + fromMe: true, + read: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "12312321319", + body: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ack: 0, + ticketId: 5, + fromMe: false, + read: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "12312321399", + body: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ack: 0, + ticketId: 11, + fromMe: false, + read: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "12312321391", + body: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ack: 3, + ticketId: 11, + fromMe: true, + read: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "12312321392", + body: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ack: 3, + ticketId: 11, + fromMe: true, + read: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "12312321393", + body: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ack: 0, + ticketId: 11, + fromMe: false, + read: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + {} + ); + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.bulkDelete("Messages", null, {}); + }, +}; diff --git a/frontend/src/components/TicketListItem/index.js b/frontend/src/components/TicketListItem/index.js new file mode 100644 index 0000000..e8f2a89 --- /dev/null +++ b/frontend/src/components/TicketListItem/index.js @@ -0,0 +1,183 @@ +import React from "react"; + +import { parseISO, format, isSameDay } from "date-fns"; + +import { makeStyles } from "@material-ui/core/styles"; +import { green } from "@material-ui/core/colors"; +import ListItem from "@material-ui/core/ListItem"; +import ListItemText from "@material-ui/core/ListItemText"; +import ListItemAvatar from "@material-ui/core/ListItemAvatar"; +import Typography from "@material-ui/core/Typography"; +import Avatar from "@material-ui/core/Avatar"; +import Divider from "@material-ui/core/Divider"; +import Badge from "@material-ui/core/Badge"; +import Button from "@material-ui/core/Button"; + +import { i18n } from "../../translate/i18n"; + +const useStyles = makeStyles(theme => ({ + ticket: { + position: "relative", + "& .hidden-button": { + display: "none", + }, + "&:hover .hidden-button": { + display: "flex", + position: "absolute", + left: "50%", + }, + }, + noTicketsDiv: { + display: "flex", + height: "100px", + margin: 40, + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + }, + + noTicketsText: { + textAlign: "center", + color: "rgb(104, 121, 146)", + fontSize: "14px", + lineHeight: "1.4", + }, + + noTicketsTitle: { + textAlign: "center", + fontSize: "16px", + fontWeight: "600", + margin: "0px", + }, + + contactNameWrapper: { + display: "flex", + justifyContent: "space-between", + }, + + lastMessageTime: { + justifySelf: "flex-end", + }, + + closedBadge: { + alignSelf: "center", + justifySelf: "flex-end", + marginRight: 32, + marginLeft: "auto", + }, + + contactLastMessage: { + paddingRight: 20, + }, + + newMessagesCount: { + alignSelf: "center", + marginRight: 8, + marginLeft: "auto", + }, + + badgeStyle: { + color: "white", + backgroundColor: green[500], + }, +})); + +const TicketListItem = ({ + ticket, + handleAcepptTicket, + handleSelectTicket, + ticketId, +}) => { + const classes = useStyles(); + + return ( + + { + if (ticket.status === "pending" && handleAcepptTicket) return; + handleSelectTicket(e, ticket); + }} + selected={ticketId && +ticketId === ticket.id} + className={classes.ticket} + > + + + + + + {ticket.contact.name} + + {ticket.status === "closed" && ( + + )} + {ticket.lastMessage && ( + + {isSameDay(parseISO(ticket.updatedAt), new Date()) ? ( + <>{format(parseISO(ticket.updatedAt), "HH:mm")}> + ) : ( + <>{format(parseISO(ticket.updatedAt), "dd/MM/yyyy")}> + )} + + )} + + } + secondary={ + + + {ticket.lastMessage || } + + + + + } + /> + {ticket.status === "pending" && handleAcepptTicket ? ( + handleAcepptTicket(ticket.id)} + > + {i18n.t("ticketsList.buttons.accept")} + + ) : null} + + + + ); +}; + +export default TicketListItem; diff --git a/frontend/src/components/Tickets/index.js b/frontend/src/components/Tickets/index.js index f9c8d0c..f347d91 100644 --- a/frontend/src/components/Tickets/index.js +++ b/frontend/src/components/Tickets/index.js @@ -1,7 +1,5 @@ -import React, { useState, useEffect, useReducer } from "react"; +import React, { useState, useEffect } from "react"; import { useHistory, useParams } from "react-router-dom"; -import openSocket from "socket.io-client"; -import { toast } from "react-toastify"; import { makeStyles } from "@material-ui/core/styles"; import Paper from "@material-ui/core/Paper"; @@ -16,14 +14,19 @@ import IconButton from "@material-ui/core/IconButton"; import AddIcon from "@material-ui/icons/Add"; import FormControlLabel from "@material-ui/core/FormControlLabel"; import Switch from "@material-ui/core/Switch"; +import ListItem from "@material-ui/core/ListItem"; +import ListItemText from "@material-ui/core/ListItemText"; import TicketsSkeleton from "../TicketsSkeleton"; import NewTicketModal from "../NewTicketModal"; -import TicketsList from "../TicketsList"; +// import TicketsList from "../TicketsList"; +import TicketListItem from "../TicketListItem"; + import TabPanel from "../TabPanel"; import { i18n } from "../../translate/i18n"; import api from "../../services/api"; +import useTickets from "../../hooks/useTickets"; const useStyles = makeStyles(theme => ({ ticketsWrapper: { @@ -154,140 +157,45 @@ 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 === "UPDATE_TICKETS") { - 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]); - } else { - 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 = action.payload; - - const ticketIndex = state.findIndex(t => t.id === ticketId); - if (ticketIndex !== -1) { - state[ticketIndex].unreadMessages = 0; - } - return [...state]; - } - - if (action.type === "RESET") { - return []; - } -}; - const Tickets = () => { const classes = useStyles(); const history = useHistory(); - const token = localStorage.getItem("token"); const userId = +localStorage.getItem("userId"); const { ticketId } = useParams(); - const [loading, setLoading] = useState(false); const [searchParam, setSearchParam] = useState(""); const [tab, setTab] = useState("open"); const [newTicketModalOpen, setNewTicketModalOpen] = useState(false); const [showAllTickets, setShowAllTickets] = useState(false); - const [pageNumber, setPageNumber] = useState(1); - const [hasMore, setHasMore] = useState(false); - const [tickets, dispatch] = useReducer(reducer, []); + + const { + tickets: ticketsOpen, + hasMore: hasMoreOpen, + loading: loadingOpen, + dispatch: dispatchOpen, + } = useTickets({ + pageNumber, + searchParam, + status: "open", + }); + + const { + tickets: ticketsPending, + hasMore: hasMorePending, + loading: loadingPending, + dispatch: dispatchPending, + } = useTickets({ + pageNumber, + searchParam, + status: "pending", + }); useEffect(() => { - dispatch({ type: "RESET" }); + dispatchOpen({ type: "RESET" }); + dispatchPending({ type: "RESET" }); setPageNumber(1); - }, [searchParam, tab]); - - useEffect(() => { - setLoading(true); - const delayDebounceFn = setTimeout(() => { - const fetchTickets = async () => { - try { - const { data } = await api.get("/tickets", { - params: { searchParam, pageNumber, status: tab }, - }); - dispatch({ - type: "LOAD_TICKETS", - payload: data.tickets, - }); - setHasMore(data.hasMore); - setLoading(false); - } catch (err) { - console.log(err); - } - }; - fetchTickets(); - }, 500); - return () => clearTimeout(delayDebounceFn); - }, [searchParam, pageNumber, token, tab]); - - 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: data.ticketId }); - } - - if (data.action === "updateStatus" || data.action === "create") { - dispatch({ type: "UPDATE_TICKETS", payload: data.ticket }); - } - - if (data.action === "delete") { - dispatch({ type: "DELETE_TICKET", payload: data.ticketId }); - if (ticketId && data.ticketId === +ticketId) { - toast.warn(i18n.t("tickets.toasts.deleted")); - history.push("/chat"); - } - } - }); - - socket.on("appMessage", data => { - if (data.action === "create") { - dispatch({ type: "UPDATE_TICKETS", payload: data.ticket }); - } - }); - - return () => { - socket.disconnect(); - }; - }, [history, ticketId, userId]); + }, [searchParam, tab, dispatchOpen, dispatchPending]); const loadMore = () => { setPageNumber(prevState => prevState + 1); @@ -297,9 +205,12 @@ const Tickets = () => { history.push(`/chat/${ticket.id}`); }; + // console.log(tickets); + const handleSearchContact = e => { if (e.target.value === "") { setSearchParam(e.target.value.toLowerCase()); + setTab("open"); return; } setSearchParam(e.target.value.toLowerCase()); @@ -307,7 +218,7 @@ const Tickets = () => { }; const handleScroll = e => { - if (!hasMore || loading) return; + if (!hasMoreOpen || loadingOpen) return; const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; if (scrollHeight - (scrollTop + 100) < clientHeight) { @@ -331,16 +242,16 @@ const Tickets = () => { history.push(`/chat/${ticketId}`); }; - const countTickets = (status, userId) => { - const ticketsFound = tickets.filter( - t => - (t.status === status && t.userId === userId) || - (t.status === status && showAllTickets) - ).length; + // const countTickets = (status, userId) => { + // const ticketsFound = tickets.filter( + // t => + // (t.status === status && t.userId === userId) || + // (t.status === status && showAllTickets) + // ).length; - if (ticketsFound === 0) return ""; - return ticketsFound; - }; + // if (ticketsFound === 0) return ""; + // return ticketsFound; + // }; return ( @@ -397,9 +308,7 @@ const Tickets = () => { > {i18n.t("tickets.tabs.open.assignedHeader")} - - {countTickets("open", userId)} - + {ticketsOpen.length} { - - {loading && } + {ticketsOpen.length === 0 ? ( + + + You haven't received any messages yet. + + + ) : ( + ticketsOpen.map(ticket => ( + + )) + )} + {loadingOpen && } { {i18n.t("tickets.tabs.open.pendingHeader")} - {countTickets("pending", null)} + {ticketsPending.length} - - {loading && } + {ticketsPending.length === 0 ? ( + + + You haven't received any messages yet. + + + ) : ( + ticketsPending.map(ticket => ( + + )) + )} + {loadingPending && } @@ -480,7 +397,7 @@ const Tickets = () => { onScroll={handleScroll} > - { status="closed" userId={null} /> - {loading && } + {loading && } */} @@ -502,7 +419,7 @@ const Tickets = () => { onScroll={handleScroll} > - { noTicketsMessage={i18n.t("tickets.tabs.search.noTicketsMessage")} status="all" /> - {loading && } + {loading && } */} diff --git a/frontend/src/components/TicketsList/index.js b/frontend/src/components/TicketsList/index.js index 4051645..26d103a 100644 --- a/frontend/src/components/TicketsList/index.js +++ b/frontend/src/components/TicketsList/index.js @@ -110,7 +110,7 @@ const TicketsList = ({ dense button onClick={e => { - if (ticket.status === "pending") return; + if (ticket.status === "pending" && handleAcepptTicket) return; handleSelectTicket(e, ticket); }} selected={ticketId && +ticketId === ticket.id} @@ -179,7 +179,7 @@ const TicketsList = ({ } /> - {ticket.status === "pending" ? ( + {ticket.status === "pending" && handleAcepptTicket ? ( ({ + tabContainer: { + overflowY: "auto", + maxHeight: 350, + "&::-webkit-scrollbar": { + width: "8px", + height: "8px", + }, + "&::-webkit-scrollbar-thumb": { + boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", + backgroundColor: "#e8e8e8", + }, + }, + popoverPaper: { + width: "100%", + maxWidth: 350, + marginLeft: theme.spacing(2), + marginRight: theme.spacing(1), + [theme.breakpoints.down("sm")]: { + maxWidth: 270, + }, + }, + noShadow: { + boxShadow: "none !important", + }, +})); + +const NotificationsPopOver = () => { + const classes = useStyles(); + + const history = useHistory(); + const userId = +localStorage.getItem("userId"); + const soundAlert = useRef(new Audio(require("../../assets/sound.mp3"))); + const ticketId = +history.location.pathname.split("/")[2]; + const anchorEl = useRef(); + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + if (!("Notification" in window)) { + console.log("This browser doesn't support notifications"); + } else { + Notification.requestPermission(); + } + }, []); + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + socket.emit("joinNotification"); + + socket.on("appMessage", data => { + if (data.action === "create") { + if ( + (ticketId && + data.message.ticketId === +ticketId && + document.visibilityState === "visible") || + (data.ticket.userId !== userId && data.ticket.userId) + ) + return; + showDesktopNotification(data); + } + }); + + return () => { + socket.disconnect(); + }; + }, [history, ticketId, userId]); + + const { tickets } = useTickets({ status: "open" }); + + const showDesktopNotification = ({ message, contact, ticket }) => { + const options = { + body: `${message.body} - ${format(new Date(), "HH:mm")}`, + icon: contact.profilePicUrl, + tag: ticket.id, + }; + let notification = new Notification( + `${i18n.t("tickets.notification.message")} ${contact.name}`, + options + ); + + notification.onclick = function (event) { + event.preventDefault(); // + window.open(`/chat/${ticket.id}`, "_self"); + }; + + document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "visible") { + notification.close(); + } + }); + + soundAlert.current.play(); + }; + + const handleClick = useCallback(() => { + setIsOpen(!isOpen); + }, [isOpen, setIsOpen]); + + const handleClickAway = useCallback(() => { + setIsOpen(false); + }, [setIsOpen]); + + const handleSelectTicket = (e, ticket) => { + history.push(`/chat/${ticket.id}`); + handleClickAway(); + }; + + return ( + <> + + + + + + + + {tickets.length === 0 ? ( + + + You haven't received any messages yet. + + + ) : ( + tickets.map(ticket => ( + + )) + )} + + + > + ); +}; + +export default NotificationsPopOver; diff --git a/frontend/src/components/_layout/index.js b/frontend/src/components/_layout/index.js index 66d2e4a..aa7bc64 100644 --- a/frontend/src/components/_layout/index.js +++ b/frontend/src/components/_layout/index.js @@ -1,32 +1,22 @@ -import React, { useState, useContext, useEffect, useRef } from "react"; +import React, { useState, useContext, useEffect } from "react"; import clsx from "clsx"; import { makeStyles } from "@material-ui/core/styles"; import Drawer from "@material-ui/core/Drawer"; - import AppBar from "@material-ui/core/AppBar"; import Toolbar from "@material-ui/core/Toolbar"; import List from "@material-ui/core/List"; import Typography from "@material-ui/core/Typography"; import Divider from "@material-ui/core/Divider"; -import IconButton from "@material-ui/core/IconButton"; -import Badge from "@material-ui/core/Badge"; - import MenuIcon from "@material-ui/icons/Menu"; -import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; -import NotificationsIcon from "@material-ui/icons/Notifications"; -import MainListItems from "./MainListItems"; -import AccountCircle from "@material-ui/icons/AccountCircle"; - import MenuItem from "@material-ui/core/MenuItem"; import Menu from "@material-ui/core/Menu"; +import IconButton from "@material-ui/core/IconButton"; +import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; +import AccountCircle from "@material-ui/icons/AccountCircle"; -import openSocket from "socket.io-client"; -import { format } from "date-fns"; -// import { toast } from "react-toastify"; -import { useHistory } from "react-router-dom"; -import { i18n } from "../../translate/i18n"; - +import MainListItems from "./MainListItems"; +import NotificationsPopOver from "./NotificationsPopOver"; import { AuthContext } from "../../context/Auth/AuthContext"; const drawerWidth = 240; @@ -115,80 +105,20 @@ const MainDrawer = ({ appTitle, children }) => { const classes = useStyles(); const [open, setOpen] = useState(true); const [anchorEl, setAnchorEl] = React.useState(null); - const menuOpen = Boolean(anchorEl); + const [menuOpen, setMenuOpen] = useState(false); const drawerState = localStorage.getItem("drawerOpen"); - const history = useHistory(); - const ticketId = +history.location.pathname.split("/")[2]; - const soundAlert = useRef(new Audio(require("../../assets/sound.mp3"))); - const userId = +localStorage.getItem("userId"); - // const [notifications, setNotifications] = useState([]); - - useEffect(() => { - if (!("Notification" in window)) { - console.log("This browser doesn't support notifications"); - } else { - Notification.requestPermission(); - } - }, []); - useEffect(() => { if (drawerState === "0") { setOpen(false); } }, [drawerState]); - useEffect(() => { - const socket = openSocket(process.env.REACT_APP_BACKEND_URL); - socket.emit("joinNotification"); - - socket.on("appMessage", data => { - if (data.action === "create") { - if ( - (ticketId && - data.message.ticketId === +ticketId && - document.visibilityState === "visible") || - (data.ticket.userId !== userId && data.ticket.userId) - ) - return; - showDesktopNotification(data); - } - }); - - return () => { - socket.disconnect(); - }; - }, [history, ticketId, userId]); - - const showDesktopNotification = ({ message, contact, ticket }) => { - const options = { - body: `${message.body} - ${format(new Date(), "HH:mm")}`, - icon: contact.profilePicUrl, - tag: ticket.id, - }; - let notification = new Notification( - `${i18n.t("tickets.notification.message")} ${contact.name}`, - options - ); - - notification.onclick = function (event) { - event.preventDefault(); // - window.open(`/chat/${ticket.id}`, "_self"); - }; - - document.addEventListener("visibilitychange", () => { - if (document.visibilityState === "visible") { - notification.close(); - } - }); - - soundAlert.current.play(); - }; - const handleDrawerOpen = () => { setOpen(true); localStorage.setItem("drawerOpen", 1); }; + const handleDrawerClose = () => { setOpen(false); localStorage.setItem("drawerOpen", 0); @@ -196,10 +126,12 @@ const MainDrawer = ({ appTitle, children }) => { const handleMenu = event => { setAnchorEl(event.currentTarget); + setMenuOpen(true); }; - const handleClose = () => { + const handleCloseMenu = () => { setAnchorEl(null); + setMenuOpen(false); }; return ( @@ -225,7 +157,7 @@ const MainDrawer = ({ appTitle, children }) => { { > {appTitle} - - - - - + { horizontal: "right", }} open={menuOpen} - onClose={handleClose} + onClose={handleCloseMenu} > - Profile + Profile Logout diff --git a/frontend/src/hooks/useTickets/index.js b/frontend/src/hooks/useTickets/index.js new file mode 100644 index 0000000..7c45d8a --- /dev/null +++ b/frontend/src/hooks/useTickets/index.js @@ -0,0 +1,140 @@ +import { useState, useEffect, useReducer } from "react"; +import openSocket from "socket.io-client"; +import { useHistory } from "react-router-dom"; + +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 = action.payload; + + const ticketIndex = state.findIndex(t => t.id === ticket.id); + if (ticketIndex !== -1) { + if (ticket.status !== state[ticketIndex]) { + state.splice(ticketIndex, 1); + } else { + state[ticketIndex] = ticket; + state.unshift(state.splice(ticketIndex, 1)[0]); + } + } else { + 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 = action.payload; + + const ticketIndex = state.findIndex(t => t.id === ticketId); + if (ticketIndex !== -1) { + state[ticketIndex].unreadMessages = 0; + } + return [...state]; + } + + if (action.type === "RESET") { + return []; + } +}; + +const useTickets = ({ searchParam, pageNumber, status, date }) => { + const history = useHistory(); + + const [loading, setLoading] = useState(true); + const [hasMore, setHasMore] = useState(false); + const [tickets, dispatch] = useReducer(reducer, []); + + useEffect(() => { + setLoading(true); + const delayDebounceFn = setTimeout(() => { + const fetchTickets = async () => { + try { + const { data } = await api.get("/tickets", { + params: { + searchParam, + pageNumber, + status, + date, + }, + }); + dispatch({ + type: "LOAD_TICKETS", + payload: data.tickets, + }); + setHasMore(data.hasMore); + setLoading(false); + } catch (err) { + console.log(err); + } + }; + fetchTickets(); + }, 500); + return () => clearTimeout(delayDebounceFn); + }, [searchParam, pageNumber, status, date]); + + 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: data.ticketId }); + } + + if (data.action === "updateStatus" || data.action === "create") { + console.log("to aqui", status, data.ticket); + dispatch({ + type: "UPDATE_TICKETS", + payload: data.ticket, + status: status, + }); + } + + if (data.action === "delete") { + dispatch({ type: "DELETE_TICKET", payload: data.ticketId }); + } + }); + + socket.on("appMessage", data => { + if (data.action === "create") { + dispatch({ type: "UPDATE_TICKETS", payload: data.ticket }); + } + }); + + return () => { + socket.disconnect(); + }; + }, [status]); + + return { loading, tickets, hasMore, dispatch }; +}; + +export default useTickets; diff --git a/frontend/src/pages/Dashboard/Chart.js b/frontend/src/pages/Dashboard/Chart.js index adf53ba..3f9d8be 100644 --- a/frontend/src/pages/Dashboard/Chart.js +++ b/frontend/src/pages/Dashboard/Chart.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { useTheme } from "@material-ui/core/styles"; import { BarChart, @@ -14,12 +14,14 @@ import { startOfHour, parseISO, format } from "date-fns"; import { i18n } from "../../translate/i18n"; import Title from "./Title"; -import api from "../../services/api"; +import useTickets from "../../hooks/useTickets"; const Chart = () => { const theme = useTheme(); - const [tickets, setTickets] = useState([]); + const date = useRef(new Date().toISOString()); + const { tickets } = useTickets({ date: date.current }); + const [chartData, setChartData] = useState([ { time: "08:00", amount: 0 }, { time: "09:00", amount: 0 }, @@ -36,38 +38,25 @@ const Chart = () => { ]); useEffect(() => { - const feathTickets = async () => { - try { - const { data } = await api.get("/tickets", { - params: { date: new Date().toISOString() }, - }); - const ticketsData = data.tickets; - setTickets(ticketsData); - setChartData(prevState => { - let aux = [...prevState]; + setChartData(prevState => { + let aux = [...prevState]; - aux.map(a => { - ticketsData.forEach(ticket => { - if ( - format(startOfHour(parseISO(ticket.createdAt)), "HH:mm") === - a.time - ) { - return a.amount++; - } else { - return a; - } - }); + aux.map(a => { + tickets.forEach(ticket => { + if ( + format(startOfHour(parseISO(ticket.createdAt)), "HH:mm") === a.time + ) { + return a.amount++; + } else { return a; - }); - - return aux; + } }); - } catch (err) { - console.log(err); - } - }; - feathTickets(); - }, []); + return a; + }); + + return aux; + }); + }, [tickets]); return ( diff --git a/frontend/src/pages/Login/index.js b/frontend/src/pages/Login/index.js index 5145cbe..23c7df0 100644 --- a/frontend/src/pages/Login/index.js +++ b/frontend/src/pages/Login/index.js @@ -20,8 +20,8 @@ import { AuthContext } from "../../context/Auth/AuthContext"; const Copyright = () => { return ( - {"Copyright © "} - + {"Copyleft "} + Canove {" "} {new Date().getFullYear()} diff --git a/frontend/src/pages/Signup/index.js b/frontend/src/pages/Signup/index.js index 52d4928..97f42de 100644 --- a/frontend/src/pages/Signup/index.js +++ b/frontend/src/pages/Signup/index.js @@ -23,8 +23,8 @@ import api from "../../services/api"; const Copyright = () => { return ( - {"Copyright © "} - + {"Copyleft "} + Canove {" "} {new Date().getFullYear()} diff --git a/frontend/src/pages/WhatsAuth/WhatsAuth.js b/frontend/src/pages/WhatsAuth/WhatsAuth.js index d0fd435..3773159 100644 --- a/frontend/src/pages/WhatsAuth/WhatsAuth.js +++ b/frontend/src/pages/WhatsAuth/WhatsAuth.js @@ -51,8 +51,6 @@ const WhatsAuth = () => { fetchSession(); }, []); - console.log("session", session); - useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL);