From 860d462d376ded45c5cc5fbf5a62cde7b0289243 Mon Sep 17 00:00:00 2001 From: gustavo-gsp Date: Thu, 18 Apr 2024 10:18:17 -0300 Subject: [PATCH] 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 = () => { <>