From 6e8eba118751ce8c38d090c21087a4e63e335462 Mon Sep 17 00:00:00 2001 From: canove Date: Tue, 25 Aug 2020 12:24:19 -0300 Subject: [PATCH] feat: show pending messages in appbar --- backend/src/controllers/TicketController.js | 17 +- backend/src/middleware/is-auth.js | 2 +- .../index.js} | 33 +- .../src/components/TicketListItem/index.js | 4 +- .../src/components/TicketOptionsMenu/index.js | 3 + frontend/src/components/Tickets/index.js | 324 +++--------------- frontend/src/components/TicketsList/index.js | 292 +++++++--------- frontend/src/components/_layout/index.js | 4 +- frontend/src/hooks/useTickets/index.js | 41 ++- frontend/src/pages/Contacts/index.js | 2 +- frontend/src/translate/languages/en.js | 2 +- 11 files changed, 255 insertions(+), 469 deletions(-) rename frontend/src/components/{_layout/NotificationsPopOver.js => NotificationsPopOver/index.js} (83%) diff --git a/backend/src/controllers/TicketController.js b/backend/src/controllers/TicketController.js index e0ff97b..5ae04ab 100644 --- a/backend/src/controllers/TicketController.js +++ b/backend/src/controllers/TicketController.js @@ -13,12 +13,14 @@ exports.index = async (req, res) => { status = "", date = "", searchParam = "", + showAll, } = req.query; + const userId = req.userId; + const limit = 20; const offset = limit * (pageNumber - 1); - let whereCondition = {}; let includeCondition = [ { model: Contact, @@ -27,16 +29,20 @@ exports.index = async (req, res) => { }, ]; + let whereCondition = { userId: userId }; + + if (showAll === "true") { + whereCondition = {}; + } + if (status) { whereCondition = { ...whereCondition, status: status, }; } - // else if (status === "closed") { - // whereCondition = { ...whereCondition, status: "closed" }; - // } - else if (searchParam) { + + if (searchParam) { includeCondition = [ ...includeCondition, { @@ -56,7 +62,6 @@ exports.index = async (req, res) => { ]; whereCondition = { - ...whereCondition, [Sequelize.Op.or]: [ { "$contact.name$": Sequelize.where( diff --git a/backend/src/middleware/is-auth.js b/backend/src/middleware/is-auth.js index 633de47..6bfaabc 100644 --- a/backend/src/middleware/is-auth.js +++ b/backend/src/middleware/is-auth.js @@ -15,7 +15,7 @@ module.exports = async (req, res, next) => { if (error) { return res.status(401).json({ error: "Invalid token" }); } - req.userId = token.userId; + req.userId = result.userId; // todo >> find user in DB and store in req.user to use latter, or throw an error if user not exists anymore next(); }); diff --git a/frontend/src/components/_layout/NotificationsPopOver.js b/frontend/src/components/NotificationsPopOver/index.js similarity index 83% rename from frontend/src/components/_layout/NotificationsPopOver.js rename to frontend/src/components/NotificationsPopOver/index.js index 218e133..5fadd84 100644 --- a/frontend/src/components/_layout/NotificationsPopOver.js +++ b/frontend/src/components/NotificationsPopOver/index.js @@ -56,6 +56,7 @@ const NotificationsPopOver = () => { const ticketId = +history.location.pathname.split("/")[2]; const anchorEl = useRef(); const [isOpen, setIsOpen] = useState(false); + const [notifications, setNotifications] = useState([]); useEffect(() => { if (!("Notification" in window)) { @@ -87,7 +88,27 @@ const NotificationsPopOver = () => { }; }, [history, ticketId, userId]); - const { tickets } = useTickets({ status: "open" }); + const { tickets: openTickets } = useTickets({ status: "open" }); + const { tickets: pendingTickets } = useTickets({ status: "pending" }); + + useEffect(() => { + if (openTickets.length > 0 || pendingTickets.length > 0) { + let aux = []; + + openTickets.forEach(ticket => { + if (ticket.unreadMessages > 0) { + aux.push(ticket); + } + }); + pendingTickets.forEach(ticket => { + if (ticket.unreadMessages > 0) { + aux.push(ticket); + } + }); + + setNotifications(aux); + } + }, [openTickets, pendingTickets]); const showDesktopNotification = ({ message, contact, ticket }) => { const options = { @@ -135,7 +156,7 @@ const NotificationsPopOver = () => { aria-label="Open Notifications" color="inherit" > - + @@ -155,14 +176,12 @@ const NotificationsPopOver = () => { onClose={handleClickAway} > - {tickets.length === 0 ? ( + {notifications.length === 0 ? ( - - You haven't received any messages yet. - + No tickets with unread messages. ) : ( - tickets.map(ticket => ( + notifications.map(ticket => ( { const classes = useStyles(); @@ -99,7 +99,7 @@ const TicketListItem = ({ if (ticket.status === "pending" && handleAcepptTicket) return; handleSelectTicket(e, ticket); }} - selected={ticketId && +ticketId === ticket.id} + selected={selectedTicketId && +selectedTicketId === ticket.id} className={classes.ticket} > diff --git a/frontend/src/components/TicketOptionsMenu/index.js b/frontend/src/components/TicketOptionsMenu/index.js index e6c63bf..e558aac 100644 --- a/frontend/src/components/TicketOptionsMenu/index.js +++ b/frontend/src/components/TicketOptionsMenu/index.js @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import { useHistory } from "react-router-dom"; import { toast } from "react-toastify"; @@ -11,11 +12,13 @@ import ConfirmationModal from "../ConfirmationModal"; const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { const [confirmationOpen, setConfirmationOpen] = useState(false); + const history = useHistory(); const handleDeleteTicket = async () => { try { await api.delete(`/tickets/${ticket.id}`); toast.success("Ticket deletado com sucesso."); + history.push("/chat"); } catch (err) { toast.error("Erro ao deletar o ticket"); } diff --git a/frontend/src/components/Tickets/index.js b/frontend/src/components/Tickets/index.js index f347d91..6540c2a 100644 --- a/frontend/src/components/Tickets/index.js +++ b/frontend/src/components/Tickets/index.js @@ -1,9 +1,8 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { useHistory, useParams } from "react-router-dom"; import { makeStyles } from "@material-ui/core/styles"; import Paper from "@material-ui/core/Paper"; -import List from "@material-ui/core/List"; import SearchIcon from "@material-ui/icons/Search"; import InputBase from "@material-ui/core/InputBase"; import Tabs from "@material-ui/core/Tabs"; @@ -14,19 +13,13 @@ 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 TicketListItem from "../TicketListItem"; - +import TicketsList from "../TicketsList"; import TabPanel from "../TabPanel"; import { i18n } from "../../translate/i18n"; import api from "../../services/api"; -import useTickets from "../../hooks/useTickets"; const useStyles = makeStyles(theme => ({ ticketsWrapper: { @@ -40,7 +33,6 @@ const useStyles = makeStyles(theme => ({ }, tabsHeader: { - // display: "flex", flex: "none", backgroundColor: "#eee", }, @@ -56,87 +48,21 @@ const useStyles = makeStyles(theme => ({ width: 120, }, - halfTicketsList: { - height: "50%", - overflowY: "scroll", - "&::-webkit-scrollbar": { - width: "8px", - height: "8px", - }, - "&::-webkit-scrollbar-thumb": { - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", - backgroundColor: "#e8e8e8", - }, - borderTop: "1px solid rgba(0, 0, 0, 0.12)", - }, - - fullHeightTicketsList: { - flex: 1, - overflowY: "scroll", - "&::-webkit-scrollbar": { - width: "8px", - height: "8px", - }, - "&::-webkit-scrollbar-thumb": { - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", - backgroundColor: "#e8e8e8", - }, - borderTop: "2px solid rgba(0, 0, 0, 0.12)", - }, - - ticketsListHeader: { - display: "flex", - alignItems: "center", - fontWeight: 500, - fontSize: "16px", - height: "56px", - color: "rgb(67, 83, 105)", - padding: "0px 12px", - borderBottom: "1px solid rgba(0, 0, 0, 0.12)", - }, - - ticketsCount: { - fontWeight: "normal", - color: "rgb(104, 121, 146)", - marginLeft: "8px", - fontSize: "14px", - }, - ticketsListActions: { flex: "none", marginLeft: "auto", }, - 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", - }, - - contactsSearchBox: { + searchBox: { position: "relative", + display: "flex", + alignItems: "center", background: "#fafafa", padding: "10px 13px", }, serachInputWrapper: { + flex: 1, background: "#fff", display: "flex", borderRadius: 40, @@ -150,7 +76,7 @@ const useStyles = makeStyles(theme => ({ alignSelf: "center", }, - contactsSearchInput: { + searchInput: { flex: 1, border: "none", borderRadius: 30, @@ -167,46 +93,11 @@ const Tickets = () => { const [tab, setTab] = useState("open"); const [newTicketModalOpen, setNewTicketModalOpen] = useState(false); const [showAllTickets, setShowAllTickets] = useState(false); - const [pageNumber, setPageNumber] = useState(1); - - 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(() => { - dispatchOpen({ type: "RESET" }); - dispatchPending({ type: "RESET" }); - setPageNumber(1); - }, [searchParam, tab, dispatchOpen, dispatchPending]); - - const loadMore = () => { - setPageNumber(prevState => prevState + 1); - }; const handleSelectTicket = (e, ticket) => { history.push(`/chat/${ticket.id}`); }; - // console.log(tickets); - const handleSearchContact = e => { if (e.target.value === "") { setSearchParam(e.target.value.toLowerCase()); @@ -217,15 +108,6 @@ const Tickets = () => { setTab("search"); }; - const handleScroll = e => { - if (!hasMoreOpen || loadingOpen) return; - const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; - - if (scrollHeight - (scrollTop + 100) < clientHeight) { - loadMore(); - } - }; - const handleChangeTab = (e, newValue) => { setTab(newValue); }; @@ -242,17 +124,6 @@ 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; - - // if (ticketsFound === 0) return ""; - // return ticketsFound; - // }; - return ( { /> - +
+
+ setShowAllTickets(prevState => !prevState)} + name="showAllTickets" + color="primary" + /> + } + /> + setNewTicketModalOpen(true)} + style={{ marginLeft: 20 }} + > + + +
- -
- {i18n.t("tickets.tabs.open.assignedHeader")} - {ticketsOpen.length} -
- setShowAllTickets(prevState => !prevState)} - name="showAllTickets" - color="primary" - /> - } - /> - setNewTicketModalOpen(true)} - style={{ marginLeft: 20 }} - > - - -
-
- - {ticketsOpen.length === 0 ? ( - - - You haven't received any messages yet. - - - ) : ( - ticketsOpen.map(ticket => ( - - )) - )} - {loadingOpen && } - -
- -
- {i18n.t("tickets.tabs.open.pendingHeader")} - - {ticketsPending.length} - -
- - {ticketsPending.length === 0 ? ( - - - You haven't received any messages yet. - - - ) : ( - ticketsPending.map(ticket => ( - - )) - )} - {loadingPending && } - -
+ {/* */} +
- - - {/* - {loading && } */} - - + - - - {/* - {loading && } */} - - +
); diff --git a/frontend/src/components/TicketsList/index.js b/frontend/src/components/TicketsList/index.js index 26d103a..b551930 100644 --- a/frontend/src/components/TicketsList/index.js +++ b/frontend/src/components/TicketsList/index.js @@ -1,38 +1,55 @@ -import React from "react"; -import { parseISO, format, isSameDay } from "date-fns"; +import React, { useState, useEffect } from "react"; 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 List from "@material-ui/core/List"; +import Paper from "@material-ui/core/Paper"; +import TicketListItem from "../TicketListItem"; +import TicketsSkeleton from "../TicketsSkeleton"; +import useTickets from "../../hooks/useTickets"; import { i18n } from "../../translate/i18n"; const useStyles = makeStyles(theme => ({ - ticket: { + ticketsListWrapper: { position: "relative", - "& .hidden-button": { - display: "none", - }, - "&:hover .hidden-button": { - display: "flex", - position: "absolute", - left: "50%", - }, - }, - noTicketsDiv: { display: "flex", - height: "100px", - margin: 40, + height: "100%", flexDirection: "column", + overflow: "hidden", + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + }, + + ticketsList: { + flex: 1, + overflowY: "scroll", + "&::-webkit-scrollbar": { + width: "8px", + height: "8px", + }, + "&::-webkit-scrollbar-thumb": { + boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", + backgroundColor: "#e8e8e8", + }, + borderTop: "2px solid rgba(0, 0, 0, 0.12)", + }, + + ticketsListHeader: { + display: "flex", alignItems: "center", - justifyContent: "center", + fontWeight: 500, + fontSize: "16px", + height: "56px", + color: "rgb(67, 83, 105)", + padding: "0px 12px", + borderBottom: "1px solid rgba(0, 0, 0, 0.12)", + }, + + ticketsCount: { + fontWeight: "normal", + color: "rgb(104, 121, 146)", + marginLeft: "8px", + fontSize: "14px", }, noTicketsText: { @@ -49,163 +66,100 @@ const useStyles = makeStyles(theme => ({ margin: "0px", }, - contactNameWrapper: { + noTicketsDiv: { 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], + height: "100px", + margin: 40, + flexDirection: "column", + alignItems: "center", + justifyContent: "center", }, })); const TicketsList = ({ - tickets, status, - userId, + searchParam, handleSelectTicket, - showAllTickets, - ticketId, - noTicketsTitle, - loading, - noTicketsMessage, handleAcepptTicket, + selectedTicketId, + showAll, }) => { const classes = useStyles(); + const [pageNumber, setPageNumber] = useState(1); - let viewTickets = []; - - tickets.forEach(ticket => { - if ( - (ticket.status === status && ticket.userId === userId) || - (ticket.status === status && showAllTickets) || - (ticket.status === "closed" && status === "closed") || - status === "all" - ) - viewTickets.push( - - { - 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 ? ( - - ) : null} -
- -
- ); + const { tickets, hasMore, loading, dispatch } = useTickets({ + pageNumber, + searchParam, + status, + showAll, }); - if (viewTickets.length > 0) { - return viewTickets; - } else if (!loading) { - return ( -
- {noTicketsTitle} -

{noTicketsMessage}

-
- ); - } else return null; + useEffect(() => { + dispatch({ type: "RESET" }); + setPageNumber(1); + }, [status, searchParam, dispatch, showAll]); + + const loadMore = () => { + setPageNumber(prevState => prevState + 1); + }; + + const handleScroll = e => { + if (!hasMore || loading) return; + + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + + if (scrollHeight - (scrollTop + 100) < clientHeight) { + loadMore(); + } + }; + + return ( +
+ + + {status === "open" && ( +
+ {i18n.t("tickets.tabs.open.assignedHeader")} + {tickets.length} +
+ )} + {status === "pending" && ( +
+ {i18n.t("tickets.tabs.open.pendingHeader")} + {tickets.length} +
+ )} + {tickets.length === 0 && !loading ? ( +
+ Nothing here! +

+ No tickets found with this status or search term. +

+
+ ) : ( + <> + {tickets.map(ticket => ( + + ))} + + )} + {loading && } +
+
+
+ ); }; export default TicketsList; diff --git a/frontend/src/components/_layout/index.js b/frontend/src/components/_layout/index.js index aa7bc64..65ac8e9 100644 --- a/frontend/src/components/_layout/index.js +++ b/frontend/src/components/_layout/index.js @@ -16,7 +16,7 @@ import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; import AccountCircle from "@material-ui/icons/AccountCircle"; import MainListItems from "./MainListItems"; -import NotificationsPopOver from "./NotificationsPopOver"; +import NotificationsPopOver from "../NotificationsPopOver"; import { AuthContext } from "../../context/Auth/AuthContext"; const drawerWidth = 240; @@ -181,7 +181,7 @@ const MainDrawer = ({ appTitle, children }) => { > {appTitle} - + {/* */}
{ } if (action.type === "UPDATE_TICKETS") { - const ticket = action.payload; + const { ticket, status, loggedUser } = action.payload; const ticketIndex = state.findIndex(t => t.id === ticket.id); if (ticketIndex !== -1) { - if (ticket.status !== state[ticketIndex]) { + if (ticket.status !== state[ticketIndex].status) { state.splice(ticketIndex, 1); } else { state[ticketIndex] = ticket; state.unshift(state.splice(ticketIndex, 1)[0]); } - } else { + } else if ( + ticket.status === status && + (ticket.userId === loggedUser || + !ticket.userId || + ticket.status === "closed") + ) { state.unshift(ticket); } return [...state]; @@ -65,13 +69,14 @@ const reducer = (state, action) => { } }; -const useTickets = ({ searchParam, pageNumber, status, date }) => { - const history = useHistory(); - +const useTickets = ({ searchParam, pageNumber, status, date, showAll }) => { + const userId = +localStorage.getItem("userId"); const [loading, setLoading] = useState(true); const [hasMore, setHasMore] = useState(false); const [tickets, dispatch] = useReducer(reducer, []); + console.log("rendering"); + useEffect(() => { setLoading(true); const delayDebounceFn = setTimeout(() => { @@ -83,6 +88,7 @@ const useTickets = ({ searchParam, pageNumber, status, date }) => { pageNumber, status, date, + showAll, }, }); dispatch({ @@ -98,7 +104,7 @@ const useTickets = ({ searchParam, pageNumber, status, date }) => { fetchTickets(); }, 500); return () => clearTimeout(delayDebounceFn); - }, [searchParam, pageNumber, status, date]); + }, [searchParam, pageNumber, status, date, showAll]); useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL); @@ -110,11 +116,13 @@ const useTickets = ({ searchParam, pageNumber, status, date }) => { } if (data.action === "updateStatus" || data.action === "create") { - console.log("to aqui", status, data.ticket); dispatch({ type: "UPDATE_TICKETS", - payload: data.ticket, - status: status, + payload: { + ticket: data.ticket, + status: status, + loggedUser: userId, + }, }); } @@ -125,14 +133,21 @@ const useTickets = ({ searchParam, pageNumber, status, date }) => { socket.on("appMessage", data => { if (data.action === "create") { - dispatch({ type: "UPDATE_TICKETS", payload: data.ticket }); + dispatch({ + type: "UPDATE_TICKETS", + payload: { + ticket: data.ticket, + status: status, + loggedUser: userId, + }, + }); } }); return () => { socket.disconnect(); }; - }, [status]); + }, [status, userId]); return { loading, tickets, hasMore, dispatch }; }; diff --git a/frontend/src/pages/Contacts/index.js b/frontend/src/pages/Contacts/index.js index b90e4f7..078abaa 100644 --- a/frontend/src/pages/Contacts/index.js +++ b/frontend/src/pages/Contacts/index.js @@ -100,7 +100,7 @@ const Contacts = () => { } }; fetchContacts(); - }, 1000); + }, 500); return () => clearTimeout(delayDebounceFn); }, [searchParam, page, rowsPerPage]); diff --git a/frontend/src/translate/languages/en.js b/frontend/src/translate/languages/en.js index 37dc017..7018fe4 100644 --- a/frontend/src/translate/languages/en.js +++ b/frontend/src/translate/languages/en.js @@ -37,7 +37,7 @@ const messages = { dashboard: { charts: { perDay: { - title: "Tickets today:", + title: "Tickets today: ", }, }, },