From 5a0ed8368d5bda387d9ca8cb0a933a9bb4d91d45 Mon Sep 17 00:00:00 2001 From: adriano Date: Tue, 23 Apr 2024 19:51:11 -0300 Subject: [PATCH] Update to control message sending cadence by agents to prevent WhatsApp banning --- backend/src/controllers/ReportController.ts | 16 +- backend/src/controllers/SettingController.ts | 7 +- backend/src/controllers/TicketController.ts | 99 ++++-- ...115601-add-column-remoteDone-to-tickets.ts | 15 + ...240423193637-add-column-obj-to-settings.ts | 14 + ...add-enable-remote-tickets-send-controll.ts | 22 ++ backend/src/helpers/AutoRemoteTickets.ts | 78 +++++ backend/src/helpers/RedisClient.ts | 22 +- backend/src/helpers/controllByNumber.ts | 41 +++ backend/src/models/Setting.ts | 3 + backend/src/models/Ticket.ts | 6 +- backend/src/routes/settingRoutes.ts | 2 +- backend/src/server.ts | 29 +- .../MessageServices/CreateMessageService.ts | 31 +- .../ReportByNumberQueueService.ts | 317 ++++++++++++------ .../SettingServices/UpdateSettingService.ts | 15 +- .../TicketServices/ListTicketsService.ts | 9 +- .../TicketServices/UpdateTicketService.ts | 19 +- .../WbotServices/wbotMessageListener.ts | 12 +- frontend/src/components/MessageInput/index.js | 21 +- frontend/src/components/MessagesList/index.js | 8 + frontend/src/components/Ticket/index.js | 21 +- .../src/components/TicketListItem/index.js | 258 ++++++++------ frontend/src/components/TicketsList/index.js | 43 ++- .../CountTicketMsgProvider.js | 17 + frontend/src/hooks/useTickets/index.js | 52 +-- frontend/src/pages/Report/index.js | 87 ++++- frontend/src/pages/Settings/index.js | 128 ++++++- 28 files changed, 1061 insertions(+), 331 deletions(-) create mode 100644 backend/src/database/migrations/20240420115601-add-column-remoteDone-to-tickets.ts create mode 100644 backend/src/database/migrations/20240423193637-add-column-obj-to-settings.ts create mode 100644 backend/src/database/seeds/20240423173230-add-enable-remote-tickets-send-controll.ts create mode 100644 backend/src/helpers/AutoRemoteTickets.ts create mode 100644 backend/src/helpers/controllByNumber.ts create mode 100644 frontend/src/context/CountTicketMsgProvider/CountTicketMsgProvider.js diff --git a/backend/src/controllers/ReportController.ts b/backend/src/controllers/ReportController.ts index d7d70b3..13ef50a 100644 --- a/backend/src/controllers/ReportController.ts +++ b/backend/src/controllers/ReportController.ts @@ -30,6 +30,7 @@ type IndexQuery = { queueId: string; pageNumber: string; userQueues: []; + isRemote: string; }; type ReportOnQueue = { @@ -262,7 +263,7 @@ export const reportMessagesUserByDateStartDateEnd = async ( data_query_messages[i].fromMe = "Cliente"; } - data_query_messages[i].id = i + 1; + data_query_messages[i].id = i + 1; } return res.status(200).json(data_query_messages); @@ -302,15 +303,19 @@ export const reportService = async ( throw new AppError("ERR_NO_PERMISSION", 403); } - const { startDate, endDate, queueId } = req.query as IndexQuery; + const { startDate, endDate, queueId, isRemote } = req.query as IndexQuery; console.log( `startDate: ${startDate} | endDate: ${endDate} | queueId: ${queueId}` ); + console.log("IS REMOTE: ", isRemote); + console.log("isRemote: ", isRemote && isRemote == "true" ? true : false); + const reportService = await ReportByNumberQueueService({ startDate, - endDate + endDate, + isRemote: isRemote && isRemote == "true" ? true : false }); return res.status(200).json({ reportService }); @@ -328,12 +333,13 @@ export const reportServiceByQueue = async ( throw new AppError("ERR_NO_PERMISSION", 403); } - const { startDate, endDate, queueId } = req.query as IndexQuery; + const { startDate, endDate, queueId, isRemote } = req.query as IndexQuery; const reportService = await ReportByNumberQueueService({ startDate, endDate, - queue: true + queue: true, + isRemote: isRemote && isRemote == "true" ? true : false }); return res.status(200).json({ reportService }); diff --git a/backend/src/controllers/SettingController.ts b/backend/src/controllers/SettingController.ts index 3964a75..6451036 100644 --- a/backend/src/controllers/SettingController.ts +++ b/backend/src/controllers/SettingController.ts @@ -123,11 +123,12 @@ export const update = async ( throw new AppError("ERR_NO_PERMISSION", 403); } const { settingKey: key } = req.params; - const { value } = req.body; + const { value, obj } = req.body; const setting = await UpdateSettingService({ key, - value + value, + obj }); if (key && key == "whatsaAppCloudApi") { @@ -165,7 +166,7 @@ export const update = async ( } loadSettings(); - + const io = getIO(); io.emit("settings", { action: "update", diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index 272bc10..367275c 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -75,10 +75,11 @@ import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl"; import CreateContactService from "../services/ContactServices/CreateContactService"; import { botSendMessage } from "../services/WbotServices/wbotMessageListener"; import WhatsappQueue from "../models/WhatsappQueue"; -import { get } from "../helpers/RedisClient"; +import { del, get, set } from "../helpers/RedisClient"; import CountStatusChatEndService from "../services/StatusChatEndService/CountStatusChatEndService"; import Queue from "../models/Queue"; import StatusChatEnd from "../models/StatusChatEnd"; +import controllByNumber from "../helpers/controllByNumber"; export const index = async (req: Request, res: Response): Promise => { const { @@ -101,20 +102,23 @@ export const index = async (req: Request, res: Response): Promise => { queueIds = JSON.parse(queueIdsStringified); } - const { tickets, count, hasMore } = await ListTicketsService({ - searchParam, - pageNumber, - status, - date, - showAll, - userId, - queueIds, - withUnreadMessages, - unlimited, - searchParamContent - }); + const { tickets, count, hasMore, remoteTicketsControll } = + await ListTicketsService({ + searchParam, + pageNumber, + status, + date, + showAll, + userId, + queueIds, + withUnreadMessages, + unlimited, + searchParamContent + }); - return res.status(200).json({ tickets, count, hasMore }); + return res + .status(200) + .json({ tickets, count, hasMore, remoteTicketsControll }); }; export const remoteTicketCreation = async ( @@ -232,7 +236,8 @@ export const remoteTicketCreation = async ( const botInfo = await BotIsOnQueue("botqueue"); - let ticket = await Ticket.findOne({ + // ticket from queueChoice or bot + let ticket: any = await Ticket.findOne({ where: { [Op.or]: [ { contactId, status: "queueChoice" }, @@ -258,27 +263,69 @@ export const remoteTicketCreation = async ( } } - if (!ticket) { - ticket = await FindOrCreateTicketService( - contact, - whatsappId, - 0, - undefined, - queueId, - true + ticket = await Ticket.findOne({ + where: { + [Op.or]: [ + { contactId, status: "pending" }, + { contactId, status: "open" } + ] + } + }); + + if (ticket) { + console.log( + `THE CAMPAIGN TICKET WAS NOT CREATED BECAUSE THE TICKET IS PENDING OR OPEN` ); - botSendMessage(ticket, `${msg}`); + + return res.status(422).json({ + msg: `The campaign ticket was not created because the number ${contact_to} already has a ticket open or pending` + }); } + ticket = await FindOrCreateTicketService( + contact, + whatsappId, + 0, + undefined, + queueId, + true + ); + + // botSendMessage(ticket, `${msg}`); + + await ticket.update({ + lastMessage: msg + }); + + await set( + `remote:ticketId:${ticket.id}`, + JSON.stringify({ + id: ticket.id, + createdAt: ticket.createdAt, + updatedAt: ticket.updatedAt, + whatsappId: ticket.whatsappId + }) + ); + const io = getIO(); io.to(ticket.status).emit("ticket", { action: "update", ticket }); + const obj = await controllByNumber(); + + if (obj?.tickets) { + io.emit("remoteTickesControll", { + action: "update", + tickets: obj.ticketIds + }); + } + console.log( `REMOTE TICKET CREATION FROM ENDPOINT | STATUS: 200 | MSG: success` ); + return res.status(200).json({ msg: "success" }); } @@ -465,7 +512,7 @@ export const update = async ( } const schedulingNotifyCreate = await CreateSchedulingNotifyService({ - ticketId: scheduleData.ticketId, + ticketId: scheduleData.ticketId, statusChatEndId: `${statusChatEnd.id}`, schedulingDate: scheduleData.schedulingDate, schedulingTime: scheduleData.schedulingTime, @@ -648,5 +695,7 @@ export const remove = async ( ticketId: +ticketId }); + await del(`remote:ticketId:${ticketId}`); + return res.status(200).json({ message: "ticket deleted" }); }; diff --git a/backend/src/database/migrations/20240420115601-add-column-remoteDone-to-tickets.ts b/backend/src/database/migrations/20240420115601-add-column-remoteDone-to-tickets.ts new file mode 100644 index 0000000..ad14fff --- /dev/null +++ b/backend/src/database/migrations/20240420115601-add-column-remoteDone-to-tickets.ts @@ -0,0 +1,15 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Tickets", "remoteDone", { + type: DataTypes.BOOLEAN, + allowNull: true, + defaultValue: false + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Tickets", "remoteDone"); + } +}; diff --git a/backend/src/database/migrations/20240423193637-add-column-obj-to-settings.ts b/backend/src/database/migrations/20240423193637-add-column-obj-to-settings.ts new file mode 100644 index 0000000..b9734cf --- /dev/null +++ b/backend/src/database/migrations/20240423193637-add-column-obj-to-settings.ts @@ -0,0 +1,14 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Settings", "obj", { + type: DataTypes.STRING, + allowNull: true + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Settings", "obj"); + } +}; diff --git a/backend/src/database/seeds/20240423173230-add-enable-remote-tickets-send-controll.ts b/backend/src/database/seeds/20240423173230-add-enable-remote-tickets-send-controll.ts new file mode 100644 index 0000000..36d3a18 --- /dev/null +++ b/backend/src/database/seeds/20240423173230-add-enable-remote-tickets-send-controll.ts @@ -0,0 +1,22 @@ +import { QueryInterface } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.bulkInsert( + "Settings", + [ + { + key: "remoteTicketSendControll", + value: "true", + createdAt: new Date(), + updatedAt: new Date() + } + ], + {} + ); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.bulkDelete("Settings", {}); + } +}; diff --git a/backend/src/helpers/AutoRemoteTickets.ts b/backend/src/helpers/AutoRemoteTickets.ts new file mode 100644 index 0000000..cc90827 --- /dev/null +++ b/backend/src/helpers/AutoRemoteTickets.ts @@ -0,0 +1,78 @@ +import { intervalToDuration } from "date-fns"; +import { del, get, set } from "./RedisClient"; +import { getIO } from "../libs/socket"; +import controllByNumber from "./controllByNumber"; + +let timer: any; + +const AutoRemoteTickets = async () => { + try { + let obj: any = await controllByNumber(); + + if (!obj?.tickets) return; + + for (const ticket of obj.tickets) { + if (!ticket.includes("messageDateTime")) continue; + + let match = ticket.match(/"messageDateTime":("[^"]*")/); + let messageDateTime = match ? match[1] : null; + console.log("messageDateTime: ", messageDateTime); + + match = ticket.match(/"whatsappId":(\d+)/); + let whatsappId = match ? match[1] : null; + console.log("whatsappId: ", whatsappId); + + const whatsapp = await get({ + key: `whatsapp:${whatsappId}` + }); + + match = whatsapp.match(/"number":"(\d+)"/); + let number = match ? match[1] : null; + console.log("number: ", number); + + match = ticket.match(/"id":(\d+)/); + let ticketId = match ? match[1] : null; + console.log("ticketId: ", ticketId); + + number = JSON.parse(number); + ticketId = JSON.parse(ticketId); + + let timeDiff: any = intervalToDuration({ + start: new Date(JSON.parse(messageDateTime)), + end: new Date() + }); + + console.log("______timeDiff: ", timeDiff); + + if (timeDiff.seconds > 59) { + del(`remote:ticketId:${ticketId}`); + + obj = await controllByNumber(); + + const io = getIO(); + io.emit("remoteTickesControll", { + action: "update", + tickets: obj.ticketIds + }); + } + } + } catch (error) { + console.log("There was an error on auto remote tickets service: ", error); + } +}; + +const schedule = async () => { + try { + clearInterval(timer); + + await AutoRemoteTickets(); + } catch (error) { + console.log("error on schedule: ", error); + } finally { + timer = setInterval(schedule, 10000); + } +}; + +timer = setInterval(schedule, 10000); + +export default schedule; diff --git a/backend/src/helpers/RedisClient.ts b/backend/src/helpers/RedisClient.ts index 6fd90b2..fb99d6e 100644 --- a/backend/src/helpers/RedisClient.ts +++ b/backend/src/helpers/RedisClient.ts @@ -29,14 +29,24 @@ export async function getSimple(key: string) { export async function get({ key, value, parse }: getData) { if (key.includes("*")) { - const keys = await redis.keys(key); + const keys = await redis.keys(key); if (keys.length > 0) { - for (const key of keys) { - const val = await redis.get(key); - if (val.includes(value)) { - if (parse) return JSON.parse(val); - return val; + if (value) { + for (const key of keys) { + const val = await redis.get(key); + if (val.includes(value)) { + if (parse) return JSON.parse(val); + return val; + } } + } else { + let res: any[] = []; + for (const key of keys) { + const val = await redis.get(key); + if (parse) res.push(JSON.parse(val)); + res.push(val); + } + return res; } } return null; diff --git a/backend/src/helpers/controllByNumber.ts b/backend/src/helpers/controllByNumber.ts new file mode 100644 index 0000000..d74afa4 --- /dev/null +++ b/backend/src/helpers/controllByNumber.ts @@ -0,0 +1,41 @@ +import { get, set } from "./RedisClient"; + +async function controllByNumber() { + let tickets = await get({ key: "remote:ticketId*", parse: false }); + + if (!tickets) return { ticketIds: [], tickets: null }; + + let controll: any[] = []; + + for (const ticket of tickets) { + let match = ticket.match(/"whatsappId":(\d+)/); + let whatsappId = match ? match[1] : null; + + const whatsapp = await get({ + key: `whatsapp:${whatsappId}` + }); + + match = whatsapp.match(/"number":"(\d+)"/); + let number = match ? match[1] : null; + + match = ticket.match(/"id":(\d+)/); + let ticketId = match ? match[1] : null; + + number = JSON.parse(number); + ticketId = JSON.parse(ticketId); + + const index = controll.findIndex((c: any) => c.number == number); + + if (index == -1) { + controll.push({ ticketId, number }); + } + } + + const ticketIds = controll.map((c: any) => c.ticketId); + + set(`remote:controll`, JSON.stringify(ticketIds)); + + return { ticketIds, tickets }; +} + +export default controllByNumber; diff --git a/backend/src/models/Setting.ts b/backend/src/models/Setting.ts index b58e57a..dec71a9 100644 --- a/backend/src/models/Setting.ts +++ b/backend/src/models/Setting.ts @@ -16,6 +16,9 @@ class Setting extends Model { @Column value: string; + @Column + obj: string; + @CreatedAt createdAt: Date; diff --git a/backend/src/models/Ticket.ts b/backend/src/models/Ticket.ts index 71d12dd..3f8b218 100644 --- a/backend/src/models/Ticket.ts +++ b/backend/src/models/Ticket.ts @@ -21,7 +21,7 @@ import User from "./User"; import Whatsapp from "./Whatsapp"; import SchedulingNotify from "./SchedulingNotify"; -import StatusChatEnd from "./StatusChatEnd" +import StatusChatEnd from "./StatusChatEnd"; @Table class Ticket extends Model { @@ -47,6 +47,10 @@ class Ticket extends Model { @Column isRemote: boolean; + @Default(false) + @Column + remoteDone: boolean; + @ForeignKey(() => StatusChatEnd) @Column statusChatEndId: number; diff --git a/backend/src/routes/settingRoutes.ts b/backend/src/routes/settingRoutes.ts index dd9f6e7..280fb7c 100644 --- a/backend/src/routes/settingRoutes.ts +++ b/backend/src/routes/settingRoutes.ts @@ -9,7 +9,7 @@ settingRoutes.get("/settings", SettingController.index); settingRoutes.get("/settings/ticket/:number", SettingController.ticketSettings); -// routes.get("/settings/:settingKey", isAuth, SettingsController.show); +// settingRoutes.get("/settings/:settingKey", isAuth, SettingsController.show); settingRoutes.put( "/settings/ticket", diff --git a/backend/src/server.ts b/backend/src/server.ts index 4c9025b..9fba998 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -13,6 +13,7 @@ import { loadContactsCache } from "./helpers/ContactsCache"; import { loadSchedulesCache } from "./helpers/SchedulingNotifyCache"; import { delRestoreControllFile } from "./helpers/RestoreControll"; import "./helpers/AutoCloseTickets"; +import "./helpers/AutoRemoteTickets" import "./helpers/SchedulingNotifySendMessage"; import axios from "axios"; @@ -90,17 +91,27 @@ gracefulShutdown(server); const { phoneNumberId, id, greetingMessage } = whatsapps[i]; if (phoneNumberId) { - await set( - `whatsapp:${whatsapps[i].dataValues.id}`, - JSON.stringify({ - number: whatsapps[i].dataValues.number, - id, - greetingMessage, - phoneNumberId - }) - ); + // await set( + // `whatsapp:${whatsapps[i].dataValues.id}`, + // JSON.stringify({ + // number: whatsapps[i].dataValues.number, + // id, + // greetingMessage, + // phoneNumberId + // }) + // ); } + await set( + `whatsapp:${whatsapps[i].dataValues.id}`, + JSON.stringify({ + number: whatsapps[i].dataValues.number, + id, + greetingMessage, + phoneNumberId + }) + ); + if (phoneNumberId) { continue; } diff --git a/backend/src/services/MessageServices/CreateMessageService.ts b/backend/src/services/MessageServices/CreateMessageService.ts index 1c37495..6262728 100644 --- a/backend/src/services/MessageServices/CreateMessageService.ts +++ b/backend/src/services/MessageServices/CreateMessageService.ts @@ -1,4 +1,5 @@ import AppError from "../../errors/AppError"; +import { get, set } from "../../helpers/RedisClient"; import { updateTicketCacheByTicketId } from "../../helpers/TicketCache"; import { getIO } from "../../libs/socket"; import Message from "../../models/Message"; @@ -21,10 +22,8 @@ interface Request { const CreateMessageService = async ({ messageData -}: Request): Promise => { - +}: Request): Promise => { try { - await Message.upsert(messageData); const message = await Message.findByPk(messageData.id, { @@ -47,13 +46,35 @@ const CreateMessageService = async ({ throw new Error("ERR_CREATING_MESSAGE"); } - if (message.ticket.status != "queueChoice") { + //////////////////// SETTINGS /////////////////////////// + let ticketRemote = await get({ + key: `remote:ticketId:${message.ticket.id}` + }); + + if (ticketRemote && !ticketRemote.includes("messageDateTime")) { + ticketRemote = JSON.parse(ticketRemote); + + ticketRemote = { + ...ticketRemote, + ...{ messageDateTime: message.createdAt } + }; + + const ticket = await Ticket.findByPk(message.ticket.id); + ticket?.update({ remoteDone: true }); + + console.log('MESSAGE SERVICE: XXXXXXXXXXXXXXXXXXXXXXX') + + set(`remote:ticketId:${message.ticket.id}`, JSON.stringify(ticketRemote)); + } + ////////////////////////////////////////////////////////// + + if (message.ticket.status != "queueChoice") { await updateTicketCacheByTicketId(message.ticket.id, { lastMessage: message.body, updatedAt: new Date(message.ticket.updatedAt).toISOString(), "contact.profilePicUrl": message.ticket.contact.profilePicUrl, unreadMessages: message.ticket.unreadMessages - }); + }); const io = getIO(); io.to(message.ticketId.toString()) diff --git a/backend/src/services/ReportServices/ReportByNumberQueueService.ts b/backend/src/services/ReportServices/ReportByNumberQueueService.ts index 02c07dd..64fd234 100644 --- a/backend/src/services/ReportServices/ReportByNumberQueueService.ts +++ b/backend/src/services/ReportServices/ReportByNumberQueueService.ts @@ -15,14 +15,17 @@ interface Request { startDate: string | number; endDate: string; queue?: boolean; + isRemote?: boolean; } const ReportByNumberQueueService = async ({ startDate, endDate, - queue = false + queue = false, + isRemote = false }: Request): Promise => { let reportServiceData: any[] = []; + const includeIsRemote = isRemote ? "t.isRemote = true AND" : ""; const whatsapps = await Whatsapp.findAll(); @@ -30,6 +33,11 @@ const ReportByNumberQueueService = async ({ for (const whatsapp of whatsapps) { const { id, name, number } = whatsapp; + let startedByClient: any; + let avgChatWaitingTime: any; + let pendingChat: any; + let closedChat: any; + if ( !number || reportServiceData.findIndex((w: any) => w?.number == number) != -1 @@ -45,16 +53,17 @@ const ReportByNumberQueueService = async ({ JOIN Messages m ON t.id = m.ticketId JOIN Whatsapps w ON t.whatsappId = w.id JOIN Queues q ON q.id = t.queueId - WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + WHERE ${includeIsRemote} DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) AND m.fromAgent = 1 AND w.number = ${number};`, { type: QueryTypes.SELECT } ); - // CHAT STARTED BY CLIENT - const startedByClient: any = await sequelize.query( - `SELECT COUNT(DISTINCT t.id) AS ticket_count + if (!isRemote) { + // CHAT STARTED BY CLIENT + startedByClient = await sequelize.query( + `SELECT COUNT(DISTINCT t.id) AS ticket_count FROM Tickets t JOIN Messages m ON t.id = m.ticketId JOIN Whatsapps w ON t.whatsappId = w.id @@ -63,12 +72,28 @@ const ReportByNumberQueueService = async ({ AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) AND m.fromMe = 0 AND w.number = ${number};`, - { type: QueryTypes.SELECT } - ); + { type: QueryTypes.SELECT } + ); + } else { + // CHAT RESPONSE BY CLIENT + startedByClient = await sequelize.query( + `SELECT COUNT(DISTINCT t.id) AS ticket_count + FROM Tickets t + JOIN Messages m ON t.id = m.ticketId + JOIN Whatsapps w ON t.whatsappId = w.id + JOIN Queues q ON q.id = t.queueId + WHERE ${includeIsRemote} DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + AND m.createdAt = (SELECT MAX(createdAt) FROM Messages WHERE ticketId = t.id) + AND m.fromMe = 0 + AND w.number = ${number};`, + { type: QueryTypes.SELECT } + ); + } - // CHAT CLOSED - const closedChat: any = await sequelize.query( - `SELECT COUNT(DISTINCT t.id) AS ticket_count + if (!isRemote) { + // CHAT CLOSED + closedChat = await sequelize.query( + `SELECT COUNT(DISTINCT t.id) AS ticket_count FROM Tickets t JOIN Messages m ON t.id = m.ticketId JOIN Whatsapps w ON t.whatsappId = w.id @@ -76,12 +101,26 @@ const ReportByNumberQueueService = async ({ WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' AND t.status = 'closed' AND w.number = ${number};`, - { type: QueryTypes.SELECT } - ); + { type: QueryTypes.SELECT } + ); + } else { + // CHAT CLOSED + closedChat = await sequelize.query( + `SELECT COUNT(DISTINCT t.id) AS ticket_count + FROM Tickets t + JOIN Whatsapps w ON t.whatsappId = w.id + JOIN Queues q ON q.id = t.queueId + WHERE ${includeIsRemote} DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + AND t.status = 'closed' + AND w.number = ${number};`, + { type: QueryTypes.SELECT } + ); + } - // CHAT WAINTING TIME - const avgChatWaitingTime: any = await sequelize.query( - ` + if (!isRemote) { + // CHAT WAINTING TIME + avgChatWaitingTime = await sequelize.query( + ` SELECT TIME_FORMAT( SEC_TO_TIME( TIMESTAMPDIFF( @@ -104,25 +143,27 @@ const ReportByNumberQueueService = async ({ ) ) ), '%H:%i:%s') AS WAITING_TIME - FROM Tickets t - JOIN Messages m ON t.id = m.ticketId - JOIN Whatsapps w ON t.whatsappId = w.id - JOIN Queues q ON q.id = t.queueId - WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' - AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) - AND m.fromMe = 0 - -- AND q.id = 2 - AND w.number = ${number} - AND t.status IN ('open', 'closed') - HAVING WAITING_TIME IS NOT NULL - ORDER BY - WAITING_TIME;`, - { type: QueryTypes.SELECT } - ); + FROM Tickets t + JOIN Messages m ON t.id = m.ticketId + JOIN Whatsapps w ON t.whatsappId = w.id + JOIN Queues q ON q.id = t.queueId + WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) + AND m.fromMe = 0 + -- AND q.id = 2 + AND w.number = ${number} + AND t.status IN ('open', 'closed') + HAVING WAITING_TIME IS NOT NULL + ORDER BY + WAITING_TIME;`, + { type: QueryTypes.SELECT } + ); + } - // CHAT PENDING - const pendingChat: any = await sequelize.query( - `SELECT COUNT(DISTINCT t.id) AS ticket_count + if (!isRemote) { + // CHAT PENDING + pendingChat = await sequelize.query( + `SELECT COUNT(DISTINCT t.id) AS ticket_count FROM Tickets t JOIN Messages m ON t.id = m.ticketId JOIN Whatsapps w ON t.whatsappId = w.id @@ -131,23 +172,41 @@ const ReportByNumberQueueService = async ({ AND t.status = 'pending' AND w.number = ${number};`, - { type: QueryTypes.SELECT } - ); - + { type: QueryTypes.SELECT } + ); + } else { + // CHAT PENDING REMOTE + pendingChat = await sequelize.query( + `SELECT COUNT(DISTINCT t.id) AS ticket_count + FROM Tickets t + JOIN Whatsapps w ON t.whatsappId = w.id + JOIN Queues q ON q.id = t.queueId + WHERE ${includeIsRemote} DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + AND t.status = 'pending' + AND w.number = ${number};`, + + { type: QueryTypes.SELECT } + ); + } + reportServiceData.push({ id, name, number, startedByAgent: startedByAgent[0]?.ticket_count, - startedByClient: startedByClient[0]?.ticket_count, + startedByClient: startedByClient ? startedByClient[0]?.ticket_count : 0, closedChat: closedChat[0]?.ticket_count, - avgChatWaitingTime: avg(avgChatWaitingTime), + avgChatWaitingTime: avgChatWaitingTime ? avg(avgChatWaitingTime) : 0, pendingChat: pendingChat[0]?.ticket_count }); } } else { for (const whatsapp of whatsapps) { const { id, name, number } = whatsapp; + let startedByClient: any; + let avgChatWaitingTime: any; + let pendingChat: any; + let closedChat: any; if ( !number || @@ -172,16 +231,17 @@ const ReportByNumberQueueService = async ({ JOIN Messages m ON t.id = m.ticketId JOIN Whatsapps w ON t.whatsappId = w.id JOIN Queues q ON q.id = t.queueId - WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + WHERE ${includeIsRemote} DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) AND m.fromAgent = 1 AND q.id = ${q.id};`, { type: QueryTypes.SELECT } ); - // CHAT STARTED BY CLIENT - const startedByClient: any = await sequelize.query( - `SELECT COUNT(DISTINCT t.id) AS ticket_count + if (!isRemote) { + // CHAT STARTED BY CLIENT + startedByClient = await sequelize.query( + `SELECT COUNT(DISTINCT t.id) AS ticket_count FROM Tickets t JOIN Messages m ON t.id = m.ticketId JOIN Whatsapps w ON t.whatsappId = w.id @@ -190,64 +250,97 @@ const ReportByNumberQueueService = async ({ AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) AND m.fromMe = 0 AND q.id = ${q.id};`, - { type: QueryTypes.SELECT } - ); - - // CHAT CLOSED - const closedChat: any = await sequelize.query( - `SELECT COUNT(DISTINCT t.id) AS ticket_count + { type: QueryTypes.SELECT } + ); + } else { + // CHAT RESPONSE BY CLIENT + startedByClient = await sequelize.query( + `SELECT COUNT(DISTINCT t.id) AS ticket_count FROM Tickets t JOIN Messages m ON t.id = m.ticketId JOIN Whatsapps w ON t.whatsappId = w.id JOIN Queues q ON q.id = t.queueId - WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' - AND t.status = 'closed' - AND q.id = ${q.id};`, - { type: QueryTypes.SELECT } - ); + WHERE ${includeIsRemote} DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + AND m.createdAt = (SELECT MAX(createdAt) FROM Messages WHERE ticketId = t.id) + AND m.fromMe = 0 + AND q.id = ${q.id};`, + { type: QueryTypes.SELECT } + ); + } - // CHAT WAINTING TIME - const avgChatWaitingTime: any = await sequelize.query( - `SELECT TIME_FORMAT( - SEC_TO_TIME( - TIMESTAMPDIFF( - SECOND, - ( - SELECT createdAt - FROM Messages - WHERE ticketId = m.ticketId - AND fromMe = 0 - ORDER BY createdAt ASC - LIMIT 1 - ), - ( - SELECT createdAt - FROM Messages - WHERE ticketId = m.ticketId - AND fromAgent = 1 - ORDER BY createdAt ASC - LIMIT 1 - ) - ) - ), '%H:%i:%s') AS WAITING_TIME - FROM Tickets t - JOIN Messages m ON t.id = m.ticketId - JOIN Whatsapps w ON t.whatsappId = w.id - JOIN Queues q ON q.id = t.queueId - WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' - AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) - AND m.fromMe = 0 - AND q.id = ${q.id} - AND t.status IN ('open', 'closed') - HAVING WAITING_TIME IS NOT NULL - ORDER BY - WAITING_TIME;`, - { type: QueryTypes.SELECT } - ); + if (!isRemote) { + // CHAT CLOSED + closedChat = await sequelize.query( + `SELECT COUNT(DISTINCT t.id) AS ticket_count + FROM Tickets t + JOIN Messages m ON t.id = m.ticketId + JOIN Whatsapps w ON t.whatsappId = w.id + JOIN Queues q ON q.id = t.queueId + WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + AND t.status = 'closed' + AND q.id = ${q.id};`, + { type: QueryTypes.SELECT } + ); + } + else{ + // CHAT CLOSED REMOTE + closedChat = await sequelize.query( + `SELECT COUNT(DISTINCT t.id) AS ticket_count + FROM Tickets t + JOIN Whatsapps w ON t.whatsappId = w.id + JOIN Queues q ON q.id = t.queueId + WHERE ${includeIsRemote} DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + AND t.status = 'closed' + AND q.id = ${q.id};`, + { type: QueryTypes.SELECT } + ); + } - // CHAT PENDING - const pendingChat: any = await sequelize.query( - `SELECT COUNT(DISTINCT t.id) AS ticket_count + if (!isRemote) { + // CHAT WAINTING TIME + avgChatWaitingTime = await sequelize.query( + `SELECT TIME_FORMAT( + SEC_TO_TIME( + TIMESTAMPDIFF( + SECOND, + ( + SELECT createdAt + FROM Messages + WHERE ticketId = m.ticketId + AND fromMe = 0 + ORDER BY createdAt ASC + LIMIT 1 + ), + ( + SELECT createdAt + FROM Messages + WHERE ticketId = m.ticketId + AND fromAgent = 1 + ORDER BY createdAt ASC + LIMIT 1 + ) + ) + ), '%H:%i:%s') AS WAITING_TIME + FROM Tickets t + JOIN Messages m ON t.id = m.ticketId + JOIN Whatsapps w ON t.whatsappId = w.id + JOIN Queues q ON q.id = t.queueId + WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) + AND m.fromMe = 0 + AND q.id = ${q.id} + AND t.status IN ('open', 'closed') + HAVING WAITING_TIME IS NOT NULL + ORDER BY + WAITING_TIME;`, + { type: QueryTypes.SELECT } + ); + } + + if (!isRemote) { + // CHAT PENDING + pendingChat = await sequelize.query( + `SELECT COUNT(DISTINCT t.id) AS ticket_count FROM Tickets t JOIN Messages m ON t.id = m.ticketId JOIN Whatsapps w ON t.whatsappId = w.id @@ -256,8 +349,22 @@ const ReportByNumberQueueService = async ({ AND t.status = 'pending' AND q.id = ${q.id};`, - { type: QueryTypes.SELECT } - ); + { type: QueryTypes.SELECT } + ); + } else { + // CHAT PENDING REMOTE + pendingChat = await sequelize.query( + `SELECT COUNT(DISTINCT t.id) AS ticket_count + FROM Tickets t + JOIN Whatsapps w ON t.whatsappId = w.id + JOIN Queues q ON q.id = t.queueId + WHERE ${includeIsRemote} DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + AND t.status = 'pending' + AND q.id = ${q.id};`, + + { type: QueryTypes.SELECT } + ); + } reportServiceData.push({ id, @@ -266,9 +373,11 @@ const ReportByNumberQueueService = async ({ queueName: q.name, queueColor: q.color, startedByAgent: startedByAgent[0]?.ticket_count, - startedByClient: startedByClient[0]?.ticket_count, + startedByClient: startedByClient + ? startedByClient[0]?.ticket_count + : 0, closedChat: closedChat[0]?.ticket_count, - avgChatWaitingTime: avg(avgChatWaitingTime), + avgChatWaitingTime: avgChatWaitingTime ? avg(avgChatWaitingTime) : 0, pendingChat: pendingChat[0]?.ticket_count }); } @@ -283,23 +392,23 @@ export default ReportByNumberQueueService; function avg(avgChatWaitingTime: any) { let waitingAVG: any = avgChatWaitingTime .filter((t: any) => t?.WAITING_TIME) - .map((t: any) => t.WAITING_TIME) + .map((t: any) => t.WAITING_TIME); if (waitingAVG.length > 0) { - let midIndex = Math.floor((0 + waitingAVG.length) / 2) + let midIndex = Math.floor((0 + waitingAVG.length) / 2); if (waitingAVG.length % 2 == 1) { - waitingAVG = waitingAVG[midIndex] + waitingAVG = waitingAVG[midIndex]; } else { waitingAVG = calculateAverageTime( waitingAVG[midIndex - 1], waitingAVG[midIndex] - ) + ); } } else { - waitingAVG = 0 + waitingAVG = 0; } - return waitingAVG + return waitingAVG; } function calculateAverageTime(time1: string, time2: string) { diff --git a/backend/src/services/SettingServices/UpdateSettingService.ts b/backend/src/services/SettingServices/UpdateSettingService.ts index 59bf6b2..90fc3cd 100644 --- a/backend/src/services/SettingServices/UpdateSettingService.ts +++ b/backend/src/services/SettingServices/UpdateSettingService.ts @@ -4,12 +4,15 @@ import Setting from "../../models/Setting"; interface Request { key: string; value: string; + obj?: string; } const UpdateSettingService = async ({ key, - value + value, + obj }: Request): Promise => { + console.log("key: ", key, " | value: ", value, " | obj: ", obj); try { const setting = await Setting.findOne({ @@ -20,12 +23,16 @@ const UpdateSettingService = async ({ throw new AppError("ERR_NO_SETTING_FOUND", 404); } - await setting.update({ value }); + if (obj) { + obj = JSON.stringify(obj); + } + await setting.update({ value, obj }); + + await setting.reload(); return setting; - } catch (error: any) { - console.error('===> Error on UpdateSettingService.ts file: \n', error) + console.error("===> Error on UpdateSettingService.ts file: \n", error); throw new AppError(error.message); } }; diff --git a/backend/src/services/TicketServices/ListTicketsService.ts b/backend/src/services/TicketServices/ListTicketsService.ts index 4d31e3f..f91a671 100644 --- a/backend/src/services/TicketServices/ListTicketsService.ts +++ b/backend/src/services/TicketServices/ListTicketsService.ts @@ -20,6 +20,7 @@ import ListTicketServiceCache from "./ListTicketServiceCache"; import { searchTicketCache, loadTicketsCache } from "../../helpers/TicketCache"; import { getWbot } from "../../libs/wbot"; import User from "../../models/User"; +import { get } from "../../helpers/RedisClient"; interface Request { searchParam?: string; @@ -38,6 +39,7 @@ interface Response { tickets: Ticket[]; count: number; hasMore: boolean; + remoteTicketsControll?: object[]; } const ListTicketsService = async ({ @@ -230,7 +232,7 @@ const ListTicketsService = async ({ console.log("kkkkkkkkk limit: ", limit); - const { count, rows: tickets } = await Ticket.findAndCountAll({ + let { count, rows: tickets } = await Ticket.findAndCountAll({ where: whereCondition, include: includeCondition, distinct: true, @@ -241,10 +243,13 @@ const ListTicketsService = async ({ const hasMore = count > offset + tickets.length; + const ticketIds = await get({ key: `remote:controll`, parse: true }); + return { tickets, count, - hasMore + hasMore, + remoteTicketsControll: ticketIds ? ticketIds : [] }; }; diff --git a/backend/src/services/TicketServices/UpdateTicketService.ts b/backend/src/services/TicketServices/UpdateTicketService.ts index 545e015..f508a15 100644 --- a/backend/src/services/TicketServices/UpdateTicketService.ts +++ b/backend/src/services/TicketServices/UpdateTicketService.ts @@ -10,7 +10,7 @@ import { createOrUpdateTicketCache } from "../../helpers/TicketCache"; import AppError from "../../errors/AppError"; import sendWhatsAppMessageSocket from "../../helpers/SendWhatsappMessageSocket"; import BotIsOnQueue from "../../helpers/BotIsOnQueue"; -import { deleteObject } from "../../helpers/RedisClient"; +import { del, deleteObject, get, set } from "../../helpers/RedisClient"; var flatten = require("flat"); interface TicketData { @@ -71,6 +71,23 @@ const UpdateTicketService = async ({ await CheckContactOpenTickets(ticket.contact.id, ticket.whatsappId); } + if (status == "closed") { + del(`remote:ticketId:${ticket.id}`); + + let ticketsIds = await get({ key: `remote:controll`, parse: true }); + const index = ticketsIds.findIndex((t: any) => t == ticket.id); + + console.log("ticketsIds 1: ", ticketsIds); + + if (index != -1) { + ticketsIds.splice(index, 1); + + console.log("ticketsIds 2: ", ticketsIds); + + set(`remote:controll`, JSON.stringify(ticketsIds)); + } + } + await ticket.update({ status, queueId, diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 7d18638..cc4e801 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -94,7 +94,8 @@ import { findByContain, findObject, get, - getSimple + getSimple, + set } from "../../helpers/RedisClient"; import FindOrCreateTicketServiceBot from "../TicketServices/FindOrCreateTicketServiceBot"; import ShowTicketService from "../TicketServices/ShowTicketService"; @@ -325,7 +326,7 @@ const verifyMessage = async ( } else { messageData = { ...messageData, body: JSON.stringify(msg.vCards) }; } - } + } await CreateMessageService({ messageData }); }; @@ -357,7 +358,7 @@ const verifyQueue = async ( //////////////// EXTRAIR APENAS O NÚMERO /////////////////// selectedOption = selectedOption.match(/\d+/); /////////////////////////////////// - + if (selectedOption) choosenQueue = queues[+selectedOption - 1]; } } @@ -761,14 +762,11 @@ const handleMessage = async ( unreadMessages // groupContact ); - } + } if (getSettingValue("oneContactChatWithManyWhats")?.value == "disabled") { // Para responder para o cliente pelo mesmo whatsapp que ele enviou a mensagen if (wbot.id != ticket.whatsappId) { - // console.log('PARA RESPONDER PELO MEMOS WHATSAPP wbot.id: ', wbot.id, ' | wbot.status: ', wbot.status) - // console.log('WHATSAPP STATUS ticket.whatsappId: ', ticket.whatsappId) - try { await ticket.update({ whatsappId: wbot.id }); } catch (error: any) { diff --git a/frontend/src/components/MessageInput/index.js b/frontend/src/components/MessageInput/index.js index 8d7890f..9f0b895 100644 --- a/frontend/src/components/MessageInput/index.js +++ b/frontend/src/components/MessageInput/index.js @@ -43,6 +43,7 @@ import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketH import ModalTemplate from "../ModalTemplate" import { render } from '@testing-library/react' +import { countTicketMsgContext } from "../../context/CountTicketMsgProvider/CountTicketMsgProvider" const Mp3Recorder = new MicRecorder({ bitRate: 128 }) @@ -208,10 +209,12 @@ const useStyles = makeStyles((theme) => ({ }, })) -const MessageInput = ({ ticketStatus }) => { +const MessageInput = ({ ticketStatus, ticketLastMessage, ticketIsRemote }) => { const { tabOption, setTabOption } = useContext(TabTicketContext) + const { countTicketMsg, setCountTicketMsg } = useContext(countTicketMsgContext) + const classes = useStyles() const { ticketId } = useParams() @@ -233,11 +236,21 @@ const MessageInput = ({ ticketStatus }) => { const isRun = useRef(false) - useEffect(() => { inputRef.current.focus() }, [replyingMessage]) + + useEffect(() => { + + if (ticketIsRemote && countTicketMsg === 0 && ticketLastMessage && ticketLastMessage.trim().length > 0) { + setInputMessage(ticketLastMessage) + } + else { + setInputMessage("") + } + }, [countTicketMsg, ticketIsRemote, ticketLastMessage]) + useEffect(() => { inputRef.current.focus() return () => { @@ -352,6 +365,8 @@ const MessageInput = ({ ticketStatus }) => { setTemplates(data.data) } + setCountTicketMsg(1) + } catch (err) { toastError(err) } @@ -444,7 +459,7 @@ const MessageInput = ({ ticketStatus }) => { const { data } = await api.get("/quickAnswers/", { params: { searchParam: inputMessage.substring(1), userId: user.id }, }) - + setQuickAnswer(data.quickAnswers) if (data.quickAnswers.length > 0) { setTypeBar(true) diff --git a/frontend/src/components/MessagesList/index.js b/frontend/src/components/MessagesList/index.js index 5a99c79..cc46ec4 100644 --- a/frontend/src/components/MessagesList/index.js +++ b/frontend/src/components/MessagesList/index.js @@ -34,6 +34,7 @@ import whatsBackground from "../../assets/wa-background.png" import api from "../../services/api" import toastError from "../../errors/toastError" +import { CountTicketMsgProvider, countTicketMsgContext } from "../../context/CountTicketMsgProvider/CountTicketMsgProvider" const useStyles = makeStyles((theme) => ({ messagesListWrapper: { @@ -328,6 +329,8 @@ const MessagesList = ({ ticketId, isGroup }) => { const { user } = useContext(AuthContext) + const { setCountTicketMsg } = useContext(countTicketMsgContext) + useEffect(() => { dispatch({ type: "RESET" }) setPageNumber(1) @@ -409,6 +412,11 @@ const MessagesList = ({ ticketId, isGroup }) => { }) if (currentTicketId.current === ticketId) { + + if (data?.messages) { + setCountTicketMsg(data.messages.length) + } + dispatch({ type: "LOAD_MESSAGES", payload: data.messages }) setHasMore(data.hasMore) setLoading(false) diff --git a/frontend/src/components/Ticket/index.js b/frontend/src/components/Ticket/index.js index abc5cd3..9eab965 100644 --- a/frontend/src/components/Ticket/index.js +++ b/frontend/src/components/Ticket/index.js @@ -16,6 +16,7 @@ import MessagesList from "../MessagesList" import api from "../../services/api" import { ReplyMessageProvider } from "../../context/ReplyingMessage/ReplyingMessageContext" import toastError from "../../errors/toastError" +import { CountTicketMsgProvider } from "../../context/CountTicketMsgProvider/CountTicketMsgProvider" const drawerWidth = 320 @@ -192,11 +193,21 @@ const Ticket = () => { - - + + + + + + + + + ({ ticket: { @@ -101,13 +102,15 @@ const useStyles = makeStyles(theme => ({ }, })) -const TicketListItem = ({ ticket }) => { +const TicketListItem = ({ ticket, remoteTicketsControll, settings }) => { const classes = useStyles() const history = useHistory() const [loading, setLoading] = useState(false) const { ticketId } = useParams() const isMounted = useRef(true) - const { user } = useContext(AuthContext) + const { user, getSettingValue } = useContext(AuthContext) + const [_remoteTicketsControll, setRemoteTicketsControll] = useState([]) + const [_settings, setSettings] = useState(null) useEffect(() => { return () => { @@ -115,6 +118,19 @@ const TicketListItem = ({ ticket }) => { } }, []) + useEffect(() => { + setSettings(settings) + }, [settings]) + + useEffect(() => { + setRemoteTicketsControll(remoteTicketsControll) + }, [remoteTicketsControll]) + + useEffect(() => { + setRemoteTicketsControll(remoteTicketsControll) + }, [settings]) + + const handleAcepptTicket = async id => { setLoading(true) try { @@ -131,8 +147,6 @@ const TicketListItem = ({ ticket }) => { setLoading(false) } - - history.push(`/tickets/${id}`) } @@ -142,124 +156,164 @@ const TicketListItem = ({ ticket }) => { return ( - { - if (ticket.status === "pending") return - handleSelectTicket(ticket.id) - }} - selected={ticketId && +ticketId === ticket.id} - className={clsx(classes.ticket, { - [classes.pendingTicket]: ticket.status === "pending", - })} + - { + if (ticket.status === "pending") return + handleSelectTicket(ticket.id) + }} + selected={ticketId && +ticketId === ticket.id} + className={clsx(classes.ticket, { + [classes.pendingTicket]: ticket.status === "pending", + })} + + style={((ticket.status === "open" || ticket.status === "closed") && ticket?.isRemote) ? { + border: (ticket.status === "open" || ticket.status === "closed") ? "1px solid rgba(121,123,127,0.9)" : "1px solid transparent", + } : {}} + > - - - - - - - - {ticket.contact.name} - - {ticket.status === "closed" && ( - - )} - {ticket.lastMessage && ( + + + + + + + + {ticket.contact.name} + + {ticket.status === "closed" && ( + + )} + {ticket.lastMessage && ( + + {ticket?.phoneNumberId && Oficial}{" "} + {isSameDay(parseISO(ticket.updatedAt), new Date()) ? ( + <>{format(parseISO(ticket.updatedAt), "HH:mm")} + ) : ( + <>{format(parseISO(ticket.updatedAt), "dd/MM/yyyy")} + )} + + )} + + } + secondary={ + + - {ticket?.phoneNumberId && Oficial}{" "} - {isSameDay(parseISO(ticket.updatedAt), new Date()) ? ( - <>{format(parseISO(ticket.updatedAt), "HH:mm")} + {ticket.lastMessage ? ( + {ticket.lastMessage} ) : ( - <>{format(parseISO(ticket.updatedAt), "dd/MM/yyyy")} +
)}
- )} -
- } - secondary={ - - - {ticket.lastMessage ? ( - {ticket.lastMessage} - ) : ( -
- )} -
- + - {/* */} -
- } - /> - {ticket.status === "pending" && ( - handleAcepptTicket(ticket.id)} - > - {/* {i18n.t("ticketsList.buttons.accept")} */} - <> - {/* {i18n.t("ticketsList.buttons.accept")}
CAMPANHA */} + + } + /> + {ticket.status === "pending" && ( - {ticket?.isRemote ? ( - <>{i18n.t("ticketsList.buttons.accept")}
CAMPANHA - ) : ( - <>{i18n.t("ticketsList.buttons.accept")} - )} - -
- )} -
+ 0 && + getSettingValue('remoteTicketSendControll') && + getSettingValue('remoteTicketSendControll') === 'enabled') && !ticket?.remoteDone && !_remoteTicketsControll?.includes(+ticket.id)) ? + { style: { backgroundColor: "rgba(121,123,127,0.5)", color: "white" } } : + { style: { backgroundColor: "rgba(121,123,127,0.9)", color: "white" } } : + + { color: "primary" })} + + variant="contained" + + disabled={true ? ((settings && + settings.length > 0 && + getSettingValue('remoteTicketSendControll') && + getSettingValue('remoteTicketSendControll') === 'enabled') && ticket?.isRemote && !ticket?.remoteDone && !_remoteTicketsControll?.includes(+ticket.id)) : false} + + + // disabled={true ? (ticket?.isRemote && !ticket?.remoteDone && !_remoteTicketsControll?.includes(+ticket.id)) : false} + + className={classes.acceptButton} + size="small" + loading={loading} + onClick={e => handleAcepptTicket(ticket.id)} + > + + <> + {(ticket?.isRemote && !ticket?.remoteDone) ? ( + <>{i18n.t("ticketsList.buttons.accept")}
CAMPANHA + ) : ( + <>{i18n.t("ticketsList.buttons.accept")} + )} + + + +
+ + + + )} + + +
) diff --git a/frontend/src/components/TicketsList/index.js b/frontend/src/components/TicketsList/index.js index 9754d79..ec05eb6 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 { Divider } from "@material-ui/core" const useStyles = makeStyles(theme => ({ ticketsListWrapper: { @@ -181,10 +182,18 @@ const TicketsList = (props) => { const classes = useStyles() const [pageNumber, setPageNumber] = useState(1) const [ticketsList, dispatch] = useReducer(reducer, []) - const { user } = useContext(AuthContext) + const { user, setting, } = useContext(AuthContext) const { searchTicket } = useContext(SearchTicketContext) + const [_remoteTicketsControll, setRemoteTicketsControll] = useState([]) + + const [settings, setSettings] = useState([]) + + useEffect(() => { + setSettings(setting) + }, [setting]) + useEffect(() => { dispatch({ type: "RESET" }) @@ -192,17 +201,22 @@ const TicketsList = (props) => { }, [status, searchParam, searchParamContent, showAll, selectedQueueIds, searchTicket]) - const { tickets, hasMore, loading } = useTickets({ + let { tickets, hasMore, loading, remoteTicketsControll } = useTickets({ pageNumber, searchParam, searchParamContent, status, - showAll, + showAll, queueIds: JSON.stringify(selectedQueueIds), tab, unlimited: status === 'open' ? "all" : "false" }) + + useEffect(() => { + setRemoteTicketsControll(remoteTicketsControll) + }, [remoteTicketsControll]) + useEffect(() => { if (!status && !searchParam) return @@ -301,6 +315,27 @@ const TicketsList = (props) => { } }) + + socket.on('remoteTickesControll', (data) => { + + if (data.action === 'update') { + setRemoteTicketsControll(data.tickets) + } + }) + + + socket.on('settings', (data) => { + if (data.action === 'update') { + setSettings((prevState) => { + const aux = [...prevState] + const settingIndex = aux.findIndex((s) => s.key === data.setting.key) + aux[settingIndex].value = data.setting.value + return aux + }) + } + }) + + return () => { socket.disconnect() } @@ -354,7 +389,7 @@ const TicketsList = (props) => { ) : ( <> {ticketsList.map(ticket => ( - + ))} )} diff --git a/frontend/src/context/CountTicketMsgProvider/CountTicketMsgProvider.js b/frontend/src/context/CountTicketMsgProvider/CountTicketMsgProvider.js new file mode 100644 index 0000000..0af146c --- /dev/null +++ b/frontend/src/context/CountTicketMsgProvider/CountTicketMsgProvider.js @@ -0,0 +1,17 @@ +import React, { useState, createContext } from "react" + +const countTicketMsgContext = createContext() + + +const CountTicketMsgProvider = ({ children }) => { + + const [countTicketMsg, setCountTicketMsg] = useState(0) + + return ( + + {children} + + ) +} + +export { countTicketMsgContext, CountTicketMsgProvider } \ No newline at end of file diff --git a/frontend/src/hooks/useTickets/index.js b/frontend/src/hooks/useTickets/index.js index 7dfe366..4ce180b 100644 --- a/frontend/src/hooks/useTickets/index.js +++ b/frontend/src/hooks/useTickets/index.js @@ -1,7 +1,7 @@ -import { useState, useEffect } from "react"; -import toastError from "../../errors/toastError"; +import { useState, useEffect } from "react" +import toastError from "../../errors/toastError" -import api from "../../services/api"; +import api from "../../services/api" const useTickets = ({ searchParam, @@ -15,21 +15,21 @@ const useTickets = ({ unlimited, tab }) => { - const [loading, setLoading] = useState(true); - const [hasMore, setHasMore] = useState(false); - const [tickets, setTickets] = useState([]); - + const [loading, setLoading] = useState(true) + const [hasMore, setHasMore] = useState(false) + const [tickets, setTickets] = useState([]) + const [remoteTicketsControll, setRemoteTicketsControll] = useState([]) useEffect(() => { - setLoading(true); + setLoading(true) const delayDebounceFn = setTimeout(() => { const fetchTickets = async () => { try { - if ((tab === 'search') && ( !searchParam || searchParam.trim().length === 0 || searchParam.trim().length >40 || searchParam.endsWith(' '))) { + if ((tab === 'search') && (!searchParam || searchParam.trim().length === 0 || searchParam.trim().length > 40 || searchParam.endsWith(' '))) { return } @@ -45,24 +45,26 @@ const useTickets = ({ withUnreadMessages, unlimited }, - }); + }) + setTickets(data.tickets) + setHasMore(data.hasMore) + setLoading(false) + + if (data?.remoteTicketsControll) { + setRemoteTicketsControll(data.remoteTicketsControll.map(t => +t)) + } - setTickets(data.tickets); - setHasMore(data.hasMore); - setLoading(false); - - } catch (err) { - setLoading(false); - toastError(err); + setLoading(false) + toastError(err) } - }; - fetchTickets(); - }, 500); - return () => clearTimeout(delayDebounceFn); + } + fetchTickets() + }, 500) + return () => clearTimeout(delayDebounceFn) }, [ searchParam, searchParamContent, @@ -74,9 +76,9 @@ const useTickets = ({ withUnreadMessages, tab, unlimited - ]); + ]) - return { tickets, loading, hasMore }; -}; + return { tickets, loading, hasMore, remoteTicketsControll } +} -export default useTickets; +export default useTickets diff --git a/frontend/src/pages/Report/index.js b/frontend/src/pages/Report/index.js index 95083fe..c57242d 100644 --- a/frontend/src/pages/Report/index.js +++ b/frontend/src/pages/Report/index.js @@ -11,7 +11,7 @@ import { AuthContext } from "../../context/Auth/AuthContext" import { Can } from "../../components/Can" import FormControlLabel from "@mui/material/FormControlLabel" import Checkbox from '@mui/material/Checkbox' -import { Button } from "@material-ui/core" +import { Button, Tooltip } from "@material-ui/core" import ReportModal from "../../components/ReportModal" import ReportModalType from "../../components/ReportModalType" import MaterialTable from 'material-table' @@ -299,6 +299,7 @@ const Report = () => { const [csvFile, setCsvFile] = useState() const [selectedValue, setSelectedValue] = useState('created') const [checked, setChecked] = useState(true) + const [checkedRemote, setCheckedRemote] = useState(false) const [queues, setQueues] = useState([]) const [queueId, setQueue] = useState(null) @@ -363,7 +364,7 @@ const Report = () => { if (reportOption === '1') { const { data } = await api.get("/reports/", { params: { userId, startDate, endDate, pageNumber: pageNumberTickets, createdOrUpdated: selectedValue, queueId }, userQueues: userA.queues }) - + let ticketsQueue = data.tickets let userQueues = userA.queues let filterQueuesTickets = [] @@ -378,7 +379,7 @@ const Report = () => { messagesToFilter: ticket.messages.map(message => message.body).join(' '), })) dispatchQ({ type: "LOAD_QUERY", payload: tickets }) - console.log(tickets) + // console.log(tickets) setHasMore(data.hasMore) setTotalCountTickets(data.count) setLoading(false) @@ -395,15 +396,13 @@ const Report = () => { } else if (reportOption === '3') { - const dataQuery = await api.get("/reports/services/numbers", { params: { startDate, endDate }, }) + const dataQuery = await api.get("/reports/services/numbers", { params: { startDate, endDate, isRemote: checkedRemote }, }) dispatchQ({ type: "RESET" }) dispatchQ({ type: "LOAD_QUERY", payload: dataQuery?.data?.reportService }) } else if (reportOption === '4') { - const dataQuery = await api.get("/reports/services/queues", { params: { startDate, endDate }, }) - - console.log(' dataQuery?.data?.reportService: ', dataQuery?.data?.reportService) + const dataQuery = await api.get("/reports/services/queues", { params: { startDate, endDate, isRemote: checkedRemote }, }) dispatchQ({ type: "RESET" }) dispatchQ({ type: "LOAD_QUERY", payload: dataQuery?.data?.reportService }) @@ -419,7 +418,7 @@ const Report = () => { }, 500) return () => clearTimeout(delayDebounceFn) - }, [userId, queueId, checked, startDate, endDate, reportOption, pageNumberTickets, totalCountTickets, selectedValue]) + }, [userId, queueId, checked, checkedRemote, startDate, endDate, reportOption, pageNumberTickets, totalCountTickets, selectedValue]) const handleCheckBoxChange = (value) => { @@ -460,7 +459,7 @@ const Report = () => { } // Get from report type option - const reportTypeValue = (data) => { + const reportTypeValue = (data) => { let type = '1' if (data === '1') type = 'default' if (data === '2') type = 'synthetic' @@ -737,6 +736,10 @@ const Report = () => { setChecked(event.target.checked) } + const handleChangeRemote = (event) => { + setCheckedRemote(event.target.checked) + } + return ( { } + {(reportOption === '3' || reportOption === '4') && +
+ + + + + + + +
+ } @@ -959,7 +991,7 @@ const Report = () => { title={i18n.t("reports.listTitles.title4_1")} columns={ - [ + !checkedRemote ? [ { title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, }, { title: 'Conversas iniciadas', field: 'startedByAgent', }, { title: 'Conversas recebidas', field: 'startedByClient' }, @@ -967,7 +999,15 @@ const Report = () => { { title: `Tempo médio de espera`, field: 'avgChatWaitingTime' }, { title: 'Aguardando', field: 'pendingChat' } - ] + ] : + [ + { title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, }, + { title: 'Conversas iniciadas', field: 'startedByAgent', }, + { title: 'Conversas respondidas', field: 'startedByClient' }, + { title: `Conversas finalizadas`, field: 'closedChat' }, + { title: 'Aguardando', field: 'pendingChat' } + + ] } data={dataRows} @@ -1003,7 +1043,7 @@ const Report = () => { title={i18n.t("reports.listTitles.title5_1")} columns={ - [ + !checkedRemote ? [ { title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, }, { @@ -1023,7 +1063,28 @@ const Report = () => { { title: `Tempo médio de espera`, field: 'avgChatWaitingTime' }, { title: 'Aguardando', field: 'pendingChat' } - ] + ] : + + [ + + { title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, }, + { + title: 'Fila', field: 'queueName', + cellStyle: (evt, rowData) => { + return { + whiteSpace: 'nowrap', + backgroundColor: rowData?.queueColor || 'inherit', + color: 'white' + } + } + + }, + { title: 'Conversas iniciadas', field: 'startedByAgent', }, + { title: 'Conversas respondidas', field: 'startedByClient' }, + { title: `Conversas finalizadas`, field: 'closedChat' }, + { title: 'Aguardando', field: 'pendingChat' } + + ] } data={dataRows} diff --git a/frontend/src/pages/Settings/index.js b/frontend/src/pages/Settings/index.js index 0fe21e2..a506ced 100644 --- a/frontend/src/pages/Settings/index.js +++ b/frontend/src/pages/Settings/index.js @@ -12,13 +12,38 @@ import api from '../../services/api' import { i18n } from '../../translate/i18n.js' import toastError from '../../errors/toastError' +import TextField from '@material-ui/core/TextField' +import Button from '@material-ui/core/Button' + + //-------- import { AuthContext } from '../../context/Auth/AuthContext' import { Can } from '../../components/Can' +import { boolean } from 'yup' // import Button from "@material-ui/core/Button"; +const IntegerInput = ({ value, onChange }) => { + const handleChange = (event) => { + const inputValue = event.target.value + // Only allow digits 0-9 + if (/^\d{0,3}$/.test(inputValue)) { + onChange(inputValue) + } + } + + return ( + + ) +} + const useStyles = makeStyles((theme) => ({ root: { display: 'flex', @@ -48,11 +73,48 @@ const Settings = () => { const [settings, setSettings] = useState([]) + + const [number1, setNumber1] = useState('') + const [number2, setNumber2] = useState('') + + const handleNumber1Change = (value) => { + setNumber1(value) + } + + const handleNumber2Change = (value) => { + setNumber2(value) + } + + const handleGetValues = () => { + let e = { + target: { + value: 'enabled', name: 'remoteTicketSendControll', obj: (number1.trim().length > 0 && number2.trim().length > 0) ? { seconds1: number1, seconds2: number2 } : null + } + } + + handleChangeSetting(e) + } + + useEffect(() => { const fetchSession = async () => { try { const { data } = await api.get('/settings') + console.log('data.settings: ', data.settings) setSettings(data.settings) + + if (data?.settings) { + let { obj } = data.settings.find((s) => s.key === 'remoteTicketSendControll') + + if (!obj) return + + obj = JSON.parse(obj) + console.log('SETTING obj: ', obj) + + setNumber1(obj.seconds1) + setNumber2(obj.seconds2) + } + } catch (err) { toastError(err) } @@ -60,6 +122,7 @@ const Settings = () => { fetchSession() }, []) + useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL) @@ -81,17 +144,15 @@ const Settings = () => { } }, []) - // useEffect(() => { - // console.log('------> settings: ', settings) - // }, [settings]) const handleChangeSetting = async (e) => { const selectedValue = e.target.value - const settingKey = e.target.name + const settingKey = e.target.name try { await api.put(`/settings/${settingKey}`, { value: selectedValue, + // obj: e.target?.obj ? e.target.obj : null }) if (settingKey === 'farewellMessageByQueue' && @@ -117,12 +178,20 @@ const Settings = () => { } } - const getSettingValue = (key) => { - const { value } = settings.find((s) => s.key === key) + const getSettingValue = (key, _obj = false) => { + const { value, obj } = settings.find((s) => s.key === key) + + if (_obj) + return obj return value } + const isSaveDisabled = (settings && + settings.length > 0 && + getSettingValue('remoteTicketSendControll') === 'disabled') + + return ( { +
+ + + + Controle de envio de mensagem de ticket remoto por numero + + + + + {/* +
+

Tempo aleatorio em segundos

+ +
+ + + +
+ +
+
*/} + +
+
+ )} /> ) } + + export default Settings