diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index b1980c2..d90020d 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -76,14 +76,14 @@ export const update = async ( const { ticketId } = req.params; const ticketData: TicketData = req.body; - const { ticket, oldStatus, ticketUser } = await UpdateTicketService({ + const { ticket, oldStatus, oldUserId } = await UpdateTicketService({ ticketData, ticketId }); const io = getIO(); - if (ticket.status !== oldStatus) { + if (ticket.status !== oldStatus || ticket.user?.id !== oldUserId) { io.to(oldStatus).emit("ticket", { action: "delete", ticketId: ticket.id @@ -92,8 +92,7 @@ export const update = async ( io.to(ticket.status).to("notification").to(ticketId).emit("ticket", { action: "updateStatus", - ticket, - user: ticketUser + ticket }); return res.status(200).json(ticket); diff --git a/backend/src/controllers/UserController.ts b/backend/src/controllers/UserController.ts index e27669c..e1c8a19 100644 --- a/backend/src/controllers/UserController.ts +++ b/backend/src/controllers/UserController.ts @@ -16,9 +16,6 @@ type IndexQuery = { }; export const index = async (req: Request, res: Response): Promise => { - if (req.user.profile !== "admin") { - throw new AppError("ERR_NO_PERMISSION", 403); // should be handled better. - } const { searchParam, pageNumber } = req.query as IndexQuery; const { users, count, hasMore } = await ListUsersService({ diff --git a/backend/src/helpers/UpdateDeletedUserOpenTicketsStatus.ts b/backend/src/helpers/UpdateDeletedUserOpenTicketsStatus.ts index 60f8687..110ba03 100644 --- a/backend/src/helpers/UpdateDeletedUserOpenTicketsStatus.ts +++ b/backend/src/helpers/UpdateDeletedUserOpenTicketsStatus.ts @@ -8,7 +8,7 @@ const UpdateDeletedUserOpenTicketsStatus = async ( tickets.forEach(async t => { const ticketId = t.id.toString(); - const { ticket, oldStatus, ticketUser } = await UpdateTicketService({ + const { ticket, oldStatus } = await UpdateTicketService({ ticketData: { status: "pending" }, ticketId }); @@ -23,8 +23,7 @@ const UpdateDeletedUserOpenTicketsStatus = async ( io.to(ticket.status).to(ticketId).emit("ticket", { action: "updateStatus", - ticket, - user: ticketUser + ticket }); }); }; diff --git a/backend/src/services/TicketServices/UpdateTicketService.ts b/backend/src/services/TicketServices/UpdateTicketService.ts index df42ac5..ed61b64 100644 --- a/backend/src/services/TicketServices/UpdateTicketService.ts +++ b/backend/src/services/TicketServices/UpdateTicketService.ts @@ -17,8 +17,8 @@ interface Request { interface Response { ticket: Ticket; - ticketUser: User | null; oldStatus: string; + oldUserId: number | undefined; } const UpdateTicketService = async ({ @@ -34,6 +34,11 @@ const UpdateTicketService = async ({ model: Contact, as: "contact", attributes: ["id", "name", "number", "profilePicUrl"] + }, + { + model: User, + as: "user", + attributes: ["id", "name"] } ] }); @@ -45,6 +50,7 @@ const UpdateTicketService = async ({ await SetTicketMessagesAsRead(ticket); const oldStatus = ticket.status; + const oldUserId = ticket.user?.id; if (oldStatus === "closed") { await CheckContactOpenTickets(ticket.contact.id); @@ -54,9 +60,10 @@ const UpdateTicketService = async ({ status, userId }); - const ticketUser = await ticket.$get("user", { attributes: ["id", "name"] }); - return { ticket, oldStatus, ticketUser }; + await ticket.reload(); + + return { ticket, oldStatus, oldUserId }; }; export default UpdateTicketService; diff --git a/frontend/src/components/NewTicketModal/index.js b/frontend/src/components/NewTicketModal/index.js index 4b77639..2b6fb9d 100644 --- a/frontend/src/components/NewTicketModal/index.js +++ b/frontend/src/components/NewTicketModal/index.js @@ -14,27 +14,17 @@ import Autocomplete, { } from "@material-ui/lab/Autocomplete"; import CircularProgress from "@material-ui/core/CircularProgress"; -import { makeStyles } from "@material-ui/core/styles"; - import { i18n } from "../../translate/i18n"; import api from "../../services/api"; import ButtonWithSpinner from "../ButtonWithSpinner"; import ContactModal from "../ContactModal"; -const useStyles = makeStyles(theme => ({ - root: { - display: "flex", - flexWrap: "wrap", - }, -})); - const filter = createFilterOptions({ trim: true, }); const NewTicketModal = ({ modalOpen, onClose }) => { const history = useHistory(); - const classes = useStyles(); const userId = +localStorage.getItem("userId"); const [options, setOptions] = useState([]); @@ -156,7 +146,7 @@ const NewTicketModal = ({ modalOpen, onClose }) => { }; return ( -
+ <> { -
+ ); }; diff --git a/frontend/src/components/Ticket/index.js b/frontend/src/components/Ticket/index.js index 1985b32..01eff14 100644 --- a/frontend/src/components/Ticket/index.js +++ b/frontend/src/components/Ticket/index.js @@ -101,7 +101,7 @@ const Ticket = () => { socket.on("ticket", data => { if (data.action === "updateStatus") { - setTicket({ ...data.ticket, user: data.user }); + setTicket(data.ticket); } if (data.action === "delete") { diff --git a/frontend/src/components/TicketOptionsMenu/index.js b/frontend/src/components/TicketOptionsMenu/index.js index 804a1a3..6aa21a6 100644 --- a/frontend/src/components/TicketOptionsMenu/index.js +++ b/frontend/src/components/TicketOptionsMenu/index.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { toast } from "react-toastify"; @@ -8,9 +8,18 @@ import Menu from "@material-ui/core/Menu"; import { i18n } from "../../translate/i18n"; import api from "../../services/api"; import ConfirmationModal from "../ConfirmationModal"; +import TransferTicketModal from "../TransferTicketModal"; const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { const [confirmationOpen, setConfirmationOpen] = useState(false); + const [transferTicketModalOpen, setTransferTicketModalOpen] = useState(false); + const isMounted = useRef(true); + + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); const handleDeleteTicket = async () => { try { @@ -34,6 +43,17 @@ const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { handleClose(); }; + const handleOpenTransferModal = e => { + setTransferTicketModalOpen(true); + handleClose(); + }; + + const handleCloseTransferTicketModal = () => { + if (isMounted.current) { + setTransferTicketModalOpen(false); + } + }; + return ( <> { {i18n.t("ticketOptionsMenu.delete")} - {i18n.t("ticketOptionsMenu.transfer")} + + {i18n.t("ticketOptionsMenu.transfer")} + { > {i18n.t("ticketOptionsMenu.confirmationModal.message")} + ); }; diff --git a/frontend/src/components/TransferTicketModal/index.js b/frontend/src/components/TransferTicketModal/index.js new file mode 100644 index 0000000..130286d --- /dev/null +++ b/frontend/src/components/TransferTicketModal/index.js @@ -0,0 +1,162 @@ +import React, { useState, useEffect } from "react"; +import { useHistory } from "react-router-dom"; +import { toast } from "react-toastify"; + +import Button from "@material-ui/core/Button"; +import TextField from "@material-ui/core/TextField"; +import Dialog from "@material-ui/core/Dialog"; + +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import Autocomplete, { + createFilterOptions, +} from "@material-ui/lab/Autocomplete"; +import CircularProgress from "@material-ui/core/CircularProgress"; + +import { i18n } from "../../translate/i18n"; +import api from "../../services/api"; +import ButtonWithSpinner from "../ButtonWithSpinner"; + +const filterOptions = createFilterOptions({ + trim: true, +}); + +const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => { + const history = useHistory(); + const [options, setOptions] = useState([]); + const [loading, setLoading] = useState(false); + const [searchParam, setSearchParam] = useState(""); + const [selectedUser, setSelectedUser] = useState(null); + + useEffect(() => { + if (!modalOpen || searchParam.length < 3) { + setLoading(false); + return; + } + setLoading(true); + const delayDebounceFn = setTimeout(() => { + const fetchUsers = async () => { + try { + const { data } = await api.get("/users/", { + params: { searchParam }, + }); + setOptions(data.users); + setLoading(false); + } catch (err) { + setLoading(false); + const errorMsg = err.response?.data?.error; + if (errorMsg) { + if (i18n.exists(`backendErrors.${errorMsg}`)) { + toast.error(i18n.t(`backendErrors.${errorMsg}`)); + } else { + toast.error(err.response.data.error); + } + } else { + toast.error("Unknown error"); + } + } + }; + + fetchUsers(); + }, 500); + return () => clearTimeout(delayDebounceFn); + }, [searchParam, modalOpen]); + + const handleClose = () => { + onClose(); + setSearchParam(""); + setSelectedUser(null); + }; + + const handleSaveTicket = async e => { + e.preventDefault(); + if (!ticketid || !selectedUser) return; + setLoading(true); + try { + await api.put(`/tickets/${ticketid}`, { + userId: selectedUser.id, + status: "open", + }); + setLoading(false); + history.push(`/tickets`); + } catch (err) { + setLoading(false); + const errorMsg = err.response?.data?.error; + if (errorMsg) { + if (i18n.exists(`backendErrors.${errorMsg}`)) { + toast.error(i18n.t(`backendErrors.${errorMsg}`)); + } else { + toast.error(err.response.data.error); + } + } else { + toast.error("Unknown error"); + } + } + }; + + return ( + +
+ + {i18n.t("transferTicketModal.title")} + + + `${option.name}`} + onChange={(e, newValue) => { + setSelectedUser(newValue); + }} + options={options} + filterOptions={filterOptions} + freeSolo + noOptionsText={i18n.t("transferTicketModal.noOptions")} + loading={loading} + renderInput={params => ( + setSearchParam(e.target.value)} + InputProps={{ + ...params.InputProps, + endAdornment: ( + + {loading ? ( + + ) : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + + + + + {i18n.t("transferTicketModal.buttons.ok")} + + +
+
+ ); +}; + +export default TransferTicketModal; diff --git a/frontend/src/translate/languages/en.js b/frontend/src/translate/languages/en.js index 4aad66c..da0690b 100644 --- a/frontend/src/translate/languages/en.js +++ b/frontend/src/translate/languages/en.js @@ -165,6 +165,15 @@ const messages = { showAll: "All", }, }, + transferTicketModal: { + title: "Transfer Ticket", + fieldLabel: "Type to search for users", + noOptions: "No user found with this name", + buttons: { + ok: "Transfer", + cancel: "Cancel", + }, + }, ticketsList: { pendingHeader: "Queue", assignedHeader: "Working on", diff --git a/frontend/src/translate/languages/es.js b/frontend/src/translate/languages/es.js index 5c7e6fd..2cc3dd2 100644 --- a/frontend/src/translate/languages/es.js +++ b/frontend/src/translate/languages/es.js @@ -168,6 +168,15 @@ const messages = { showAll: "Todos", }, }, + transferTicketModal: { + title: "Transferir Ticket", + fieldLabel: "Escriba para buscar usuarios", + noOptions: "No se encontraron usuarios con ese nombre", + buttons: { + ok: "Transferir", + cancel: "Cancelar", + }, + }, ticketsList: { pendingHeader: "Cola", assignedHeader: "Trabajando en", diff --git a/frontend/src/translate/languages/pt.js b/frontend/src/translate/languages/pt.js index 41a32da..b92c905 100644 --- a/frontend/src/translate/languages/pt.js +++ b/frontend/src/translate/languages/pt.js @@ -165,6 +165,15 @@ const messages = { showAll: "Todos", }, }, + transferTicketModal: { + title: "Transferir Ticket", + fieldLabel: "Digite para buscar usuários", + noOptions: "Nenhum usuário encontrado com esse nome", + buttons: { + ok: "Transferir", + cancel: "Cancelar", + }, + }, ticketsList: { pendingHeader: "Aguardando", assignedHeader: "Atendendo",