From 860d462d376ded45c5cc5fbf5a62cde7b0289243 Mon Sep 17 00:00:00 2001 From: gustavo-gsp Date: Thu, 18 Apr 2024 10:18:17 -0300 Subject: [PATCH 1/3] feat: add sound notifications and popOver for new tickets in waiting status, with option to enable in settings Details: - Implemented functionality to provide sound notifications and popOver alerts for new tickets in waiting status. Users can enable this feature in the settings. feat: add average waiting time in the Waiting tab on the Tickets screen, with option to enable in settings Details: - Added functionality to display the average waiting time in the Waiting tab on the Tickets screen. Users can enable this feature in the settings. feat: add ticket link in simple export directly from the reports table Details: - Implemented functionality to include the ticket link in the simple export directly from the reports table. feat: remove report export options for supervisor profile Details: - Removed the report export options for the supervisor profile to restrict access. --- backend/package.json | 1 + ...enable-block-audio-video-media-settings.ts | 22 ++++ ...dd-enable-waiting-time-tickets-settings.ts | 22 ++++ ...-add-enable-notification-transfer-queue.ts | 22 ++++ .../WbotServices/wbotMessageListener.ts | 53 +++++--- .../components/NotificationsPopOver/index.js | 115 +++++++++++++----- .../src/components/TicketsManager/index.js | 91 +++++++++++++- frontend/src/pages/Report/index.js | 98 ++++++++------- 8 files changed, 324 insertions(+), 100 deletions(-) create mode 100644 backend/src/database/seeds/20240416141716-add-enable-block-audio-video-media-settings.ts create mode 100644 backend/src/database/seeds/20240417151300-add-enable-waiting-time-tickets-settings.ts create mode 100644 backend/src/database/seeds/20240417185331-add-enable-notification-transfer-queue.ts diff --git a/backend/package.json b/backend/package.json index 1c8646e..1e5bd13 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,6 +8,7 @@ "watch": "tsc -w", "start": "nodemon --expose-gc dist/server.js", "dev:server": "ts-node-dev --respawn --transpile-only --ignore node_modules src/server.ts", + "dev": "nodemon --exec npm run dev:server", "pretest": "NODE_ENV=test sequelize db:migrate && NODE_ENV=test sequelize db:seed:all", "test": "NODE_ENV=test jest", "posttest": "NODE_ENV=test sequelize db:migrate:undo:all" diff --git a/backend/src/database/seeds/20240416141716-add-enable-block-audio-video-media-settings.ts b/backend/src/database/seeds/20240416141716-add-enable-block-audio-video-media-settings.ts new file mode 100644 index 0000000..511de98 --- /dev/null +++ b/backend/src/database/seeds/20240416141716-add-enable-block-audio-video-media-settings.ts @@ -0,0 +1,22 @@ +import { QueryInterface } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.bulkInsert( + "Settings", + [ + { + key: "blockAudioVideoMedia", + value: "disabled", + createdAt: new Date(), + updatedAt: new Date() + } + ], + {} + ); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.bulkDelete("Settings", {}); + } +}; diff --git a/backend/src/database/seeds/20240417151300-add-enable-waiting-time-tickets-settings.ts b/backend/src/database/seeds/20240417151300-add-enable-waiting-time-tickets-settings.ts new file mode 100644 index 0000000..8bb6828 --- /dev/null +++ b/backend/src/database/seeds/20240417151300-add-enable-waiting-time-tickets-settings.ts @@ -0,0 +1,22 @@ +import { QueryInterface } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.bulkInsert( + "Settings", + [ + { + key: "waitingTimeTickets", + value: "disabled", + createdAt: new Date(), + updatedAt: new Date() + } + ], + {} + ); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.bulkDelete("Settings", {}); + } +}; diff --git a/backend/src/database/seeds/20240417185331-add-enable-notification-transfer-queue.ts b/backend/src/database/seeds/20240417185331-add-enable-notification-transfer-queue.ts new file mode 100644 index 0000000..00c4069 --- /dev/null +++ b/backend/src/database/seeds/20240417185331-add-enable-notification-transfer-queue.ts @@ -0,0 +1,22 @@ +import { QueryInterface } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.bulkInsert( + "Settings", + [ + { + key: "notificationTransferQueue", + value: "disabled", + createdAt: new Date(), + updatedAt: new Date() + } + ], + {} + ); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.bulkDelete("Settings", {}); + } +}; diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 2739c73..f76c338 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -165,7 +165,7 @@ const verifyMediaMessage = async ( if (!media) { throw new Error("ERR_WAPP_DOWNLOAD_MEDIA"); } - + let mediaAuthorized = true; let messageData = { id: msg.id.id, ticketId: ticket.id, @@ -179,7 +179,9 @@ const verifyMediaMessage = async ( phoneNumberId: msg?.phoneNumberId, fromAgent: false }; - + if(messageData.mediaType === 'video' || messageData.mediaType === 'audio' && getSettingValue('blockAudioVideoMedia')?.value === 'enabled'){ + mediaAuthorized = false; + } if (msg?.fromMe) { messageData = { ...messageData, fromAgent: true }; } @@ -194,23 +196,36 @@ const verifyMediaMessage = async ( body: media.filename }; } - - try { - await writeFileAsync( - join(__dirname, "..", "..", "..", "..", "..", "public", media.filename), - media.data, - "base64" - ); - } catch (err) { - Sentry.captureException(err); - logger.error(`There was an error: wbotMessageLitener.ts: ${err}`); + if(mediaAuthorized){ + try { + await writeFileAsync( + join(__dirname, "..", "..", "..", "..", "..", "public", media.filename), + media.data, + "base64" + ); + } catch (err) { + Sentry.captureException(err); + logger.error(`There was an error: wbotMessageLitener.ts: ${err}`); + } } } - - await ticket.update({ lastMessage: msg.body || media.filename }); - const newMessage = await CreateMessageService({ messageData }); - - return newMessage; + if(mediaAuthorized){ + await ticket.update({ lastMessage: msg.body || media.filename }); + const newMessage = await CreateMessageService({ messageData }); + return newMessage; + }else{ + if (ticket.status !== "queueChoice") { + botSendMessage( + ticket, + `Atenção! Mensagem ignorada, tipo de mídia não suportado.` + ); + } + messageData.body = `Mensagem de *${messageData.mediaType}* ignorada, tipo de mídia não suportado.`; + messageData.mediaUrl = ''; + await ticket.update({ lastMessage: `Mensagem de *${messageData.mediaType}* ignorada, tipo de mídia não suportado.`}); + const newMessage = await CreateMessageService({ messageData }); + console.log(`--------->>> Mensagem do tipo: ${messageData.mediaType}, ignorada!`) + } }; // const verifyMediaMessage = async ( @@ -397,13 +412,15 @@ const verifyQueue = async ( } let body = ""; - + const io = getIO(); if (botOptions.length > 0) { body = `\u200e${choosenQueue.greetingMessage}\n\n${botOptions}\n${final_message.msg}`; } else { body = `\u200e${choosenQueue.greetingMessage}`; } + io.emit('notifyPeding', {data: {ticket, queue: choosenQueue}}); + sendWhatsAppMessageSocket(ticket, body); } else { //test del transfere o atendimento se entrar na ura infinita diff --git a/frontend/src/components/NotificationsPopOver/index.js b/frontend/src/components/NotificationsPopOver/index.js index 7840796..1e46dd1 100644 --- a/frontend/src/components/NotificationsPopOver/index.js +++ b/frontend/src/components/NotificationsPopOver/index.js @@ -20,6 +20,9 @@ import useTickets from "../../hooks/useTickets" import alertSound from "../../assets/sound.mp3" import { AuthContext } from "../../context/Auth/AuthContext" +import api from "../../services/api"; +import toastError from "../../errors/toastError"; + const useStyles = makeStyles(theme => ({ tabContainer: { overflowY: "auto", @@ -83,7 +86,7 @@ const NotificationsPopOver = () => { const historyRef = useRef(history) const { handleLogout } = useContext(AuthContext) - + const [settings, setSettings] = useState([]); // const [lastRef] = useState(+history.location.pathname.split("/")[2]) @@ -110,7 +113,22 @@ const NotificationsPopOver = () => { ticketIdRef.current = ticketIdUrl }, [ticketIdUrl]) + useEffect(() => { + const fetchSession = async () => { + try { + const { data } = await api.get('/settings') + setSettings(data.settings) + } catch (err) { + toastError(err) + } + } + fetchSession() + }, []) + const getSettingValue = (key) => { + const { value } = settings.find((s) => s.key === key) + return value + } useEffect(() => { @@ -255,49 +273,80 @@ const NotificationsPopOver = () => { if (shouldNotNotificate) return - - handleNotifications(data) } }) + socket.on('notifyPeding', data =>{ + if(settings?.length > 0 && getSettingValue('notificationTransferQueue') === 'enabled') handleNotifications("", data); + }); + return () => { socket.disconnect() } - }, [user]) + }, [user, settings]) - const handleNotifications = data => { - const { message, contact, ticket } = data + const handleNotifications = (data, notify) => { + let isQueue = false; + if(!notify){ + const { message, contact, ticket } = data - const options = { - body: `${message.body} - ${format(new Date(), "HH:mm")}`, - icon: contact.profilePicUrl, - tag: ticket.id, - renotify: true, - } - - const notification = new Notification( - `${i18n.t("tickets.notification.message")} ${contact.name}`, - options - ) - - notification.onclick = e => { - e.preventDefault() - window.focus() - historyRef.current.push(`/tickets/${ticket.id}`) - } - - setDesktopNotifications(prevState => { - const notfiticationIndex = prevState.findIndex( - n => n.tag === notification.tag - ) - if (notfiticationIndex !== -1) { - prevState[notfiticationIndex] = notification - return [...prevState] + const options = { + body: `${message.body} - ${format(new Date(), "HH:mm")}`, + icon: contact.profilePicUrl, + tag: ticket.id, + renotify: true, } - return [notification, ...prevState] - }) + const notification = new Notification( + `${i18n.t("tickets.notification.message")} ${contact.name}`, + options + ) + + notification.onclick = e => { + e.preventDefault() + window.focus() + historyRef.current.push(`/tickets/${ticket.id}`) + } + + setDesktopNotifications(prevState => { + const notfiticationIndex = prevState.findIndex( + n => n.tag === notification.tag + ) + if (notfiticationIndex !== -1) { + prevState[notfiticationIndex] = notification + return [...prevState] + } + return [notification, ...prevState] + }) + }else{ + user.queues.forEach(queue =>{ + if(queue.id === notify.data?.queue?.id){ + isQueue = true; + } + }) + if(!isQueue){ + return; + }else { + const notification = new Notification(`${i18n.t("tickets.notification.messagePeding")} ${notify.data?.queue?.name}`); + notification.onclick = e => { + e.preventDefault() + window.focus() + historyRef.current.push(`/tickets`) + } + + setDesktopNotifications(prevState => { + const notfiticationIndex = prevState.findIndex( + n => n.tag === notification.tag + ) + if (notfiticationIndex !== -1) { + prevState[notfiticationIndex] = notification + return [...prevState] + } + return [notification, ...prevState] + }) + } + } soundAlertRef.current() } diff --git a/frontend/src/components/TicketsManager/index.js b/frontend/src/components/TicketsManager/index.js index 06ac9c9..ee0db3e 100644 --- a/frontend/src/components/TicketsManager/index.js +++ b/frontend/src/components/TicketsManager/index.js @@ -33,6 +33,9 @@ import { Button } from "@material-ui/core"; import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption"; import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket"; +import useTickets from "../../hooks/useTickets" +import api from "../../services/api"; +import toastError from "../../errors/toastError"; const useStyles = makeStyles((theme) => ({ ticketsWrapper: { @@ -157,6 +160,10 @@ const TicketsManager = () => { const [openTooltipSearch, setOpenTooltipSearch] = useState(false) + const [waitingTime, setWaitingTime] = useState('00:00'); + const [tickets, setTickets] = useState([]); + const [settings, setSettings] = useState([]) + let searchTimeout; let searchContentTimeout; @@ -178,6 +185,76 @@ const TicketsManager = () => { }, [tab, setTabOption]); + useEffect(() => { + const fetchSession = async () => { + try { + const { data } = await api.get('/settings') + setSettings(data.settings) + } catch (err) { + toastError(err) + } + } + fetchSession() + }, []) + + const getSettingValue = (key) => { + const { value } = settings.find((s) => s.key === key) + return value + } + + const fetchTickets = async () =>{ + try { + const { data } = await api.get("/tickets", { + params: { + status: 'pending', + queueIds: JSON.stringify(selectedQueueIds) + }, + }); + setTickets(data.tickets); + } catch (err) { + toastError(err); + } + + } + useEffect(() => { + if(settings?.length > 0 && getSettingValue('waitingTimeTickets') === 'enabled') { + fetchTickets(); + + const intervalId = setInterval(fetchTickets, 7000); + + return () => { + clearInterval(intervalId); + }; + } + }, [selectedQueueIds, settings]); + + useEffect(() => { + const calculateAverageTime = () => { + if(tickets.length > 0){ + const now = new Date(); + const differenceTime = tickets?.map(ticket => { + const updatedAt = new Date(ticket.updatedAt); + const difference = now - updatedAt; + return difference; + }); + const sumDifferences = differenceTime.reduce((total, difference) => total + difference, 0); + const averageTimeMilliseconds = sumDifferences / tickets?.length; + let hours = Math.floor(averageTimeMilliseconds / 3600000); + const minutes = Math.floor((averageTimeMilliseconds % 3600000) / 60000); + + let days = hours >= 24 ? parseInt(hours/24) : ''; + + if(days != '') hours = hours - (24*days); + + const averageTimeFormated = `${days != '' ? `${days}d ` : days}${hours.toString().padStart(2, '0')}h${minutes.toString().padStart(2, '0')}`; + + return averageTimeFormated; + }else return '00:00'; + } + + setWaitingTime(calculateAverageTime()); + },[tickets]); + useEffect(() => { // clearTimeout(searchContentTimeout); @@ -203,7 +280,7 @@ const TicketsManager = () => { useEffect(() => { - + //console.log(selectedQueueIds); if (tabOption === 'open') { setTabOption('') @@ -448,7 +525,17 @@ const TicketsManager = () => { } value={"pending"} - /> + />{ + (settings?.length > 0 && getSettingValue('waitingTimeTickets') === 'enabled') && + + + + + } { const tickets = data.tickets.map(ticket => ({ ...ticket, messagesToFilter: ticket.messages.map(message => message.body).join(' '), + link: `${process.env.REACT_APP_FRONTEND_URL}/tickets/${ticket.id}` })) dispatchQ({ type: "LOAD_QUERY", payload: tickets }) - console.log(tickets) setHasMore(data.hasMore) setTotalCountTickets(data.count) setLoading(false) @@ -682,54 +684,56 @@ const Report = () => { const renderSwitch = (param) => { - switch (param) { - case 'empty': - return ( - <> - {query && query.length > 0 && - - } - {/* */} - ) + if(userA.profile !== 'supervisor'){ + switch (param) { + case 'empty': + return ( + <> + {query && query.length > 0 && + + } + {/* */} + ) - case 'pending' || 'processing': - return ( - <> - PROCESSING... - ) + case 'pending' || 'processing': + return ( + <> + PROCESSING... + ) - case 'success': - return ( - <> - - ) - case 'downloading': - return ( - <> - DOWNLOADING... - ) + case 'success': + return ( + <> + + ) + case 'downloading': + return ( + <> + DOWNLOADING... + ) - default: - return (<>WAITING...) + default: + return (<>WAITING...) + } } } @@ -840,7 +844,7 @@ const Report = () => { <> Date: Mon, 29 Apr 2024 10:00:51 -0300 Subject: [PATCH 2/3] feat: add new column 'transferToOtherQueues' to the Users table Details: - Created a new column named 'transferToOtherQueues' in the Users table. feat: add field 'can transfer to other queues' in the user modal Details: - Added a field in the user modal to indicate if the user can transfer tickets to other queues. feat: implement functionality to allow users with 'transferToOtherTickets' set to true to transfer tickets to other queues, even when disabled in OmniHit Details: - Implemented functionality to allow users with 'transferToOtherTickets' set to true to transfer tickets to other queues, even when this feature is disabled in OmniHit. feat: add options to enable or disable functionalities: notify on new ticket arrival, block audio and video messages, show average waiting time of tickets in waiting status Details: - Added options to enable or disable the following functionalities: notification on new ticket arrival, blocking of audio and video messages, and displaying average waiting time of tickets in waiting status. feat: add filter in the dashboard to show data only from the queues the user is linked to, and allow the user to view data from a selected queue only Details: - Added a filter in the dashboard to display data only from the queues the user is linked to, and to allow the user to view data from a selected queue only. --- backend/src/controllers/ReportController.ts | 12 ++- backend/src/controllers/TicketController.ts | 5 +- backend/src/controllers/UserController.ts | 10 ++- ...6-add-transfer-to-other-queues-to-users.ts | 15 ++++ backend/src/models/User.ts | 3 + .../CountStatusChatEndService.ts | 9 +- .../TicketServices/CountTicketService.ts | 6 +- .../TicketServices/ListTicketsService.ts | 3 +- .../UserServices/CreateUserService.ts | 8 +- .../UserServices/ListUserParamiterService.ts | 29 ++++++- .../services/UserServices/ListUsersService.ts | 3 +- .../services/UserServices/ShowUserService.ts | 3 +- .../UserServices/UpdateUserService.ts | 10 ++- .../components/Report/SelectField/index.js | 10 ++- .../src/components/TicketsManager/index.js | 17 ++-- .../components/TransferTicketModal/index.js | 6 +- frontend/src/components/UserModal/index.js | 26 +++++- frontend/src/pages/Dashboard/Chart.js | 10 ++- frontend/src/pages/Dashboard/index.js | 48 ++++++++--- frontend/src/pages/Settings/index.js | 82 +++++++++++++++++++ 20 files changed, 259 insertions(+), 56 deletions(-) create mode 100644 backend/src/database/migrations/20240425190416-add-transfer-to-other-queues-to-users.ts diff --git a/backend/src/controllers/ReportController.ts b/backend/src/controllers/ReportController.ts index d7d70b3..36c0c7c 100644 --- a/backend/src/controllers/ReportController.ts +++ b/backend/src/controllers/ReportController.ts @@ -65,7 +65,7 @@ export const reportUserByDateStartDateEnd = async ( endDate, pageNumber, createdOrUpdated, - queueId + queueId, }); const queues = await Queue.findAll({ attributes: ["id", "name"] }); @@ -84,11 +84,12 @@ export const reportUserService = async ( ) { throw new AppError("ERR_NO_PERMISSION", 403); } - const { userId, startDate, endDate } = req.query as IndexQuery; + const { userId, startDate, endDate, userQueues} = req.query as IndexQuery; // let usersProfile = await ListUserParamiterService({ profile: 'user' }) let usersProfile = await ListUserParamiterService({ profiles: ["user", "supervisor"], + userQueues: userQueues ? userQueues : undefined, raw: true }); @@ -351,15 +352,18 @@ export const reportTicksCountByStatusChatEnds = async ( throw new AppError("ERR_NO_PERMISSION", 403); } - const { startDate, endDate } = req.query as IndexQuery; + const { startDate, endDate, userQueues } = req.query as IndexQuery; const dateToday = splitDateTime( new Date(format(new Date(), "yyyy-MM-dd HH:mm:ss", { locale: ptBR })) ); + + const queueIds = userQueues ? userQueues.map(queue => parseInt(queue)) : []; const reportStatusChatEnd = await CountStatusChatEndService( startDate || dateToday.fullDate, - endDate || dateToday.fullDate + endDate || dateToday.fullDate, + queueIds ); return res.status(200).json({ reportStatusChatEnd }); diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index 272bc10..c4a59a8 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -362,9 +362,8 @@ export const show = async (req: Request, res: Response): Promise => { export const count = async (req: Request, res: Response): Promise => { // type indexQ = { status: string; date?: string; }; - const { status, date } = req.query as IndexQuery; - - const ticketCount = await CountTicketService(status, date); + const { status, date, queueIds } = req.query as IndexQuery; + const ticketCount = await CountTicketService(status, date, queueIds); return res.status(200).json(ticketCount); }; diff --git a/backend/src/controllers/UserController.ts b/backend/src/controllers/UserController.ts index 7bfcf5f..20dc4ee 100644 --- a/backend/src/controllers/UserController.ts +++ b/backend/src/controllers/UserController.ts @@ -100,7 +100,7 @@ export const index = async (req: Request, res: Response): Promise => { // }; export const all = async (req: Request, res: Response): Promise => { - let { userId, profile }: any = req.query as IndexQuery; + let { userId, profile, transferToOtherQueues }: any = req.query as IndexQuery; console.log( "userId: ", @@ -111,7 +111,7 @@ export const all = async (req: Request, res: Response): Promise => { getSettingValue("queueTransferByWhatsappScope")?.value ); - if (getSettingValue("queueTransferByWhatsappScope")?.value == "enabled") { + if (getSettingValue("queueTransferByWhatsappScope")?.value == "enabled" && !transferToOtherQueues) { if (!userId) return res.json({ users: [], queues: [] }); const obj = await ListUserByWhatsappQueuesService( @@ -145,7 +145,8 @@ export const store = async (req: Request, res: Response): Promise => { profile, positionCompany, positionId, - queueIds + queueIds, + transferToOtherQueues } = req.body; console.log("===========> req.url: ", req.url); @@ -172,7 +173,8 @@ export const store = async (req: Request, res: Response): Promise => { positionCompany, positionId, profile, - queueIds + queueIds, + transferToOtherQueues }); if (user) { diff --git a/backend/src/database/migrations/20240425190416-add-transfer-to-other-queues-to-users.ts b/backend/src/database/migrations/20240425190416-add-transfer-to-other-queues-to-users.ts new file mode 100644 index 0000000..8a653a8 --- /dev/null +++ b/backend/src/database/migrations/20240425190416-add-transfer-to-other-queues-to-users.ts @@ -0,0 +1,15 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Users", "transferToOtherQueues", { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Users", "transferToOtherQueues"); + } +}; \ No newline at end of file diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 1729d96..f4db2f7 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -51,6 +51,9 @@ class User extends Model { @Column secondaryId: string; + @Column + transferToOtherQueues: boolean; + @Default("admin") @Column profile: string; diff --git a/backend/src/services/StatusChatEndService/CountStatusChatEndService.ts b/backend/src/services/StatusChatEndService/CountStatusChatEndService.ts index bffbb5d..3a7cf0c 100644 --- a/backend/src/services/StatusChatEndService/CountStatusChatEndService.ts +++ b/backend/src/services/StatusChatEndService/CountStatusChatEndService.ts @@ -11,13 +11,14 @@ const { QueryTypes } = require("sequelize"); const CountStatusChatEndService = async ( startDate: string, - endDate: string + endDate: string, + queueIds?: number[] ) => { - const countStatusChatEnd: any = await sequelize.query( `select t.id, s.name, count(t.id) as count from Tickets t join StatusChatEnds s on -t.statusChatEndId = s.id and DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' -group by s.id;`, + t.statusChatEndId = s.id and DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + AND t.queueId IN (${queueIds}) + group by s.id;`, { type: QueryTypes.SELECT } ); diff --git a/backend/src/services/TicketServices/CountTicketService.ts b/backend/src/services/TicketServices/CountTicketService.ts index 2cdde93..08f9de3 100644 --- a/backend/src/services/TicketServices/CountTicketService.ts +++ b/backend/src/services/TicketServices/CountTicketService.ts @@ -9,7 +9,7 @@ import ptBR from 'date-fns/locale/pt-BR'; import { splitDateTime } from "../../helpers/SplitDateTime"; const dateToday = splitDateTime(new Date(format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ptBR }))) -const CountTicketService = async (status: string, date?: string): Promise => { +const CountTicketService = async (status: string, date?: string, queueIds?: string): Promise => { let where_clause = {} @@ -30,8 +30,8 @@ const CountTicketService = async (status: string, date?: string): Promise = // } } - - where_clause = { ...where_clause, status: status } + if(queueIds) where_clause = { ...where_clause, status: status, queueId: { [Op.or]: [queueIds, null] } }; + else where_clause = { ...where_clause, status: status}; const ticket = await Ticket.findAll({ where: where_clause, diff --git a/backend/src/services/TicketServices/ListTicketsService.ts b/backend/src/services/TicketServices/ListTicketsService.ts index 4d31e3f..1b8d881 100644 --- a/backend/src/services/TicketServices/ListTicketsService.ts +++ b/backend/src/services/TicketServices/ListTicketsService.ts @@ -204,7 +204,8 @@ const ListTicketsService = async ({ whereCondition = { createdAt: { [Op.between]: [+startOfDay(parseISO(date)), +endOfDay(parseISO(date))] - } + }, + queueId: { [Op.or]: [queueIds, null] }, }; } diff --git a/backend/src/services/UserServices/CreateUserService.ts b/backend/src/services/UserServices/CreateUserService.ts index f866a9b..9e4117c 100644 --- a/backend/src/services/UserServices/CreateUserService.ts +++ b/backend/src/services/UserServices/CreateUserService.ts @@ -14,6 +14,7 @@ interface Request { queueIds?: number[]; profile?: string; ignoreThrow?: boolean; + transferToOtherQueues?: boolean; } interface Response { @@ -23,6 +24,7 @@ interface Response { positionId: string; id: number; profile: string; + transferToOtherQueues: boolean; } const CreateUserService = async ({ @@ -33,7 +35,8 @@ const CreateUserService = async ({ positionId, queueIds = [], profile = "master", - ignoreThrow = false + ignoreThrow = false, + transferToOtherQueues }: Request): Promise => { try { const schema = Yup.object().shape({ @@ -84,7 +87,8 @@ const CreateUserService = async ({ name, positionCompany, positionId: !positionId ? null : positionId, - profile + profile, + transferToOtherQueues: transferToOtherQueues? transferToOtherQueues : false }, { include: ["queues"] } ); diff --git a/backend/src/services/UserServices/ListUserParamiterService.ts b/backend/src/services/UserServices/ListUserParamiterService.ts index 2d92498..2198936 100644 --- a/backend/src/services/UserServices/ListUserParamiterService.ts +++ b/backend/src/services/UserServices/ListUserParamiterService.ts @@ -10,6 +10,7 @@ interface Request { profiles?: Array; raw?: boolean; userIds?: string | number; + userQueues?: string[]; } const ListUser = async ({ @@ -17,10 +18,31 @@ const ListUser = async ({ userId, raw, userIds, - profiles + profiles, + userQueues: userQueuesToNumber }: Request): Promise => { let where_clause = {}; + let userIdInQueues: number[] = []; + + if(userQueuesToNumber !== undefined){ + let userQueues = userQueuesToNumber.map(id => parseInt(id)); + const userQueuesFiltered = await UserQueue.findAll({ + where: { queueId: { [Op.or]: [userQueues, null] } }, + order: [ + ['userId', 'ASC'] + ], + raw: true + }); + if(userQueuesFiltered) for(let queueId of userQueues){ + for(let userQueue of userQueuesFiltered){ + if(queueId == userQueue.queueId){ + const isAlready = userIdInQueues.indexOf(userQueue.userId); + if(isAlready === -1) userIdInQueues.push(userQueue.userId); + } + } + } + } if (userId && profile) { where_clause = { [Op.and]: [{ userId: userId }, { profile: profile }] @@ -39,14 +61,15 @@ const ListUser = async ({ }; } else if (profiles) { where_clause = { - profile: { [Op.in]: profiles } + profile: { [Op.in]: profiles }, + id: {[Op.in]: userIdInQueues} }; } const users = await User.findAll({ where: where_clause, raw, - attributes: ["id", "name", "email", "positionCompany"], + attributes: ["id", "name", "email", "positionCompany", "transferToOtherQueues"], include: [ { model: Queue, as: "queues", attributes: ["id", "name", "color"] } diff --git a/backend/src/services/UserServices/ListUsersService.ts b/backend/src/services/UserServices/ListUsersService.ts index a55ca5c..bfcb4d3 100644 --- a/backend/src/services/UserServices/ListUsersService.ts +++ b/backend/src/services/UserServices/ListUsersService.ts @@ -66,7 +66,8 @@ const ListUsersService = async ({ "email", "positionCompany", "profile", - "createdAt" + "createdAt", + "transferToOtherQueues" ], limit, offset, diff --git a/backend/src/services/UserServices/ShowUserService.ts b/backend/src/services/UserServices/ShowUserService.ts index 1f3d272..8b9d6c8 100644 --- a/backend/src/services/UserServices/ShowUserService.ts +++ b/backend/src/services/UserServices/ShowUserService.ts @@ -12,7 +12,8 @@ const ShowUserService = async (id: string | number): Promise => { "profile", "positionCompany", "positionId", - "tokenVersion" + "tokenVersion", + "transferToOtherQueues" ], include: [ { model: Queue, as: "queues", attributes: ["id", "name", "color"] }, diff --git a/backend/src/services/UserServices/UpdateUserService.ts b/backend/src/services/UserServices/UpdateUserService.ts index 78a5fd2..a4f88f4 100644 --- a/backend/src/services/UserServices/UpdateUserService.ts +++ b/backend/src/services/UserServices/UpdateUserService.ts @@ -12,6 +12,7 @@ interface UserData { positionId?: string; profile?: string; queueIds?: number[]; + transferToOtherQueues: boolean; } interface Request { @@ -75,7 +76,8 @@ const UpdateUserService = async ({ name, positionCompany, positionId, - queueIds = [] + queueIds = [], + transferToOtherQueues } = userData; try { @@ -90,7 +92,8 @@ const UpdateUserService = async ({ profile, positionCompany, positionId: !positionId ? null : positionId, - name + name, + transferToOtherQueues }); await user.$set("queues", queueIds); @@ -117,7 +120,8 @@ const UpdateUserService = async ({ profile: _user.profile, queues: _user.queues, positionId: _user?.positionId, - position: _user.position + position: _user.position, + transferToOtherQueues: _user.transferToOtherQueues }; return serializedUser; diff --git a/frontend/src/components/Report/SelectField/index.js b/frontend/src/components/Report/SelectField/index.js index ef14f72..b8b48c0 100644 --- a/frontend/src/components/Report/SelectField/index.js +++ b/frontend/src/components/Report/SelectField/index.js @@ -10,11 +10,19 @@ const SelectTextFields = (props) => { if (!props.textBoxFieldSelected) { props.currencies.push({ 'value': 0, 'label': '' }) } + + if(props.textBoxFieldSelected === 'All'){ + const already = props.currencies.findIndex(obj => obj.value === 'All'); + if (already === -1) { + props.currencies.push({ 'value': 'All', 'label': 'All' }) + } + } + useEffect(() => { props.func(currency) - }, [currency, props]) + }, [currency, props.textBoxFieldSelected]) const handleChange = (event) => { diff --git a/frontend/src/components/TicketsManager/index.js b/frontend/src/components/TicketsManager/index.js index ee0db3e..21e8a87 100644 --- a/frontend/src/components/TicketsManager/index.js +++ b/frontend/src/components/TicketsManager/index.js @@ -19,6 +19,7 @@ import FindInPageIcon from '@material-ui/icons/FindInPage'; import FormControlLabel from "@material-ui/core/FormControlLabel"; import Switch from "@material-ui/core/Switch"; +import openSocket from "socket.io-client" import NewTicketModal from "../NewTicketModal"; import TicketsList from "../TicketsList"; @@ -219,12 +220,18 @@ const TicketsManager = () => { useEffect(() => { if(settings?.length > 0 && getSettingValue('waitingTimeTickets') === 'enabled') { fetchTickets(); + const intervalId = setInterval(fetchTickets, 55000); + const socket = openSocket(process.env.REACT_APP_BACKEND_URL) - const intervalId = setInterval(fetchTickets, 7000); - - return () => { - clearInterval(intervalId); - }; + socket.on("ticketStatus", (data) => { + if (data.action === "update") { + fetchTickets(); + } + }) + return () => { + socket.disconnect() + clearInterval(intervalId) + } } }, [selectedQueueIds, settings]); diff --git a/frontend/src/components/TransferTicketModal/index.js b/frontend/src/components/TransferTicketModal/index.js index 472565b..02acdfc 100644 --- a/frontend/src/components/TransferTicketModal/index.js +++ b/frontend/src/components/TransferTicketModal/index.js @@ -97,7 +97,7 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => { } else { - if (settings?.find(e => e?.key === 'queueTransferByWhatsappScope')?.value === 'enabled') { + if (settings?.find(e => e?.key === 'queueTransferByWhatsappScope')?.value === 'enabled' && !user.transferToOtherQueues) { setQueues(_queues) } else { @@ -190,7 +190,7 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => { try { - if (settings?.find(e => e?.key === 'queueTransferByWhatsappScope')?.value === 'enabled') { + if (settings?.find(e => e?.key === 'queueTransferByWhatsappScope')?.value === 'enabled' && !user.transferToOtherQueues) { const { data } = await api.get(`/users/all`, { params: { userId: user.id }, }) @@ -202,7 +202,7 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => { else { const { data } = await api.get(`/users/all`, { - params: { profile: 'user' }, + params: { profile: 'user', transferToOtherQueues: user.transferToOtherQueues }, }) setUsers(data.users) diff --git a/frontend/src/components/UserModal/index.js b/frontend/src/components/UserModal/index.js index 51d660d..976fc8c 100644 --- a/frontend/src/components/UserModal/index.js +++ b/frontend/src/components/UserModal/index.js @@ -32,6 +32,7 @@ import toastError from "../../errors/toastError" import QueueSelect from "../QueueSelect" import { AuthContext } from "../../context/Auth/AuthContext" import { Can } from "../Can" +import Switch from '@mui/material/Switch' const useStyles = makeStyles(theme => ({ root: { @@ -95,6 +96,7 @@ const UserModal = ({ open, onClose, userId, }) => { const [showPassword, setShowPassword] = useState(false) const [positions, setPositions] = useState([]) const [selectedPosition, setSelectedPosition] = useState('') + const [checked, setChecked] = useState(false) useEffect(() => { const fetchUser = async () => { @@ -112,6 +114,9 @@ const UserModal = ({ open, onClose, userId, }) => { setSelectedPosition(data.positionId) else setSelectedPosition('') + + + if(data.transferToOtherQueues) setChecked(data.transferToOtherQueues); } catch (err) { toastError(err) } @@ -136,10 +141,15 @@ const UserModal = ({ open, onClose, userId, }) => { const handleClose = () => { onClose() setUser(initialState) + setChecked(false); + } + + const handleChange = (event) => { + setChecked(event.target.checked) } const handleSaveUser = async values => { - const userData = { ...values, queueIds: selectedQueueIds, positionId: selectedPosition } + const userData = { ...values, queueIds: selectedQueueIds, positionId: selectedPosition, transferToOtherQueues: checked} try { if (userId) { @@ -252,7 +262,7 @@ const UserModal = ({ open, onClose, userId, }) => { fullWidth />
- { variant="outlined" margin="dense" fullWidth - /> + /> */} + + { const theme = useTheme(); const date = useRef(new Date().toISOString()); - let { tickets } = useTickets({ date: date.current, unlimited: "current" }); + const queueIds = JSON.stringify( props.selectedQueue) || {}; + let {tickets} = useTickets({ date: date.current, unlimited: "current", queueIds }); - const [chartData, setChartData] = useState([ + const modelChar = [ { time: "08:00", amount: 0 }, { time: "09:00", amount: 0 }, { time: "10:00", amount: 0 }, @@ -35,11 +36,12 @@ const Chart = (props) => { { time: "17:00", amount: 0 }, { time: "18:00", amount: 0 }, { time: "19:00", amount: 0 }, - ]); + ] + const [chartData, setChartData] = useState(modelChar); useEffect(() => { setChartData(prevState => { - let aux = [...prevState]; + let aux = modelChar; aux.forEach(a => { tickets.forEach(ticket => { format(startOfHour(parseISO(ticket.createdAt)), "HH:mm") === a.time && a.amount++; }); diff --git a/frontend/src/pages/Dashboard/index.js b/frontend/src/pages/Dashboard/index.js index ca6fe4e..3178c19 100644 --- a/frontend/src/pages/Dashboard/index.js +++ b/frontend/src/pages/Dashboard/index.js @@ -1,4 +1,4 @@ -import React, { useContext, useReducer, useEffect, useState } from "react" +import React, { useContext, useReducer, useEffect, useState, useCallback } from "react" import { addHours, addMinutes, addSeconds, intervalToDuration } from "date-fns" @@ -11,6 +11,7 @@ import Tooltip from "@mui/material/Tooltip" import Zoom from "@mui/material/Zoom" import IconButton from "@mui/material/IconButton" import Info from "@material-ui/icons/Info" +import SelectField from "../../components/Report/SelectField" import { AuthContext } from "../../context/Auth/AuthContext" // import { i18n } from "../../translate/i18n"; @@ -254,12 +255,15 @@ const reducer = (state, action) => { } const Dashboard = () => { + const { user } = useContext(AuthContext) const classes = useStyles() const [usersOnlineInfo, dispatch] = useReducer(reducer, []) const [ticketStatusChange, setStatus] = useState() const [ticketsStatus, setTicktsStatus] = useState({ open: 0, openAll: 0, pending: 0, closed: 0 }) const [ticketStatusChatEnd, setTicketStatusChatEnd] = useState([]) - const { user } = useContext(AuthContext) + + const userQueueIds = user.queues.map((q) => q.id); + const [selectedQueue, setSelectedQueue] = useState(userQueueIds || []); useEffect(() => { dispatch({ type: "RESET" }) @@ -286,14 +290,14 @@ const Dashboard = () => { let dateToday = `${date[2]}-${date[1]}-${date[0]}` const { data } = await api.get("/reports/user/services", { - params: { userId: null, startDate: dateToday, endDate: dateToday }, + params: { userId: null, startDate: dateToday, endDate: dateToday, userQueues: selectedQueue }, }) dispatch({ type: "RESET" }) dispatch({ type: "LOAD_QUERY", payload: data.usersProfile }) const { data: ticketStatusChatEndData } = await api.get("/reports/count/statusChatEnd", { - params: { startDate: dateToday, endDate: dateToday }, + params: { startDate: dateToday, endDate: dateToday, userQueues: selectedQueue }, }) setTicketStatusChatEnd(ticketStatusChatEndData.reportStatusChatEnd) @@ -306,7 +310,7 @@ const Dashboard = () => { fetchQueries() }, 500) return () => clearTimeout(delayDebounceFn) - }, []) + }, [selectedQueue]) useEffect(() => { @@ -380,6 +384,18 @@ const Dashboard = () => { socket.disconnect() } }, []) + + const handleSelectedQueue = useCallback((queueSelected) => { + if(queueSelected !== 'All'){ + const queueIndex = user?.queues?.findIndex((q) => q.id === parseInt(queueSelected)); + const queueIds = [] + queueIds.push(user?.queues[queueIndex]?.id); + setSelectedQueue(queueIds); + }else{ + const queueIds = user?.queues?.map((queue) => queue.id); + setSelectedQueue(queueIds); + } + },[user, setSelectedQueue]) useEffect(() => { if (ticketStatusChange === "") return @@ -390,17 +406,17 @@ const Dashboard = () => { let dateToday = `${date[2]}-${date[1]}-${date[0]}` const _open = await api.get("/tickets/count", { - params: { status: "open", date: dateToday }, + params: { status: "open", date: dateToday, queueIds: selectedQueue }, }) const _closed = await api.get("/tickets/count", { - params: { status: "closed", date: dateToday }, + params: { status: "closed", date: dateToday, queueIds: selectedQueue }, }) const _pending = await api.get("/tickets/count", { - params: { status: "pending" }, + params: { status: "pending", queueIds: selectedQueue }, }) const _openAll = await api.get("/tickets/count", { - params: { status: "open" }, + params: { status: "open", queueIds: selectedQueue }, }) setTicktsStatus({ open: _open.data.count, @@ -419,7 +435,7 @@ const Dashboard = () => { fetchQueries() }, 500) return () => clearTimeout(delayDebounceFn) - }, [ticketStatusChange]) + }, [ticketStatusChange, selectedQueue]) return ( { + + { + return { 'value': obj.id, 'label': obj.name } + })} /> + { - + diff --git a/frontend/src/pages/Settings/index.js b/frontend/src/pages/Settings/index.js index 0fe21e2..097d6d4 100644 --- a/frontend/src/pages/Settings/index.js +++ b/frontend/src/pages/Settings/index.js @@ -446,6 +446,88 @@ const Settings = () => {
+
+ + + + Noficar quando entrar novo ticket na fila + + + + + +
+
+ + + + Bloquear mídias de Audio e Video + + + + + +
+ +
+ + + + Mostrar tempo de espera dos tickets aguardando + + + + + +
)} /> From fea60cf80cf1587edd3c3006f8feb9f8431e641b Mon Sep 17 00:00:00 2001 From: gustavo-gsp Date: Tue, 30 Apr 2024 16:20:48 -0300 Subject: [PATCH 3/3] fix: correct code errors Details: - Addressed various code errors and made necessary fixes. feat: add column 'type' to the Reports Mtable to differentiate remote tickets from regular ones Details: - Added a new column named 'type' to the Reports Mtable to differentiate between remote tickets and regular ones. --- .../CountStatusChatEndService.ts | 25 +- .../TicketServices/ShowTicketReport.ts | 1 + .../UserServices/ListUserParamiterService.ts | 8 +- .../UserServices/UpdateUserService.ts | 2 +- frontend/src/components/TicketsList/index.js | 11 +- .../src/components/TicketsManager/index.js | 304 ++++++++---------- .../TicketsProvider/TicketsProvider.js | 17 + frontend/src/pages/Dashboard/index.js | 2 +- frontend/src/pages/Report/index.js | 10 +- frontend/src/pages/Tickets/index.js | 5 +- 10 files changed, 209 insertions(+), 176 deletions(-) create mode 100644 frontend/src/context/TicketsProvider/TicketsProvider.js diff --git a/backend/src/services/StatusChatEndService/CountStatusChatEndService.ts b/backend/src/services/StatusChatEndService/CountStatusChatEndService.ts index 3a7cf0c..d6b28db 100644 --- a/backend/src/services/StatusChatEndService/CountStatusChatEndService.ts +++ b/backend/src/services/StatusChatEndService/CountStatusChatEndService.ts @@ -14,13 +14,26 @@ const CountStatusChatEndService = async ( endDate: string, queueIds?: number[] ) => { - const countStatusChatEnd: any = await sequelize.query( + let countStatusChatEnd: any + + if(queueIds && queueIds.length > 0){ + countStatusChatEnd = await sequelize.query( + `select t.id, s.name, count(t.id) as count from Tickets t join StatusChatEnds s on + t.statusChatEndId = s.id and DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + AND t.queueId IN (${queueIds}) + group by s.id;`, + { type: QueryTypes.SELECT } + ); + } + else{ + countStatusChatEnd = await sequelize.query( `select t.id, s.name, count(t.id) as count from Tickets t join StatusChatEnds s on - t.statusChatEndId = s.id and DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' - AND t.queueId IN (${queueIds}) - group by s.id;`, - { type: QueryTypes.SELECT } - ); + t.statusChatEndId = s.id and DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + group by s.id;`, + { type: QueryTypes.SELECT } + ); + } + return countStatusChatEnd; }; diff --git a/backend/src/services/TicketServices/ShowTicketReport.ts b/backend/src/services/TicketServices/ShowTicketReport.ts index 1c4b1ea..a3aa41e 100644 --- a/backend/src/services/TicketServices/ShowTicketReport.ts +++ b/backend/src/services/TicketServices/ShowTicketReport.ts @@ -98,6 +98,7 @@ const ShowTicketReport = async ({ "id", "status", "statusChatEnd", + "isRemote", [ Sequelize.fn( "DATE_FORMAT", diff --git a/backend/src/services/UserServices/ListUserParamiterService.ts b/backend/src/services/UserServices/ListUserParamiterService.ts index 2198936..de476cd 100644 --- a/backend/src/services/UserServices/ListUserParamiterService.ts +++ b/backend/src/services/UserServices/ListUserParamiterService.ts @@ -59,12 +59,18 @@ const ListUser = async ({ where_clause = { id: { [Op.in]: userIds } }; - } else if (profiles) { + } + else if (profiles && userIdInQueues.length > 0) { where_clause = { profile: { [Op.in]: profiles }, id: {[Op.in]: userIdInQueues} }; } + else if (profiles) { + where_clause = { + profile: { [Op.in]: profiles }, + }; + } const users = await User.findAll({ where: where_clause, diff --git a/backend/src/services/UserServices/UpdateUserService.ts b/backend/src/services/UserServices/UpdateUserService.ts index a4f88f4..2f721bb 100644 --- a/backend/src/services/UserServices/UpdateUserService.ts +++ b/backend/src/services/UserServices/UpdateUserService.ts @@ -12,7 +12,7 @@ interface UserData { positionId?: string; profile?: string; queueIds?: number[]; - transferToOtherQueues: boolean; + transferToOtherQueues?: boolean; } interface Request { diff --git a/frontend/src/components/TicketsList/index.js b/frontend/src/components/TicketsList/index.js index 9754d79..cbc0689 100644 --- a/frontend/src/components/TicketsList/index.js +++ b/frontend/src/components/TicketsList/index.js @@ -15,6 +15,7 @@ import { i18n } from "../../translate/i18n" import { AuthContext } from "../../context/Auth/AuthContext" import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket" +import { ticketsContext } from "../../context/TicketsProvider/TicketsProvider" const useStyles = makeStyles(theme => ({ ticketsListWrapper: { @@ -184,6 +185,8 @@ const TicketsList = (props) => { const { user } = useContext(AuthContext) const { searchTicket } = useContext(SearchTicketContext) + const { setTickets } = useContext(ticketsContext) + useEffect(() => { @@ -197,7 +200,7 @@ const TicketsList = (props) => { searchParam, searchParamContent, status, - showAll, + showAll, queueIds: JSON.stringify(selectedQueueIds), tab, unlimited: status === 'open' ? "all" : "false" @@ -311,6 +314,12 @@ const TicketsList = (props) => { if (typeof updateCount === "function") { updateCount(ticketsList.length) } + if (ticketsList && status === "pending"){ + setTickets(ticketsList) + } + // else{ + // setTickets([]) + // } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ticketsList]) diff --git a/frontend/src/components/TicketsManager/index.js b/frontend/src/components/TicketsManager/index.js index 21e8a87..b3e5cf9 100644 --- a/frontend/src/components/TicketsManager/index.js +++ b/frontend/src/components/TicketsManager/index.js @@ -1,42 +1,45 @@ -import React, { useContext, useEffect, useRef, useState } from "react"; +import React, { useContext, useEffect, useRef, useState } from "react" -import { makeStyles } from "@material-ui/core/styles"; -import { IconButton } from "@mui/material"; -import Paper from "@material-ui/core/Paper"; -import InputBase from "@material-ui/core/InputBase"; -import Tabs from "@material-ui/core/Tabs"; -import Tab from "@material-ui/core/Tab"; -import Badge from "@material-ui/core/Badge"; +import { makeStyles } from "@material-ui/core/styles" +import { IconButton } from "@mui/material" +import Paper from "@material-ui/core/Paper" +import InputBase from "@material-ui/core/InputBase" +import Tabs from "@material-ui/core/Tabs" +import Tab from "@material-ui/core/Tab" +import Badge from "@material-ui/core/Badge" -import Tooltip from "@material-ui/core/Tooltip"; +import Tooltip from "@material-ui/core/Tooltip" -import SearchIcon from "@material-ui/icons/Search"; -import MoveToInboxIcon from "@material-ui/icons/MoveToInbox"; -import CheckBoxIcon from "@material-ui/icons/CheckBox"; -import MenuIcon from "@material-ui/icons/Menu"; -import FindInPageIcon from '@material-ui/icons/FindInPage'; +import SearchIcon from "@material-ui/icons/Search" +import MoveToInboxIcon from "@material-ui/icons/MoveToInbox" +import CheckBoxIcon from "@material-ui/icons/CheckBox" +import MenuIcon from "@material-ui/icons/Menu" +import FindInPageIcon from '@material-ui/icons/FindInPage' -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Switch from "@material-ui/core/Switch"; +import FormControlLabel from "@material-ui/core/FormControlLabel" +import Switch from "@material-ui/core/Switch" import openSocket from "socket.io-client" -import NewTicketModal from "../NewTicketModal"; -import TicketsList from "../TicketsList"; -import TabPanel from "../TabPanel"; +import NewTicketModal from "../NewTicketModal" +import TicketsList from "../TicketsList" +import TabPanel from "../TabPanel" -import { i18n } from "../../translate/i18n"; -import { AuthContext } from "../../context/Auth/AuthContext"; -import { Can } from "../Can"; -import TicketsQueueSelect from "../TicketsQueueSelect"; -import { Button } from "@material-ui/core"; +import { i18n } from "../../translate/i18n" +import { AuthContext } from "../../context/Auth/AuthContext" +import { Can } from "../Can" +import TicketsQueueSelect from "../TicketsQueueSelect" +import { Button } from "@material-ui/core" -import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption"; +import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption" -import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket"; +import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket" import useTickets from "../../hooks/useTickets" -import api from "../../services/api"; -import toastError from "../../errors/toastError"; +import api from "../../services/api" +import toastError from "../../errors/toastError" + +import { ticketsContext } from "../../context/TicketsProvider/TicketsProvider" + const useStyles = makeStyles((theme) => ({ ticketsWrapper: { @@ -128,147 +131,114 @@ const useStyles = makeStyles((theme) => ({ hide: { display: "none !important", }, -})); +})) const DEFAULT_SEARCH_PARAM = { searchParam: "", searchParamContent: "" } const TicketsManager = () => { - const { tabOption, setTabOption } = useContext(TabTicketContext); + const { tabOption, setTabOption } = useContext(TabTicketContext) const { setSearchTicket } = useContext(SearchTicketContext) - const classes = useStyles(); + const classes = useStyles() - const [searchParam, setSearchParam] = useState(DEFAULT_SEARCH_PARAM); - const [tab, setTab] = useState("open"); - const [tabOpen, setTabOpen] = useState("open"); - const [newTicketModalOpen, setNewTicketModalOpen] = useState(false); - const [showAllTickets, setShowAllTickets] = useState(false); - const { user } = useContext(AuthContext); + const [searchParam, setSearchParam] = useState(DEFAULT_SEARCH_PARAM) + const [tab, setTab] = useState("open") + const [tabOpen, setTabOpen] = useState("open") + const [newTicketModalOpen, setNewTicketModalOpen] = useState(false) + const [showAllTickets, setShowAllTickets] = useState(false) + const { user, setting, getSettingValue } = useContext(AuthContext) - const [openCount, setOpenCount] = useState(0); - const [pendingCount, setPendingCount] = useState(0); + const [openCount, setOpenCount] = useState(0) + const [pendingCount, setPendingCount] = useState(0) - const userQueueIds = user.queues.map((q) => q.id); - const [selectedQueueIds, setSelectedQueueIds] = useState(userQueueIds || []); + const userQueueIds = user.queues.map((q) => q.id) + const [selectedQueueIds, setSelectedQueueIds] = useState(userQueueIds || []) const [showContentSearch, setShowContentSearch] = useState(false) - const searchInputRef = useRef(); - const searchContentInputRef = useRef(); - const [inputSearch, setInputSearch] = useState(''); + const searchInputRef = useRef() + const searchContentInputRef = useRef() + const [inputSearch, setInputSearch] = useState('') const [inputContentSearch, setInputContentSearch] = useState("") const [openTooltipSearch, setOpenTooltipSearch] = useState(false) - const [waitingTime, setWaitingTime] = useState('00:00'); - const [tickets, setTickets] = useState([]); + const [waitingTime, setWaitingTime] = useState('00:00') + // const [tickets, setTickets] = useState([]); const [settings, setSettings] = useState([]) - let searchTimeout; - let searchContentTimeout; + let searchTimeout + let searchContentTimeout + + const { tickets, } = useContext(ticketsContext) useEffect(() => { - if (user.profile.toUpperCase() === "ADMIN" || - user.profile.toUpperCase() === "SUPERVISOR" || - user.profile.toUpperCase() === "MASTER") { - setShowAllTickets(true); + setSettings(setting) + }, [setting]) + + useEffect(() => { + if (user.profile.toUpperCase() === "ADMIN" || + user.profile.toUpperCase() === "SUPERVISOR" || + user.profile.toUpperCase() === "MASTER") { + setShowAllTickets(true) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, []) useEffect(() => { if (tab === "search") { - searchInputRef.current.focus(); + searchInputRef.current.focus() } setTabOption(tab) - }, [tab, setTabOption]); + }, [tab, setTabOption]) useEffect(() => { - const fetchSession = async () => { - try { - const { data } = await api.get('/settings') - setSettings(data.settings) - } catch (err) { - toastError(err) - } - } - fetchSession() - }, []) - const getSettingValue = (key) => { - const { value } = settings.find((s) => s.key === key) - return value - } + if (settings?.length > 0 && getSettingValue('waitingTimeTickets') !== 'enabled') return - const fetchTickets = async () =>{ - try { - const { data } = await api.get("/tickets", { - params: { - status: 'pending', - queueIds: JSON.stringify(selectedQueueIds) - }, - }); - setTickets(data.tickets); - } catch (err) { - toastError(err); - } - - } - useEffect(() => { - if(settings?.length > 0 && getSettingValue('waitingTimeTickets') === 'enabled') { - fetchTickets(); - const intervalId = setInterval(fetchTickets, 55000); - const socket = openSocket(process.env.REACT_APP_BACKEND_URL) - - socket.on("ticketStatus", (data) => { - if (data.action === "update") { - fetchTickets(); - } - }) - return () => { - socket.disconnect() - clearInterval(intervalId) - } - } - }, [selectedQueueIds, settings]); - - useEffect(() => { - const calculateAverageTime = () => { - if(tickets.length > 0){ - const now = new Date(); + const calculateAverageTime = () => { + if (tickets.length > 0) { + const now = new Date() const differenceTime = tickets?.map(ticket => { - const updatedAt = new Date(ticket.updatedAt); - const difference = now - updatedAt; - return difference; - }); - const sumDifferences = differenceTime.reduce((total, difference) => total + difference, 0); - const averageTimeMilliseconds = sumDifferences / tickets?.length; - let hours = Math.floor(averageTimeMilliseconds / 3600000); - const minutes = Math.floor((averageTimeMilliseconds % 3600000) / 60000); - - let days = hours >= 24 ? parseInt(hours/24) : ''; - - if(days != '') hours = hours - (24*days); - - const averageTimeFormated = `${days != '' ? `${days}d ` : days}${hours.toString().padStart(2, '0')}h${minutes.toString().padStart(2, '0')}`; - - return averageTimeFormated; - }else return '00:00'; - } - - setWaitingTime(calculateAverageTime()); - },[tickets]); - + const createdAt = new Date(ticket.createdAt) + const difference = now - createdAt + return difference + }) + const sumDifferences = differenceTime.reduce((total, difference) => total + difference, 0) + const averageTimeMilliseconds = sumDifferences / tickets?.length + let hours = Math.floor(averageTimeMilliseconds / 3600000) + const minutes = Math.floor((averageTimeMilliseconds % 3600000) / 60000) + + let days = hours >= 24 ? parseInt(hours / 24) : '' + + if (days != '') hours = hours - (24 * days) + + const averageTimeFormated = `${days != '' ? `${days}d ` : days}${hours.toString().padStart(2, '0')}h${minutes.toString().padStart(2, '0')}` + + return averageTimeFormated + } else return '00:00' + } + + setWaitingTime(calculateAverageTime()) + + const intervalId = setInterval(() => { + setWaitingTime(calculateAverageTime()) + }, 10000) + + return () => clearInterval(intervalId) + + }, [tickets]) + useEffect(() => { // clearTimeout(searchContentTimeout); // setSearchParam(prev => ({ ...prev, searchParamContent: "" })) - if (!inputContentSearch) return + if (!inputContentSearch) return if (!searchContentTimeout) return @@ -278,12 +248,12 @@ const TicketsManager = () => { // }, 500); - clearTimeout(searchContentTimeout); + clearTimeout(searchContentTimeout) - setSearchParam(prev => ({ ...prev, searchParamContent: "" })) + setSearchParam(prev => ({ ...prev, searchParamContent: "" })) - }, [inputContentSearch, searchContentTimeout]); + }, [inputContentSearch, searchContentTimeout]) useEffect(() => { @@ -291,16 +261,16 @@ const TicketsManager = () => { if (tabOption === 'open') { setTabOption('') - setSearchParam(DEFAULT_SEARCH_PARAM); - setInputSearch(''); + setSearchParam(DEFAULT_SEARCH_PARAM) + setInputSearch('') setInputContentSearch('') - setTab("open"); - return; + setTab("open") + return } }, [tabOption, setTabOption]) - + const removeExtraSpace = (str) => { str = str.replace(/^\s+/g, '') @@ -315,14 +285,14 @@ const TicketsManager = () => { setSearchTicket(searchParam.searchParam) - clearTimeout(searchTimeout); + clearTimeout(searchTimeout) if (searchedTerm === "") { setSearchParam(prev => ({ ...prev, searchParam: searchedTerm })) setInputSearch(searchedTerm) setShowContentSearch(false) - setTab("open"); - return; + setTab("open") + return } if (searchedTerm.length < 4) { @@ -333,22 +303,22 @@ const TicketsManager = () => { searchTimeout = setTimeout(() => { - setSearchParam(prev => ({ ...prev, searchParam: searchedTerm })); + setSearchParam(prev => ({ ...prev, searchParam: searchedTerm })) - }, 500); - }; + }, 500) + } const handleContentSearch = e => { let searchedContentText = removeExtraSpace(e.target.value.toLowerCase()) - setInputContentSearch(searchedContentText) - + setInputContentSearch(searchedContentText) + searchContentTimeout = setTimeout(() => { - setSearchParam(prev => ({ ...prev, searchParamContent: searchedContentText })); + setSearchParam(prev => ({ ...prev, searchParamContent: searchedContentText })) - }, 500); + }, 500) } @@ -366,18 +336,18 @@ const TicketsManager = () => { } const handleChangeTab = (e, newValue) => { - setTab(newValue); - }; + setTab(newValue) + } const handleChangeTabOpen = (e, newValue) => { - setTabOpen(newValue); - }; + setTabOpen(newValue) + } const applyPanelStyle = (status) => { if (tabOpen !== status) { - return { width: 0, height: 0 }; + return { width: 0, height: 0 } } - }; + } return ( @@ -534,14 +504,22 @@ const TicketsManager = () => { value={"pending"} />{ (settings?.length > 0 && getSettingValue('waitingTimeTickets') === 'enabled') && - - - - + + + + {/* */} + + + + } @@ -580,7 +558,7 @@ const TicketsManager = () => { - ); -}; + ) +} -export default TicketsManager; \ No newline at end of file +export default TicketsManager \ No newline at end of file diff --git a/frontend/src/context/TicketsProvider/TicketsProvider.js b/frontend/src/context/TicketsProvider/TicketsProvider.js new file mode 100644 index 0000000..11b6ec9 --- /dev/null +++ b/frontend/src/context/TicketsProvider/TicketsProvider.js @@ -0,0 +1,17 @@ +import React, { useState, createContext } from "react" + +const ticketsContext = createContext() + + +const TicketsProvider = ({ children }) => { + + const [tickets, setTickets] = useState(0) + + return ( + + {children} + + ) +} + +export { ticketsContext, TicketsProvider } \ No newline at end of file diff --git a/frontend/src/pages/Dashboard/index.js b/frontend/src/pages/Dashboard/index.js index 3178c19..3fa4e2d 100644 --- a/frontend/src/pages/Dashboard/index.js +++ b/frontend/src/pages/Dashboard/index.js @@ -262,7 +262,7 @@ const Dashboard = () => { const [ticketsStatus, setTicktsStatus] = useState({ open: 0, openAll: 0, pending: 0, closed: 0 }) const [ticketStatusChatEnd, setTicketStatusChatEnd] = useState([]) - const userQueueIds = user.queues.map((q) => q.id); + const userQueueIds = user.queues?.map((q) => q.id); const [selectedQueue, setSelectedQueue] = useState(userQueueIds || []); useEffect(() => { diff --git a/frontend/src/pages/Report/index.js b/frontend/src/pages/Report/index.js index 5e7d6fe..8ddc8cd 100644 --- a/frontend/src/pages/Report/index.js +++ b/frontend/src/pages/Report/index.js @@ -224,6 +224,7 @@ Item.propTypes = { let columnsData = [ + { title: `Tipo`, field: 'isRemote' }, { title: `${i18n.t("reports.listColumns.column1_1")}`, field: 'whatsapp.name' }, { title: `${i18n.t("reports.listColumns.column1_2")}`, field: 'user.name' }, { title: `${i18n.t("reports.listColumns.column0_4")}`, field: 'contact.number' }, @@ -241,6 +242,8 @@ let columnsData = [ ] let columnsDataSuper = [ + { title: `Tipo`, field: 'isRemote' }, + { title: `${i18n.t("reports.listColumns.column1_0")}`, field: 'isRemote' }, { title: `${i18n.t("reports.listColumns.column1_1")}`, field: 'whatsapp.name' }, { title: `${i18n.t("reports.listColumns.column1_2")}`, field: 'user.name' }, { title: `${i18n.t("reports.listColumns.column0_3")}`, field: 'contact.name' }, @@ -375,11 +378,14 @@ const Report = () => { filterQueuesTickets = ticketsQueue.filter(ticket => ticket?.queue?.name === userQueues[0]?.name) } data.tickets = filterQueuesTickets - const tickets = data.tickets.map(ticket => ({ + const tickets = data.tickets.map(ticket => { + ticket.isRemote = ticket.isRemote ? 'Remoto' : 'Comum'; + return ({ ...ticket, messagesToFilter: ticket.messages.map(message => message.body).join(' '), link: `${process.env.REACT_APP_FRONTEND_URL}/tickets/${ticket.id}` - })) + }) + }) dispatchQ({ type: "LOAD_QUERY", payload: tickets }) setHasMore(data.hasMore) setTotalCountTickets(data.count) diff --git a/frontend/src/pages/Tickets/index.js b/frontend/src/pages/Tickets/index.js index 21a254d..0900471 100644 --- a/frontend/src/pages/Tickets/index.js +++ b/frontend/src/pages/Tickets/index.js @@ -11,6 +11,7 @@ import { i18n } from "../../translate/i18n"; import Hidden from "@material-ui/core/Hidden"; import { SearchTicketProvider } from "../../context/SearchTicket/SearchTicket"; +import { TicketsProvider } from "../../context/TicketsProvider/TicketsProvider" const useStyles = makeStyles((theme) => ({ chatContainer: { @@ -82,7 +83,9 @@ const Chat = () => { } > - + + +