Merge pull request #25 from canove/dev

This commit is contained in:
Cassio Santos
2020-09-26 14:36:50 -03:00
committed by GitHub
22 changed files with 365 additions and 245 deletions

View File

@@ -66,7 +66,7 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
}); });
const io = getIO(); const io = getIO();
io.to(ticketId).to("notification").emit("appMessage", { io.to(ticketId).to("notification").to(ticket.status).emit("appMessage", {
action: "create", action: "create",
message, message,
ticket, ticket,

View File

@@ -18,6 +18,7 @@ type IndexQuery = {
interface TicketData { interface TicketData {
contactId: number; contactId: number;
status: string; status: string;
userId: number;
} }
export const index = async (req: Request, res: Response): Promise<Response> => { export const index = async (req: Request, res: Response): Promise<Response> => {
@@ -46,12 +47,12 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
}; };
export const store = async (req: Request, res: Response): Promise<Response> => { export const store = async (req: Request, res: Response): Promise<Response> => {
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(); const io = getIO();
io.to("notification").emit("ticket", { io.to(ticket.status).emit("ticket", {
action: "create", action: "create",
ticket ticket
}); });
@@ -66,12 +67,24 @@ export const update = async (
const { ticketId } = req.params; const { ticketId } = req.params;
const ticketData: TicketData = req.body; const ticketData: TicketData = req.body;
const ticket = await UpdateTicketService({ ticketData, ticketId }); const { ticket, oldStatus, ticketUser } = await UpdateTicketService({
ticketData,
ticketId
});
const io = getIO(); 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).to(ticketId).emit("ticket", {
action: "updateStatus", action: "updateStatus",
ticket ticket,
user: ticketUser
}); });
return res.status(200).json(ticket); return res.status(200).json(ticket);
@@ -83,13 +96,15 @@ export const remove = async (
): Promise<Response> => { ): Promise<Response> => {
const { ticketId } = req.params; const { ticketId } = req.params;
await DeleteTicketService(ticketId); const ticket = await DeleteTicketService(ticketId);
const io = getIO(); const io = getIO();
io.to("notification").emit("ticket", { io.to(ticket.status)
action: "delete", .to(ticketId)
ticketId: +ticketId .emit("ticket", {
}); action: "delete",
ticketId: +ticketId
});
return res.status(200).json({ message: "ticket deleted" }); return res.status(200).json({ message: "ticket deleted" });
}; };

View File

@@ -24,7 +24,7 @@ const SetTicketMessagesAsRead = async (ticket: Ticket): Promise<void> => {
} }
const io = getIO(); const io = getIO();
io.to("notification").emit("ticket", { io.to(ticket.status).to("notification").emit("ticket", {
action: "updateUnread", action: "updateUnread",
ticketId: ticket.id ticketId: ticket.id
}); });

View File

@@ -18,6 +18,11 @@ export const initIO = (httpServer: Server): SocketIO => {
socket.join("notification"); socket.join("notification");
}); });
socket.on("joinTickets", status => {
console.log(`A client joined to ${status} tickets channel.`);
socket.join(status);
});
socket.on("disconnect", () => { socket.on("disconnect", () => {
console.log("Client disconnected"); console.log("Client disconnected");
}); });

View File

@@ -27,7 +27,7 @@ const UpdateContactService = async ({
const contact = await Contact.findOne({ const contact = await Contact.findOne({
where: { id: contactId }, where: { id: contactId },
attributes: ["id", "name", "number", "email"], attributes: ["id", "name", "number", "email", "profilePicUrl"],
include: ["extraInfo"] include: ["extraInfo"]
}); });

View File

@@ -22,7 +22,7 @@ const CreateMessageService = async ({
const ticket = await ShowTicketService(ticketId); const ticket = await ShowTicketService(ticketId);
if (!ticket) { if (!ticket) {
throw new AppError("No ticket found with this ID"); throw new AppError("No ticket found with this ID", 404);
} }
const message: Message = await ticket.$create("message", messageData); const message: Message = await ticket.$create("message", messageData);

View File

@@ -1,4 +1,5 @@
import { where, fn, col } from "sequelize"; import { where, fn, col } from "sequelize";
import AppError from "../../errors/AppError";
import Message from "../../models/Message"; import Message from "../../models/Message";
import Ticket from "../../models/Ticket"; import Ticket from "../../models/Ticket";
import ShowTicketService from "../TicketServices/ShowTicketService"; import ShowTicketService from "../TicketServices/ShowTicketService";
@@ -24,7 +25,7 @@ const ListMessagesService = async ({
const ticket = await ShowTicketService(ticketId); const ticket = await ShowTicketService(ticketId);
if (!ticket) { if (!ticket) {
throw new Error("No ticket found with this ID"); throw new AppError("No ticket found with this ID", 404);
} }
const whereCondition = { const whereCondition = {

View File

@@ -4,12 +4,14 @@ import Ticket from "../../models/Ticket";
interface Request { interface Request {
contactId: number; contactId: number;
status?: string; status: string;
userId: number;
} }
const CreateTicketService = async ({ const CreateTicketService = async ({
contactId, contactId,
status status,
userId
}: Request): Promise<Ticket> => { }: Request): Promise<Ticket> => {
const defaultWhatsapp = await GetDefaultWhatsApp(); const defaultWhatsapp = await GetDefaultWhatsApp();
@@ -19,7 +21,8 @@ const CreateTicketService = async ({
const { id }: Ticket = await defaultWhatsapp.$create("ticket", { const { id }: Ticket = await defaultWhatsapp.$create("ticket", {
contactId, contactId,
status status,
userId
}); });
const ticket = await Ticket.findByPk(id, { include: ["contact"] }); const ticket = await Ticket.findByPk(id, { include: ["contact"] });

View File

@@ -1,7 +1,7 @@
import Ticket from "../../models/Ticket"; import Ticket from "../../models/Ticket";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
const DeleteTicketService = async (id: string): Promise<void> => { const DeleteTicketService = async (id: string): Promise<Ticket> => {
const ticket = await Ticket.findOne({ const ticket = await Ticket.findOne({
where: { id } where: { id }
}); });
@@ -11,6 +11,8 @@ const DeleteTicketService = async (id: string): Promise<void> => {
} }
await ticket.destroy(); await ticket.destroy();
return ticket;
}; };
export default DeleteTicketService; export default DeleteTicketService;

View File

@@ -21,7 +21,7 @@ const ShowTicketService = async (id: string | number): Promise<Ticket> => {
}); });
if (!ticket) { if (!ticket) {
throw new AppError("No ticket found with this ID"); throw new AppError("No ticket found with this ID", 404);
} }
return ticket; return ticket;

View File

@@ -1,6 +1,7 @@
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import Contact from "../../models/Contact"; import Contact from "../../models/Contact";
import Ticket from "../../models/Ticket"; import Ticket from "../../models/Ticket";
import User from "../../models/User";
interface TicketData { interface TicketData {
status?: string; status?: string;
@@ -12,10 +13,16 @@ interface Request {
ticketId: string; ticketId: string;
} }
interface Response {
ticket: Ticket;
ticketUser: User | null;
oldStatus: string;
}
const UpdateTicketService = async ({ const UpdateTicketService = async ({
ticketData, ticketData,
ticketId ticketId
}: Request): Promise<Ticket> => { }: Request): Promise<Response> => {
const { status, userId } = ticketData; const { status, userId } = ticketData;
const ticket = await Ticket.findOne({ const ticket = await Ticket.findOne({
@@ -33,12 +40,15 @@ const UpdateTicketService = async ({
throw new AppError("No ticket found with this ID.", 404); throw new AppError("No ticket found with this ID.", 404);
} }
const oldStatus = ticket.status;
await ticket.update({ await ticket.update({
status, status,
userId userId
}); });
const ticketUser = await ticket.$get("user", { attributes: ["id", "name"] });
return ticket; return { ticket, oldStatus, ticketUser };
}; };
export default UpdateTicketService; export default UpdateTicketService;

View File

@@ -138,12 +138,15 @@ const handleMessage = async (
} }
const io = getIO(); const io = getIO();
io.to(ticket.id.toString()).to("notification").emit("appMessage", { io.to(ticket.id.toString())
action: "create", .to(ticket.status)
message: newMessage, .to("notification")
ticket, .emit("appMessage", {
contact action: "create",
}); message: newMessage,
ticket,
contact
});
}; };
const isValidMsg = (msg: WbotMessage): boolean => { const isValidMsg = (msg: WbotMessage): boolean => {

View File

@@ -0,0 +1,23 @@
import React from "react";
import Backdrop from "@material-ui/core/Backdrop";
import CircularProgress from "@material-ui/core/CircularProgress";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
backdrop: {
zIndex: theme.zIndex.drawer + 1,
color: "#fff",
},
}));
const BackdropLoading = () => {
const classes = useStyles();
return (
<Backdrop className={classes.backdrop} open={true}>
<CircularProgress color="inherit" />
</Backdrop>
);
};
export default BackdropLoading;

View File

@@ -293,11 +293,13 @@ const MessagesList = () => {
const { data } = await api.get("/messages/" + ticketId, { const { data } = await api.get("/messages/" + ticketId, {
params: { pageNumber }, params: { pageNumber },
}); });
setContact(data.ticket.contact); setContact(data.ticket.contact);
setTicket(data.ticket); setTicket(data.ticket);
dispatch({ type: "LOAD_MESSAGES", payload: data.messages }); dispatch({ type: "LOAD_MESSAGES", payload: data.messages });
setHasMore(data.hasMore); setHasMore(data.hasMore);
setLoading(false); setLoading(false);
if (pageNumber === 1 && data.messages.length > 1) { if (pageNumber === 1 && data.messages.length > 1) {
scrollToBottom(); scrollToBottom();
} }
@@ -318,7 +320,7 @@ const MessagesList = () => {
useEffect(() => { useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL); const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.emit("joinChatBox", ticketId, () => {}); socket.emit("joinChatBox", ticketId);
socket.on("appMessage", data => { socket.on("appMessage", data => {
if (data.action === "create") { if (data.action === "create") {
@@ -330,6 +332,17 @@ const MessagesList = () => {
} }
}); });
socket.on("ticket", data => {
if (data.action === "updateStatus") {
setTicket({ ...data.ticket, user: data.user });
}
if (data.action === "delete") {
toast.success("Ticket deleted sucessfully.");
history.push("/tickets");
}
});
socket.on("contact", data => { socket.on("contact", data => {
if (data.action === "update") { if (data.action === "update") {
setContact(data.contact); setContact(data.contact);
@@ -339,7 +352,7 @@ const MessagesList = () => {
return () => { return () => {
socket.disconnect(); socket.disconnect();
}; };
}, [ticketId]); }, [ticketId, history]);
const loadMore = () => { const loadMore = () => {
setPageNumber(prevPageNumber => prevPageNumber + 1); setPageNumber(prevPageNumber => prevPageNumber + 1);
@@ -585,11 +598,12 @@ const MessagesList = () => {
subheader={ subheader={
loading ? ( loading ? (
<Skeleton animation="wave" width={80} /> <Skeleton animation="wave" width={80} />
) : ( ) : ticket.user ? (
ticket.user &&
`${i18n.t("messagesList.header.assignedTo")} ${ `${i18n.t("messagesList.header.assignedTo")} ${
ticket.user.name ticket.user.name
}` }`
) : (
"Pending"
) )
} }
/> />

View File

@@ -1,5 +1,6 @@
import React, { useState, useRef, useCallback, useEffect } from "react"; import React, { useState, useRef, useCallback, useEffect } from "react";
import { useHistory } from "react-router-dom";
import { format } from "date-fns"; import { format } from "date-fns";
import openSocket from "socket.io-client"; import openSocket from "socket.io-client";
@@ -13,11 +14,7 @@ import Badge from "@material-ui/core/Badge";
import ChatIcon from "@material-ui/icons/Chat"; import ChatIcon from "@material-ui/icons/Chat";
import TicketListItem from "../TicketListItem"; import TicketListItem from "../TicketListItem";
// import { toast } from "react-toastify";
import { useHistory } from "react-router-dom";
import { i18n } from "../../translate/i18n"; import { i18n } from "../../translate/i18n";
import useTickets from "../../hooks/useTickets"; import useTickets from "../../hooks/useTickets";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
@@ -46,10 +43,13 @@ const NotificationsPopOver = () => {
const history = useHistory(); const history = useHistory();
const userId = +localStorage.getItem("userId"); const userId = +localStorage.getItem("userId");
const soundAlert = useRef(new Audio(require("../../assets/sound.mp3"))); const soundAlert = useRef(new Audio(require("../../assets/sound.mp3")));
const ticketId = +history.location.pathname.split("/")[2]; const ticketIdUrl = +history.location.pathname.split("/")[2];
const ticketIdRef = useRef(ticketIdUrl);
const anchorEl = useRef(); const anchorEl = useRef();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
// const [notifications, setNotifications] = useState([]); const [notifications, setNotifications] = useState([]);
const { tickets } = useTickets({ withUnreadMessages: "true" });
useEffect(() => { useEffect(() => {
if (!("Notification" in window)) { if (!("Notification" in window)) {
@@ -59,54 +59,87 @@ const NotificationsPopOver = () => {
} }
}, []); }, []);
useEffect(() => {
setNotifications(tickets);
}, [tickets]);
useEffect(() => {
ticketIdRef.current = ticketIdUrl;
}, [ticketIdUrl]);
useEffect(() => { useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL); const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.emit("joinNotification"); socket.emit("joinNotification");
socket.on("ticket", data => {
if (data.action === "updateUnread") {
setNotifications(prevState => {
const ticketIndex = prevState.findIndex(t => t.id === data.ticketId);
if (ticketIndex !== -1) {
prevState.splice(ticketIndex, 1);
return [...prevState];
}
return prevState;
});
}
});
socket.on("appMessage", data => { socket.on("appMessage", data => {
if (data.action === "create") { if (
data.action === "create" &&
(data.ticket.userId === userId || !data.ticket.userId)
) {
setNotifications(prevState => {
const ticketIndex = prevState.findIndex(t => t.id === data.ticket.id);
if (ticketIndex !== -1) {
prevState[ticketIndex] = data.ticket;
return [...prevState];
}
return [data.ticket, ...prevState];
});
if ( if (
(ticketId && (ticketIdRef.current &&
data.message.ticketId === +ticketId && data.message.ticketId === ticketIdRef.current &&
document.visibilityState === "visible") || document.visibilityState === "visible") ||
(data.ticket.userId !== userId && data.ticket.userId) (data.ticket.userId !== userId && data.ticket.userId)
) )
return; return;
showDesktopNotification(data); else {
// show desktop notification
const { message, contact, ticket } = data;
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.focus();
history.push(`/tickets/${ticket.id}`);
};
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
notification.close();
}
});
soundAlert.current.play();
}
} }
}); });
return () => { return () => {
socket.disconnect(); socket.disconnect();
}; };
}, [history, ticketId, userId]); }, [history, userId]);
const { tickets: notifications } = useTickets({ withUnreadMessages: "true" });
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(`/tickets/${ticket.id}`, "_self");
};
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
notification.close();
}
});
soundAlert.current.play();
};
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
setIsOpen(!isOpen); setIsOpen(!isOpen);
@@ -153,8 +186,6 @@ const NotificationsPopOver = () => {
<ListItemText>No tickets with unread messages.</ListItemText> <ListItemText>No tickets with unread messages.</ListItemText>
</ListItem> </ListItem>
) : ( ) : (
notifications &&
notifications.length > 0 &&
notifications.map(ticket => ( notifications.map(ticket => (
<NotificationTicket key={ticket.id}> <NotificationTicket key={ticket.id}>
<TicketListItem ticket={ticket} /> <TicketListItem ticket={ticket} />

View File

@@ -1,5 +1,4 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -12,13 +11,10 @@ import ConfirmationModal from "../ConfirmationModal";
const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => {
const [confirmationOpen, setConfirmationOpen] = useState(false); const [confirmationOpen, setConfirmationOpen] = useState(false);
const history = useHistory();
const handleDeleteTicket = async () => { const handleDeleteTicket = async () => {
try { try {
await api.delete(`/tickets/${ticket.id}`); await api.delete(`/tickets/${ticket.id}`);
toast.success("Ticket deletado com sucesso.");
history.push("/tickets");
} catch (err) { } catch (err) {
toast.error("Erro ao deletar o ticket"); toast.error("Erro ao deletar o ticket");
} }

View File

@@ -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 { makeStyles } from "@material-ui/core/styles";
import List from "@material-ui/core/List"; import List from "@material-ui/core/List";
@@ -69,11 +70,97 @@ 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 === "UPDATE_TICKET_CONTACT") {
const contact = action.payload;
const ticketIndex = state.findIndex(t => t.contactId === contact.id);
if (ticketIndex !== -1) {
state[ticketIndex].contact = contact;
}
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 TicketsList = ({ status, searchParam, showAll }) => {
const userId = +localStorage.getItem("userId");
const classes = useStyles(); const classes = useStyles();
const [pageNumber, setPageNumber] = useState(1); 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, pageNumber,
searchParam, searchParam,
status, status,
@@ -81,9 +168,61 @@ const TicketsList = ({ status, searchParam, showAll }) => {
}); });
useEffect(() => { useEffect(() => {
dispatch({ type: "RESET" }); dispatch({
setPageNumber(1); type: "LOAD_TICKETS",
}, [status, searchParam, dispatch, showAll]); 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,
});
}
});
socket.on("contact", data => {
if (data.action === "update") {
dispatch({
type: "UPDATE_TICKET_CONTACT",
payload: data.contact,
});
}
});
return () => {
socket.disconnect();
};
}, [status, showAll, userId]);
const loadMore = () => { const loadMore = () => {
setPageNumber(prevState => prevState + 1); setPageNumber(prevState => prevState + 1);
@@ -91,7 +230,9 @@ const TicketsList = ({ status, searchParam, showAll }) => {
const handleScroll = e => { const handleScroll = e => {
if (!hasMore || loading) return; if (!hasMore || loading) return;
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
if (scrollHeight - (scrollTop + 100) < clientHeight) { if (scrollHeight - (scrollTop + 100) < clientHeight) {
loadMore(); loadMore();
} }
@@ -110,16 +251,16 @@ const TicketsList = ({ status, searchParam, showAll }) => {
{status === "open" && ( {status === "open" && (
<div className={classes.ticketsListHeader}> <div className={classes.ticketsListHeader}>
{i18n.t("ticketsList.assignedHeader")} {i18n.t("ticketsList.assignedHeader")}
<span className={classes.ticketsCount}>{tickets.length}</span> <span className={classes.ticketsCount}>{ticketsList.length}</span>
</div> </div>
)} )}
{status === "pending" && ( {status === "pending" && (
<div className={classes.ticketsListHeader}> <div className={classes.ticketsListHeader}>
{i18n.t("ticketsList.pendingHeader")} {i18n.t("ticketsList.pendingHeader")}
<span className={classes.ticketsCount}>{tickets.length}</span> <span className={classes.ticketsCount}>{ticketsList.length}</span>
</div> </div>
)} )}
{tickets.length === 0 && !loading ? ( {ticketsList.length === 0 && !loading ? (
<div className={classes.noTicketsDiv}> <div className={classes.noTicketsDiv}>
<span className={classes.noTicketsTitle}> <span className={classes.noTicketsTitle}>
{i18n.t("ticketsList.noTicketsTitle")} {i18n.t("ticketsList.noTicketsTitle")}
@@ -130,7 +271,7 @@ const TicketsList = ({ status, searchParam, showAll }) => {
</div> </div>
) : ( ) : (
<> <>
{tickets.map(ticket => ( {ticketsList.map(ticket => (
<TicketListItem ticket={ticket} key={ticket.id} /> <TicketListItem ticket={ticket} key={ticket.id} />
))} ))}
</> </>

View File

@@ -17,7 +17,9 @@ import AccountCircle from "@material-ui/icons/AccountCircle";
import MainListItems from "./MainListItems"; import MainListItems from "./MainListItems";
import NotificationsPopOver from "../NotificationsPopOver"; import NotificationsPopOver from "../NotificationsPopOver";
import UserModal from "../UserModal";
import { AuthContext } from "../../context/Auth/AuthContext"; import { AuthContext } from "../../context/Auth/AuthContext";
import BackdropLoading from "../BackdropLoading";
const drawerWidth = 240; const drawerWidth = 240;
@@ -100,13 +102,15 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
const MainDrawer = ({ appTitle, children }) => { const LoggedInLayout = ({ appTitle, children }) => {
const { handleLogout } = useContext(AuthContext); const drawerState = localStorage.getItem("drawerOpen");
const userId = +localStorage.getItem("userId");
const classes = useStyles(); const classes = useStyles();
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
const [anchorEl, setAnchorEl] = React.useState(null); const [userModalOpen, setUserModalOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const drawerState = localStorage.getItem("drawerOpen"); const { handleLogout, loading } = useContext(AuthContext);
useEffect(() => { useEffect(() => {
if (drawerState === "0") { if (drawerState === "0") {
@@ -134,6 +138,15 @@ const MainDrawer = ({ appTitle, children }) => {
setMenuOpen(false); setMenuOpen(false);
}; };
const handleOpenUserModal = () => {
setUserModalOpen(true);
handleCloseMenu();
};
if (loading) {
return <BackdropLoading />;
}
return ( return (
<div className={classes.root}> <div className={classes.root}>
<Drawer <Drawer
@@ -154,6 +167,11 @@ const MainDrawer = ({ appTitle, children }) => {
</List> </List>
<Divider /> <Divider />
</Drawer> </Drawer>
<UserModal
open={userModalOpen}
onClose={() => setUserModalOpen(false)}
userId={userId}
/>
<AppBar <AppBar
position="absolute" position="absolute"
className={clsx(classes.appBar, open && classes.appBarShift)} className={clsx(classes.appBar, open && classes.appBarShift)}
@@ -179,7 +197,7 @@ const MainDrawer = ({ appTitle, children }) => {
noWrap noWrap
className={classes.title} className={classes.title}
> >
{appTitle} WHATICKET
</Typography> </Typography>
<NotificationsPopOver /> <NotificationsPopOver />
@@ -208,7 +226,7 @@ const MainDrawer = ({ appTitle, children }) => {
open={menuOpen} open={menuOpen}
onClose={handleCloseMenu} onClose={handleCloseMenu}
> >
<MenuItem onClick={handleCloseMenu}>Profile</MenuItem> <MenuItem onClick={handleOpenUserModal}>Profile</MenuItem>
<MenuItem onClick={handleLogout}>Logout</MenuItem> <MenuItem onClick={handleLogout}>Logout</MenuItem>
</Menu> </Menu>
</div> </div>
@@ -223,4 +241,4 @@ const MainDrawer = ({ appTitle, children }) => {
); );
}; };
export default MainDrawer; export default LoggedInLayout;

View File

@@ -1,80 +1,8 @@
import { useState, useEffect, useReducer } from "react"; import { useState, useEffect } from "react";
import openSocket from "socket.io-client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import api from "../../services/api"; 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 = ({ const useTickets = ({
searchParam, searchParam,
pageNumber, pageNumber,
@@ -83,10 +11,9 @@ const useTickets = ({
showAll, showAll,
withUnreadMessages, withUnreadMessages,
}) => { }) => {
const userId = +localStorage.getItem("userId");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [hasMore, setHasMore] = useState(false); const [hasMore, setHasMore] = useState(false);
const [tickets, dispatch] = useReducer(reducer, []); const [tickets, setTickets] = useState([]);
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
@@ -103,10 +30,7 @@ const useTickets = ({
withUnreadMessages, withUnreadMessages,
}, },
}); });
dispatch({ setTickets(data.tickets);
type: "LOAD_TICKETS",
payload: data.tickets,
});
setHasMore(data.hasMore); setHasMore(data.hasMore);
setLoading(false); setLoading(false);
} catch (err) { } catch (err) {
@@ -121,57 +45,7 @@ const useTickets = ({
return () => clearTimeout(delayDebounceFn); return () => clearTimeout(delayDebounceFn);
}, [searchParam, pageNumber, status, date, showAll, withUnreadMessages]); }, [searchParam, pageNumber, status, date, showAll, withUnreadMessages]);
useEffect(() => { return { tickets, loading, hasMore };
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 };
}; };
export default useTickets; export default useTickets;

View File

@@ -167,8 +167,8 @@ const Connections = () => {
}; };
const handleCloseQrModal = useCallback(() => { const handleCloseQrModal = useCallback(() => {
setQrModalOpen(false);
setSelectedWhatsApp(null); setSelectedWhatsApp(null);
setQrModalOpen(false);
}, [setQrModalOpen, setSelectedWhatsApp]); }, [setQrModalOpen, setSelectedWhatsApp]);
const handleEditWhatsApp = whatsApp => { const handleEditWhatsApp = whatsApp => {

View File

@@ -1,29 +1,13 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import { Route, Redirect } from "react-router-dom"; import { Route, Redirect } from "react-router-dom";
import Backdrop from "@material-ui/core/Backdrop";
import CircularProgress from "@material-ui/core/CircularProgress";
import { makeStyles } from "@material-ui/core/styles";
import { AuthContext } from "../context/Auth/AuthContext"; import { AuthContext } from "../context/Auth/AuthContext";
import BackdropLoading from "../components/BackdropLoading";
const useStyles = makeStyles(theme => ({
backdrop: {
zIndex: theme.zIndex.drawer + 1,
color: "#fff",
},
}));
const RouteWrapper = ({ component: Component, isPrivate = false, ...rest }) => { const RouteWrapper = ({ component: Component, isPrivate = false, ...rest }) => {
const classes = useStyles();
const { isAuth, loading } = useContext(AuthContext); const { isAuth, loading } = useContext(AuthContext);
if (loading) if (loading) return <BackdropLoading />;
return (
<Backdrop className={classes.backdrop} open={loading}>
<CircularProgress color="inherit" />
</Backdrop>
);
if (!isAuth && isPrivate) { if (!isAuth && isPrivate) {
return ( return (

View File

@@ -2,7 +2,7 @@ import React from "react";
import { BrowserRouter, Switch } from "react-router-dom"; import { BrowserRouter, Switch } from "react-router-dom";
import { ToastContainer } from "react-toastify"; import { ToastContainer } from "react-toastify";
import MainDrawer from "../components/_layout"; import LoggedInLayout from "../components/_layout";
import Dashboard from "../pages/Dashboard/"; import Dashboard from "../pages/Dashboard/";
import Tickets from "../pages/Tickets/"; import Tickets from "../pages/Tickets/";
import Signup from "../pages/Signup/"; import Signup from "../pages/Signup/";
@@ -21,7 +21,7 @@ const Routes = () => {
<Switch> <Switch>
<Route exact path="/login" component={Login} /> <Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} /> <Route exact path="/signup" component={Signup} />
<MainDrawer> <LoggedInLayout>
<Route exact path="/" component={Dashboard} isPrivate /> <Route exact path="/" component={Dashboard} isPrivate />
<Route <Route
exact exact
@@ -38,7 +38,7 @@ const Routes = () => {
<Route exact path="/contacts" component={Contacts} isPrivate /> <Route exact path="/contacts" component={Contacts} isPrivate />
<Route exact path="/users" component={Users} isPrivate /> <Route exact path="/users" component={Users} isPrivate />
<Route exact path="/Settings" component={Settings} isPrivate /> <Route exact path="/Settings" component={Settings} isPrivate />
</MainDrawer> </LoggedInLayout>
</Switch> </Switch>
<ToastContainer autoClose={3000} /> <ToastContainer autoClose={3000} />
</AuthProvider> </AuthProvider>