From d8ad464166449222e151e1a39baa8168cdfc5cef Mon Sep 17 00:00:00 2001 From: wirys da cunha francisco Date: Thu, 22 Oct 2020 13:57:22 -0300 Subject: [PATCH 1/4] Basic transfer functionality implemented --- .../src/components/TicketOptionsMenu/index.js | 21 +- .../components/TransferTicketModal/index.js | 187 ++++++++++++++++++ frontend/src/translate/languages/pt.js | 11 ++ 3 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/TransferTicketModal/index.js diff --git a/frontend/src/components/TicketOptionsMenu/index.js b/frontend/src/components/TicketOptionsMenu/index.js index 804a1a3..b12c422 100644 --- a/frontend/src/components/TicketOptionsMenu/index.js +++ b/frontend/src/components/TicketOptionsMenu/index.js @@ -8,9 +8,11 @@ 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 [TicketOpen, setTicketOpen] = useState(false); const handleDeleteTicket = async () => { try { @@ -55,7 +57,9 @@ const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { {i18n.t("ticketOptionsMenu.delete")} - {i18n.t("ticketOptionsMenu.transfer")} + setTicketOpen(true)} + >{i18n.t("ticketOptionsMenu.transfer")} { > {i18n.t("ticketOptionsMenu.confirmationModal.message")} + setTicketOpen(false)} + contactId = {ticket.contactId} + ticketid = {ticket.id} + + > + + + ); }; diff --git a/frontend/src/components/TransferTicketModal/index.js b/frontend/src/components/TransferTicketModal/index.js new file mode 100644 index 0000000..640fbd4 --- /dev/null +++ b/frontend/src/components/TransferTicketModal/index.js @@ -0,0 +1,187 @@ +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 { makeStyles } from "@material-ui/core/styles"; + +import { i18n } from "../../translate/i18n"; +import api from "../../services/api"; +import ButtonWithSpinner from "../ButtonWithSpinner"; + +const useStyles = makeStyles(theme => ({ + root: { + display: "flex", + flexWrap: "wrap", + }, +})); + +const filterOptions = createFilterOptions({ + trim: true, +}); + +const TransferTicketModal = ({ modalOpen, onClose,TechinicalId,ticketid }) => { + const history = useHistory(); + const classes = useStyles(); + const [options, setOptions] = useState([]); + const [loading, setLoading] = useState(false); + const [searchParam, setSearchParam] = useState(""); + const [selectedTechinical, setSelectedTechinical] = useState(null); + //const [hasMore, setHasMore] = useState(false); + + //const [user, setUser] = useState(null); + + + useEffect(() => { + setLoading(true); + const delayDebounceFn = setTimeout(() => { + const fetchUsers = async () => { + try { + const { data } = await api.get("/users/", { + params: { searchParam, pageNumber : 1 }, + }); + setOptions(data.users); + //setUser({ type: "LOAD_USERS", payload: data.users }); + //setHasMore(data.hasMore); + setLoading(false); + } catch (err) { + 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(""); + setSelectedTechinical(null); + }; + + const handleSaveTicket = async e => { + e.preventDefault(); + if (!selectedTechinical) return; + setLoading(true); + try { + + + + const { data: ticket } = await api.put("/tickets/"+ticketid , { + TechinicalId: TechinicalId, + userId: selectedTechinical.id, + status: "open", + }); + history.push(`/tickets/${ticket.id}`); + } catch (err) { + 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"+ err); + } + } + setLoading(false); + handleClose(); + }; + + return ( +
+ +
+ + {i18n.t("TransferTicketModal.title")} + + + + + `${option.name}`} + onChange={(e, newValue) => { + setSelectedTechinical(newValue); + }} + options={options} + filterOptions={filterOptions} + noOptionsText={i18n.t("newTicketModal.noOptions")} + loading={loading} + renderInput={params => ( + setSearchParam(e.target.value)} + id="my-input" + InputProps={{ + ...params.InputProps, + endAdornment: ( + + {loading ? ( + + ) : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + + + + + {i18n.t("newTicketModal.buttons.ok")} + + +
+
+
+ ); +}; + +export default TransferTicketModal; diff --git a/frontend/src/translate/languages/pt.js b/frontend/src/translate/languages/pt.js index 41a32da..0ee1d6c 100644 --- a/frontend/src/translate/languages/pt.js +++ b/frontend/src/translate/languages/pt.js @@ -164,6 +164,17 @@ const messages = { buttons: { showAll: "Todos", }, + }, + TransferTicketModal : { + title:"Transferir Chamado", + fieldLabel: "Digite aqui o nome do técnico", + buttons: { + ok : "Transferir", + cancel: "Cancelar", + } + + + }, ticketsList: { pendingHeader: "Aguardando", From 0ee3ccc44b4c69865977f892d5bcf2ff30db6a68 Mon Sep 17 00:00:00 2001 From: canove Date: Sat, 24 Oct 2020 18:30:48 -0300 Subject: [PATCH 2/4] feat: added transfer ticket option on frontend --- .../src/components/NewTicketModal/index.js | 14 +- .../src/components/TicketOptionsMenu/index.js | 46 +++-- .../components/TransferTicketModal/index.js | 178 ++++++++---------- frontend/src/translate/languages/en.js | 9 + frontend/src/translate/languages/pt.js | 16 +- 5 files changed, 121 insertions(+), 142 deletions(-) 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/TicketOptionsMenu/index.js b/frontend/src/components/TicketOptionsMenu/index.js index b12c422..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"; @@ -12,7 +12,14 @@ import TransferTicketModal from "../TransferTicketModal"; const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { const [confirmationOpen, setConfirmationOpen] = useState(false); - const [TicketOpen, setTicketOpen] = useState(false); + const [transferTicketModalOpen, setTransferTicketModalOpen] = useState(false); + const isMounted = useRef(true); + + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); const handleDeleteTicket = async () => { try { @@ -36,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")} - setTicketOpen(true)} - >{i18n.t("ticketOptionsMenu.transfer")} + + {i18n.t("ticketOptionsMenu.transfer")} + { {i18n.t("ticketOptionsMenu.confirmationModal.message")} setTicketOpen(false)} - contactId = {ticket.contactId} - ticketid = {ticket.id} - - > - - - + modalOpen={transferTicketModalOpen} + onClose={handleCloseTransferTicketModal} + ticketid={ticket.id} + /> ); }; diff --git a/frontend/src/components/TransferTicketModal/index.js b/frontend/src/components/TransferTicketModal/index.js index 640fbd4..33f4a32 100644 --- a/frontend/src/components/TransferTicketModal/index.js +++ b/frontend/src/components/TransferTicketModal/index.js @@ -14,48 +14,37 @@ 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"; -const useStyles = makeStyles(theme => ({ - root: { - display: "flex", - flexWrap: "wrap", - }, -})); - const filterOptions = createFilterOptions({ trim: true, }); -const TransferTicketModal = ({ modalOpen, onClose,TechinicalId,ticketid }) => { +const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => { const history = useHistory(); - const classes = useStyles(); const [options, setOptions] = useState([]); const [loading, setLoading] = useState(false); const [searchParam, setSearchParam] = useState(""); - const [selectedTechinical, setSelectedTechinical] = useState(null); - //const [hasMore, setHasMore] = useState(false); - - //const [user, setUser] = useState(null); - + 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, pageNumber : 1 }, + params: { searchParam }, }); setOptions(data.users); - //setUser({ type: "LOAD_USERS", payload: data.users }); - //setHasMore(data.hasMore); setLoading(false); } catch (err) { + setLoading(false); const errorMsg = err.response?.data?.error; if (errorMsg) { if (i18n.exists(`backendErrors.${errorMsg}`)) { @@ -68,33 +57,31 @@ const TransferTicketModal = ({ modalOpen, onClose,TechinicalId,ticketid }) => { } } }; + fetchUsers(); }, 500); return () => clearTimeout(delayDebounceFn); }, [searchParam, modalOpen]); - const handleClose = () => { onClose(); setSearchParam(""); - setSelectedTechinical(null); + setSelectedUser(null); }; const handleSaveTicket = async e => { e.preventDefault(); - if (!selectedTechinical) return; + if (!ticketid || !selectedUser) return; setLoading(true); try { - - - - const { data: ticket } = await api.put("/tickets/"+ticketid , { - TechinicalId: TechinicalId, - userId: selectedTechinical.id, + await api.put(`/tickets/${ticketid}`, { + userId: selectedUser.id, status: "open", }); - history.push(`/tickets/${ticket.id}`); + setLoading(false); + history.push(`/tickets`); } catch (err) { + setLoading(false); const errorMsg = err.response?.data?.error; if (errorMsg) { if (i18n.exists(`backendErrors.${errorMsg}`)) { @@ -103,84 +90,71 @@ const TransferTicketModal = ({ modalOpen, onClose,TechinicalId,ticketid }) => { toast.error(err.response.data.error); } } else { - toast.error("Unknown error"+ err); + toast.error("Unknown error"); } } - setLoading(false); - handleClose(); }; return ( -
- -
- - {i18n.t("TransferTicketModal.title")} - - - - - `${option.name}`} - onChange={(e, newValue) => { - setSelectedTechinical(newValue); - }} - options={options} - filterOptions={filterOptions} - noOptionsText={i18n.t("newTicketModal.noOptions")} - loading={loading} - renderInput={params => ( - setSearchParam(e.target.value)} - id="my-input" - InputProps={{ - ...params.InputProps, - endAdornment: ( - - {loading ? ( - - ) : null} - {params.InputProps.endAdornment} - - ), - }} - /> - )} - /> - - - - - {i18n.t("newTicketModal.buttons.ok")} - - -
-
-
+ +
+ + {i18n.t("transferTicketModal.title")} + + + `${option.name}`} + onChange={(e, newValue) => { + setSelectedUser(newValue); + }} + options={options} + filterOptions={filterOptions} + 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")} + + +
+
); }; diff --git a/frontend/src/translate/languages/en.js b/frontend/src/translate/languages/en.js index 4aad66c..f8860bc 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: "Transferir Ticket", + fieldLabel: "Digite aqui o nome do usuário", + noOptions: "Nenhum usuário encontrado com esse nome.", + buttons: { + ok: "Transferir", + cancel: "Cancelar", + }, + }, ticketsList: { pendingHeader: "Queue", assignedHeader: "Working on", diff --git a/frontend/src/translate/languages/pt.js b/frontend/src/translate/languages/pt.js index 0ee1d6c..6d4bd9b 100644 --- a/frontend/src/translate/languages/pt.js +++ b/frontend/src/translate/languages/pt.js @@ -165,16 +165,14 @@ const messages = { showAll: "Todos", }, }, - TransferTicketModal : { - title:"Transferir Chamado", - fieldLabel: "Digite aqui o nome do técnico", + transferTicketModal: { + title: "Transferir Ticket", + fieldLabel: "Digite aqui o nome do usuário", + noOptions: "Nenhum usuário encontrado com esse nome.", buttons: { - ok : "Transferir", - cancel: "Cancelar", - } - - - + ok: "Transferir", + cancel: "Cancelar", + }, }, ticketsList: { pendingHeader: "Aguardando", From c0611916e79bc054d130d673437d244712865024 Mon Sep 17 00:00:00 2001 From: canove Date: Sat, 24 Oct 2020 18:37:28 -0300 Subject: [PATCH 3/4] chore: updated translations --- frontend/src/components/TransferTicketModal/index.js | 1 + frontend/src/translate/languages/en.js | 10 +++++----- frontend/src/translate/languages/es.js | 9 +++++++++ frontend/src/translate/languages/pt.js | 4 ++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/TransferTicketModal/index.js b/frontend/src/components/TransferTicketModal/index.js index 33f4a32..130286d 100644 --- a/frontend/src/components/TransferTicketModal/index.js +++ b/frontend/src/components/TransferTicketModal/index.js @@ -110,6 +110,7 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => { }} options={options} filterOptions={filterOptions} + freeSolo noOptionsText={i18n.t("transferTicketModal.noOptions")} loading={loading} renderInput={params => ( diff --git a/frontend/src/translate/languages/en.js b/frontend/src/translate/languages/en.js index f8860bc..da0690b 100644 --- a/frontend/src/translate/languages/en.js +++ b/frontend/src/translate/languages/en.js @@ -166,12 +166,12 @@ const messages = { }, }, transferTicketModal: { - title: "Transferir Ticket", - fieldLabel: "Digite aqui o nome do usuário", - noOptions: "Nenhum usuário encontrado com esse nome.", + title: "Transfer Ticket", + fieldLabel: "Type to search for users", + noOptions: "No user found with this name", buttons: { - ok: "Transferir", - cancel: "Cancelar", + ok: "Transfer", + cancel: "Cancel", }, }, ticketsList: { 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 6d4bd9b..b92c905 100644 --- a/frontend/src/translate/languages/pt.js +++ b/frontend/src/translate/languages/pt.js @@ -167,8 +167,8 @@ const messages = { }, transferTicketModal: { title: "Transferir Ticket", - fieldLabel: "Digite aqui o nome do usuário", - noOptions: "Nenhum usuário encontrado com esse nome.", + fieldLabel: "Digite para buscar usuários", + noOptions: "Nenhum usuário encontrado com esse nome", buttons: { ok: "Transferir", cancel: "Cancelar", From 65deab09b41bc8e60bda3aab642efea31a02caf1 Mon Sep 17 00:00:00 2001 From: canove Date: Sat, 24 Oct 2020 19:01:04 -0300 Subject: [PATCH 4/4] feat: update tickets list on transfer --- backend/src/controllers/TicketController.ts | 7 +++---- backend/src/controllers/UserController.ts | 3 --- .../helpers/UpdateDeletedUserOpenTicketsStatus.ts | 5 ++--- .../services/TicketServices/UpdateTicketService.ts | 13 ++++++++++--- frontend/src/components/Ticket/index.js | 2 +- 5 files changed, 16 insertions(+), 14 deletions(-) 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/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") {