diff --git a/backend/package.json b/backend/package.json index 1d55190..f9c9b6d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,8 +21,8 @@ "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.5", "cors": "^2.8.5", - "date-fns": "^2.16.1", - "date-fns-tz": "^1.3.4", + "date-fns": "^2.30.0", + "date-fns-tz": "^1.3.8", "dotenv": "^8.2.0", "express": "^4.17.1", "express-async-errors": "^3.1.1", diff --git a/backend/src/controllers/SettingController.ts b/backend/src/controllers/SettingController.ts index 711af60..639cd75 100644 --- a/backend/src/controllers/SettingController.ts +++ b/backend/src/controllers/SettingController.ts @@ -6,6 +6,8 @@ import AppError from "../errors/AppError"; import UpdateSettingService from "../services/SettingServices/UpdateSettingService"; import ListSettingsService from "../services/SettingServices/ListSettingsService"; import loadSettings from "../helpers/LoadSettings"; +import updateSettingTicket from "../services/SettingServices/UpdateSettingTicket"; +import SettingTicket from "../models/SettingTicket"; export const index = async (req: Request, res: Response): Promise => { // if (req.user.profile !== "master") { @@ -14,7 +16,76 @@ export const index = async (req: Request, res: Response): Promise => { const settings = await ListSettingsService(); - return res.status(200).json(settings); + const config = await SettingTicket.findAll(); + + return res.status(200).json({ settings, config }); +}; + +export const updateTicketSettings = async ( + req: Request, + res: Response +): Promise => { + const { + outBusinessHours, + ticketExpiration, + weekend, + saturday, + sunday, + holiday + } = req.body; + + if (outBusinessHours && Object.keys(outBusinessHours).length > 0) { + await updateSettingTicket({ + ...outBusinessHours, + key: "outBusinessHours" + }); + } + + if (ticketExpiration && Object.keys(ticketExpiration).length > 0) { + await updateSettingTicket({ + ...ticketExpiration, + key: "ticketExpiration" + }); + } + + if (weekend && Object.keys(weekend).length > 0) { + await updateSettingTicket({ + ...weekend, + key: "weekend" + }); + } + + if (saturday && Object.keys(saturday).length > 0) { + await updateSettingTicket({ + ...saturday, + key: "saturday" + }); + } + + if (sunday && Object.keys(sunday).length > 0) { + await updateSettingTicket({ + ...sunday, + key: "sunday" + }); + } + + if (holiday && Object.keys(holiday).length > 0) { + await updateSettingTicket({ + ...holiday, + key: "holiday" + }); + } + + return res + .status(200) + .json({ + outBusinessHours, + ticketExpiration, + weekend, + saturday, + sunday, + holiday + }); }; export const update = async ( diff --git a/backend/src/database/index.ts b/backend/src/database/index.ts index 9749831..f5bf45d 100644 --- a/backend/src/database/index.ts +++ b/backend/src/database/index.ts @@ -14,6 +14,7 @@ import QuickAnswer from "../models/QuickAnswer"; import SchedulingNotify from "../models/SchedulingNotify"; import StatusChatEnd from "../models/StatusChatEnd"; import UserOnlineTime from "../models/UserOnlineTime"; +import SettingTicket from "../models/SettingTicket"; // eslint-disable-next-line const dbConfig = require("../config/database"); // import dbConfig from "../config/database"; @@ -35,7 +36,8 @@ const models = [ SchedulingNotify, StatusChatEnd, - UserOnlineTime, + UserOnlineTime, + SettingTicket ]; sequelize.addModels(models); diff --git a/backend/src/database/migrations/20230807152412-setting-tickets.ts b/backend/src/database/migrations/20230807152412-setting-tickets.ts new file mode 100644 index 0000000..b2581c8 --- /dev/null +++ b/backend/src/database/migrations/20230807152412-setting-tickets.ts @@ -0,0 +1,46 @@ +import { QueryInterface, DataTypes } from "sequelize" + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.createTable("SettingTickets", { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false + }, + message: { + type: DataTypes.STRING, + allowNull: true + }, + startTime: { + type: DataTypes.DATE, + allowNull: true + }, + endTime: { + type: DataTypes.DATE, + allowNull: true + }, + value: { + type: DataTypes.STRING, + allowNull: false + }, + key: { + type: DataTypes.STRING, + allowNull: false + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false + } + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.dropTable("SettingTickets"); + } +} diff --git a/backend/src/database/seeds/20230807154146-add-setting-tickets.ts b/backend/src/database/seeds/20230807154146-add-setting-tickets.ts new file mode 100644 index 0000000..9d40d31 --- /dev/null +++ b/backend/src/database/seeds/20230807154146-add-setting-tickets.ts @@ -0,0 +1,34 @@ +import { QueryInterface } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.bulkInsert( + "SettingTickets", + [ + { + message: "", + startTime: new Date(), + endTime: new Date(), + value: "disabled", + key: "outBusinessHours", + createdAt: new Date(), + updatedAt: new Date() + }, + { + message: "", + startTime: new Date(), + endTime: new Date(), + value: "disabled", + key: "ticketExpiration", + createdAt: new Date(), + updatedAt: new Date() + } + ], + {} + ); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.bulkDelete("SettingTickets", {}); + } +}; diff --git a/backend/src/database/seeds/20230808210411-add-setting-ticket-weekend.ts b/backend/src/database/seeds/20230808210411-add-setting-ticket-weekend.ts new file mode 100644 index 0000000..8ffa27d --- /dev/null +++ b/backend/src/database/seeds/20230808210411-add-setting-ticket-weekend.ts @@ -0,0 +1,34 @@ +import { QueryInterface } from "sequelize" + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.bulkInsert( + "SettingTickets", + [ + { + message: "", + startTime: new Date(), + endTime: new Date(), + value: "disabled", + key: "saturday", + createdAt: new Date(), + updatedAt: new Date() + }, + { + message: "", + startTime: new Date(), + endTime: new Date(), + value: "disabled", + key: "sunday", + createdAt: new Date(), + updatedAt: new Date() + } + ], + {} + ); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.bulkDelete("SettingTickets", {}) + } +} diff --git a/backend/src/database/seeds/20230808210840-add-setting-ticket-holiday.ts b/backend/src/database/seeds/20230808210840-add-setting-ticket-holiday.ts new file mode 100644 index 0000000..efd73ce --- /dev/null +++ b/backend/src/database/seeds/20230808210840-add-setting-ticket-holiday.ts @@ -0,0 +1,25 @@ +import { QueryInterface } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.bulkInsert( + "SettingTickets", + [ + { + message: "", + startTime: new Date(), + endTime: new Date(), + value: "disabled", + key: "holiday", + createdAt: new Date(), + updatedAt: new Date() + }, + ], + {} + ); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.bulkDelete("SettingTickets", {}); + } +}; diff --git a/backend/src/database/seeds/20230809085716-add-setting-ticket-weekend-enable.ts b/backend/src/database/seeds/20230809085716-add-setting-ticket-weekend-enable.ts new file mode 100644 index 0000000..48fd2db --- /dev/null +++ b/backend/src/database/seeds/20230809085716-add-setting-ticket-weekend-enable.ts @@ -0,0 +1,25 @@ +import { QueryInterface } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.bulkInsert( + "SettingTickets", + [ + { + message: "", + startTime: new Date(), + endTime: new Date(), + value: "disabled", + key: "weekend", + createdAt: new Date(), + updatedAt: new Date() + } + ], + {} + ); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.bulkDelete("SettingTickets", {}); + } +}; diff --git a/backend/src/helpers/AutoCloseTickets.ts b/backend/src/helpers/AutoCloseTickets.ts new file mode 100644 index 0000000..4074c37 --- /dev/null +++ b/backend/src/helpers/AutoCloseTickets.ts @@ -0,0 +1,84 @@ +import SettingTicket from "../models/SettingTicket"; +import ListTicketTimeLife from "../services/TicketServices/ListTicketTimeLife"; +import UpdateTicketService from "../services/TicketServices/UpdateTicketService"; +import BotIsOnQueue from "./BotIsOnQueue"; + +import { + format as _format, + isWithinInterval, + parse, + subMinutes +} from "date-fns"; + +import ptBR from "date-fns/locale/pt-BR"; +import { splitDateTime } from "./SplitDateTime"; + +const fsPromises = require("fs/promises"); +const fs = require("fs"); + +let timer: any; + +const AutoCloseTickets = async () => { + try { + // const botInfo = await BotIsOnQueue('botqueue') + + // if (!botInfo.userIdBot) return + + const ticketExpiration = await SettingTicket.findOne({ + where: { key: "ticketExpiration" } + }); + + if (ticketExpiration && ticketExpiration.value == "enabled") { + const startTime = splitDateTime( + new Date( + _format(new Date(ticketExpiration.startTime), "yyyy-MM-dd HH:mm:ss", { + locale: ptBR + }) + ) + ); + + const seconds = timeStringToSeconds(startTime.fullTime); + + // console.log("Ticket seconds: ", seconds); + + let tickets: any = await ListTicketTimeLife({ + timeseconds: seconds, + status: "open" + }); + + // console.log("tickets: ", tickets); + + for (let i = 0; i < tickets.length; i++) { + + await UpdateTicketService({ + ticketData: { status: "closed", statusChatEnd: "FINALIZADO" }, + ticketId: tickets[i].ticket_id, + msg: ticketExpiration.message + }); + } + } + } catch (error) { + console.log("There was an error on try close the bot tickets: ", error); + } +}; + +function timeStringToSeconds(timeString: any) { + const [hours, minutes, seconds] = timeString.split(":").map(Number); + return hours * 3600 + minutes * 60 + seconds; +} + +const schedule = async () => { + try { + clearInterval(timer); + + await AutoCloseTickets(); + } catch (error) { + console.log("error on schedule: ", error); + } finally { + timer = setInterval(schedule, 60000); + } +}; + +timer = setInterval(schedule, 60000); + +export default schedule; diff --git a/backend/src/helpers/MostRepeatedPhrase.ts b/backend/src/helpers/MostRepeatedPhrase.ts new file mode 100644 index 0000000..71ced82 --- /dev/null +++ b/backend/src/helpers/MostRepeatedPhrase.ts @@ -0,0 +1,56 @@ +import { subSeconds } from "date-fns"; +import Message from "../models/Message"; + +import { Op, Sequelize } from "sequelize"; + +const mostRepeatedPhrase = async ( + ticketId: number | string, + fromMe: boolean = false +) => { + let res: any = { body: "", occurrences: 0 }; + + try { + const mostRepeatedPhrase: any = await Message.findOne({ + where: { + ticketId: ticketId, + fromMe: fromMe ? fromMe : 0, + body: { + [Op.notRegexp]: "^[0-9]+$" + }, + updatedAt: { + [Op.between]: [+subSeconds(new Date(), 150), +new Date()] + } + }, + attributes: [ + "body", + [Sequelize.fn("COUNT", Sequelize.col("body")), "occurrences"] + ], + + group: ["body"], + order: [[Sequelize.literal("occurrences"), "DESC"]], + limit: 1 + }); + + if (mostRepeatedPhrase) { + const { body, occurrences } = mostRepeatedPhrase.get(); + + console.log( + `The most repeated phrase is "${body}" with ${occurrences} occurrences.` + ); + + const isNumber = /^\d+$/.test(body.trim()); + + if (!isNumber) { + return { body, occurrences }; + } + } else { + console.log("No phrases found."); + } + } catch (error) { + console.log("error on MostRepeatedPhrase: ", error); + } + + return { body: "", occurrences: 0 }; +}; + +export default mostRepeatedPhrase; diff --git a/backend/src/helpers/TicketConfig.ts b/backend/src/helpers/TicketConfig.ts new file mode 100644 index 0000000..830646a --- /dev/null +++ b/backend/src/helpers/TicketConfig.ts @@ -0,0 +1,183 @@ +import SettingTicket from "../models/SettingTicket"; +import { splitDateTime } from "./SplitDateTime"; +import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz"; + +import { + format as _format, + isWithinInterval, + parse, + subMinutes, + isSaturday, + isSunday, + parseISO +} from "date-fns"; +import ptBR from "date-fns/locale/pt-BR"; + +const isHoliday = async () => { + let obj = { set: false, msg: "" }; + + const holiday = await SettingTicket.findOne({ + where: { key: "holiday" } + }); + + if ( + holiday && + holiday.value == "enabled" && + holiday.message?.trim()?.length > 0 + ) { + const startTime = splitDateTime( + new Date( + _format(new Date(holiday.startTime), "yyyy-MM-dd HH:mm:ss", { + locale: ptBR + }) + ) + ); + + const currentDate = splitDateTime( + new Date( + _format(new Date(), "yyyy-MM-dd HH:mm:ss", { + locale: ptBR + }) + ) + ); + + if (currentDate.fullDate == startTime.fullDate) { + obj.set = true; + obj.msg = holiday.message.trim(); + } + } + + return obj; +}; + +const isWeekend = async () => { + let obj = { set: false, msg: "" }; + + const weekend = await SettingTicket.findOne({ + where: { key: "weekend" } + }); + + if ( + weekend && + weekend.value == "enabled" && + weekend.message?.trim()?.length > 0 + ) { + + // Specify your desired timezone + const brazilTimeZone = "America/Sao_Paulo"; + + const currentDateUtc = new Date(); + + // Convert UTC date to Brazil time zone + const currentDate = utcToZonedTime(currentDateUtc, brazilTimeZone); + + // Format the date using the desired format + const formattedDate = _format(currentDate, "yyyy-MM-dd HH:mm:ssXXX"); + + const parsedDate = parseISO(formattedDate); + + // Convert parsed date to Brazil time zone + const localDate = utcToZonedTime(parsedDate, brazilTimeZone); + + // Check if it's Saturday or Sunday + if (isSaturday(localDate)) { + const saturday = await SettingTicket.findOne({ + where: { key: "saturday" } + }); + + if (saturday && saturday.value == "enabled") { + // botSendMessage(ticket, weekend.message); + obj.set = true; + obj.msg = weekend.message; + } + } else if (isSunday(localDate)) { + const sunday = await SettingTicket.findOne({ + where: { key: "sunday" } + }); + + if (sunday && sunday.value == "enabled") { + // botSendMessage(ticket, weekend.message); + obj.set = true; + obj.msg = weekend.message; + } + } + else{ + // obj.set = true; + // obj.msg = weekend.message; + } + + return obj; + } +}; + +async function isOutBusinessTime() { + let obj = { set: false, msg: "" }; + + const outBusinessHours = await SettingTicket.findOne({ + where: { key: "outBusinessHours" } + }); + + let isWithinRange = false; + + if ( + outBusinessHours && + outBusinessHours.value == "enabled" && + outBusinessHours?.message?.trim()?.length > 0 + ) { + const ticketDateTimeUpdate = splitDateTime( + new Date( + _format(new Date(), "yyyy-MM-dd HH:mm:ss", { + locale: ptBR + }) + ) + ); + + const startTime = splitDateTime( + new Date( + _format(new Date(outBusinessHours.startTime), "yyyy-MM-dd HH:mm:ss", { + locale: ptBR + }) + ) + ); + + const endTime = splitDateTime( + new Date( + _format(new Date(outBusinessHours.endTime), "yyyy-MM-dd HH:mm:ss", { + locale: ptBR + }) + ) + ); + + const format = "HH:mm:ss"; + const parsedStartTime = parse( + ticketDateTimeUpdate.fullTime, + format, + new Date() + ); + const parsedEndTime = parse(startTime.fullTime, format, new Date()); + const parsedTimeToCheck = parse(endTime.fullTime, format, new Date()); + const timeInterval = { start: parsedStartTime, end: parsedEndTime }; + + // If the time range spans across different days, handle the date part + if (parsedEndTime < parsedStartTime) { + const nextDay = new Date(parsedStartTime); + nextDay.setDate(nextDay.getDate() + 1); + timeInterval.end = nextDay; + } + + isWithinRange = isWithinInterval(parsedTimeToCheck, timeInterval); + + if (!isWithinRange) { + obj.set = true; + obj.msg = outBusinessHours.message; + } + } + + return obj; +} + +export { + isWeekend, + isHoliday, + isOutBusinessTime +}; diff --git a/backend/src/models/SettingTicket.ts b/backend/src/models/SettingTicket.ts new file mode 100644 index 0000000..10ef6c2 --- /dev/null +++ b/backend/src/models/SettingTicket.ts @@ -0,0 +1,40 @@ +import { + Table, + Column, + CreatedAt, + UpdatedAt, + Model, + PrimaryKey, + AutoIncrement +} from "sequelize-typescript"; + +@Table +class SettingTicket extends Model { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @Column + message: string; + + @Column + startTime: Date; + + @Column + endTime: Date; + + @Column + value: string; + + @Column + key: string; + + @CreatedAt + createdAt: Date; + + @UpdatedAt + updatedAt: Date; +} + +export default SettingTicket; diff --git a/backend/src/routes/settingRoutes.ts b/backend/src/routes/settingRoutes.ts index 7047a63..dc361fb 100644 --- a/backend/src/routes/settingRoutes.ts +++ b/backend/src/routes/settingRoutes.ts @@ -3,13 +3,21 @@ import isAuth from "../middleware/isAuth"; import * as SettingController from "../controllers/SettingController"; -const settingRoutes = Router(); +const settingRoutes = Router(); settingRoutes.get("/settings", SettingController.index); // routes.get("/settings/:settingKey", isAuth, SettingsController.show); +settingRoutes.put( + "/settings/ticket", + isAuth, + SettingController.updateTicketSettings +); + + // change setting key to key in future settingRoutes.put("/settings/:settingKey", isAuth, SettingController.update); + export default settingRoutes; diff --git a/backend/src/server.ts b/backend/src/server.ts index 45082d5..b5002e8 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -10,6 +10,7 @@ import { cacheSize, flushCache, loadTicketsCache } from "./helpers/TicketCache"; import { loadContactsCache } from "./helpers/ContactsCache"; import { loadSchedulesCache } from "./helpers/SchedulingNotifyCache"; import { delRestoreControllFile } from "./helpers/RestoreControll"; +import "./helpers/AutoCloseTickets"; import "./helpers/SchedulingNotifySendMessage"; import axios from "axios"; diff --git a/backend/src/services/SettingServices/UpdateSettingTicket.ts b/backend/src/services/SettingServices/UpdateSettingTicket.ts new file mode 100644 index 0000000..5319c10 --- /dev/null +++ b/backend/src/services/SettingServices/UpdateSettingTicket.ts @@ -0,0 +1,35 @@ +import AppError from "../../errors/AppError"; +import SettingTicket from "../../models/SettingTicket"; + +interface Request { + key: string; + startTime: string; + endTime: string; + value: string; + message: string; +} + +const updateSettingTicket = async ({ + key, + startTime, + endTime, + value, + message +}: Request): Promise => { + try { + const businessHours = await SettingTicket.findOne({ where: { key } }); + + if (!businessHours) { + throw new AppError("ERR_NO_SETTING_FOUND", 404); + } + + await businessHours.update({ startTime, endTime, message, value }); + + return businessHours; + } catch (error: any) { + console.error("===> Error on UpdateSettingService.ts file: \n", error); + throw new AppError(error.message); + } +}; + +export default updateSettingTicket; diff --git a/backend/src/services/TicketServices/ListTicketTimeLife.ts b/backend/src/services/TicketServices/ListTicketTimeLife.ts new file mode 100644 index 0000000..bcaa0c8 --- /dev/null +++ b/backend/src/services/TicketServices/ListTicketTimeLife.ts @@ -0,0 +1,47 @@ + +import { Sequelize, } from "sequelize"; + +const dbConfig = require("../../config/database"); +const sequelize = new Sequelize(dbConfig); +const { QueryTypes } = require('sequelize'); + +import { splitDateTime } from "../../helpers/SplitDateTime"; +import format from 'date-fns/format'; +import ptBR from 'date-fns/locale/pt-BR'; + + +interface Request { + timeseconds: string | number; + status: string; + userId?: string | number; +} + +const ListTicketTimeLife = async ({timeseconds, status, userId }: Request): Promise => { + + let tickets = [] + + let currentDate = format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ptBR }) + + // console.log('------------------> currentDate: ', currentDate) + + if (userId) { + // CONSULTANDO FILAS PELO ID DO USUARIO + tickets = await sequelize.query(`select user.id as user_id, user.name as user_name, t.id as ticket_id from Tickets as t inner join Users as user on + t.userId = user.id and user.name = 'botqueue' and t.status='${status}' and (TIMESTAMPDIFF(SECOND, t.updatedAt, '${currentDate}')) >= ${timeseconds};`, { type: QueryTypes.SELECT }); + + } else { + + // CONSULTANDO FILAS PELO USUARIO + tickets = await sequelize.query(`select id as ticket_id from Tickets where status='${status}' and + (TIMESTAMPDIFF(SECOND, updatedAt, '${currentDate}')) >= ${timeseconds};`, { type: QueryTypes.SELECT }); + + } + + return tickets; +}; + +export default ListTicketTimeLife; + + + + diff --git a/backend/src/services/TicketServices/UpdateTicketService.ts b/backend/src/services/TicketServices/UpdateTicketService.ts index b77a324..9c6ad43 100644 --- a/backend/src/services/TicketServices/UpdateTicketService.ts +++ b/backend/src/services/TicketServices/UpdateTicketService.ts @@ -68,7 +68,8 @@ const UpdateTicketService = async ({ await ticket.reload(); - if (msg.length > 0) { + if (msg?.trim().length > 0) { + setTimeout(async () => { sendWhatsAppMessageSocket(ticket, msg); }, 2000); diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 8b1ef87..b7b0d7a 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -7,9 +7,25 @@ import { copyFolder } from "../../helpers/CopyFolder"; import { removeDir } from "../../helpers/DeleteDirectory"; import path from "path"; -import { format } from "date-fns"; +import { + isHoliday, + isOutBusinessTime, + isWeekend +} from "../../helpers/TicketConfig"; + +import { + format as _format, + isWithinInterval, + parse, + subMinutes, + isSaturday, + isSunday, + parseISO +} from "date-fns"; import ptBR from "date-fns/locale/pt-BR"; +import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz"; + import { Contact as WbotContact, Message as WbotMessage, @@ -69,6 +85,9 @@ import { getSettingValue } from "../../helpers/WhaticketSettings"; import { Op } from "sequelize"; +import SettingTicket from "../../models/SettingTicket"; +import mostRepeatedPhrase from "../../helpers/MostRepeatedPhrase"; + var lst: any[] = getWhatsappIds(); interface Session extends Client { @@ -245,27 +264,6 @@ const verifyQueue = async ( } if (choosenQueue) { - // TEST DEL - // let _ticket = await Ticket.findOne({ - // where: { - // status: { - // [Op.or]: ["open", "pending", "queueChoice"] - // }, - // contactId: contact.id, - // queueId: choosenQueue.id - // } - // }); - - // if (_ticket) { - // await UpdateTicketService({ - // ticketData: { queueId: choosenQueue.id }, - // ticketId: ticket.id - // }); - - // return; - // } - // - // Atualizando o status do ticket para mostrar notificação para o atendente da fila escolhida pelo usuário. De queueChoice para pending if (queues.length > 1 && !botInfo.isOnQueue) { await ticket.update({ status: "pending" }); @@ -306,12 +304,20 @@ const verifyQueue = async ( sendWhatsAppMessageSocket(ticket, body); } else { //test del transfere o atendimento se entrar na ura infinita - let ticket_message = await ShowTicketMessage(ticket.id, false); - if (ticket_message.length > 10) { + const repet: any = await mostRepeatedPhrase(ticket.id); + + if (repet.occurrences > 4) { await UpdateTicketService({ ticketData: { status: "pending", queueId: queues[0].id }, ticketId: ticket.id }); + + await SendWhatsAppMessage({ + body: `Seu atendimento foi transferido para um agente! + `, + ticket, + number: `${contact.number}@c.us` + }); } else { let options = ""; @@ -379,19 +385,15 @@ const botTransferTicket = async ( }); }; -const botSendMessage = ( - ticket: Ticket, - contact: Contact, - wbot: Session, - msg: string -) => { +const botSendMessage = (ticket: Ticket, msg: string) => { const debouncedSentMessage = debounce( async () => { - const sentMessage = await wbot.sendMessage( - `${contact.number}@c.us`, - `${msg}` - ); - verifyMessage(sentMessage, ticket, contact); + //OLD + // const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, `${msg}`); + // verifyMessage(sentMessage, ticket, contact); + + //NEW + await SendWhatsAppMessage({ body: msg, ticket }); }, 3000, ticket.id @@ -400,6 +402,27 @@ const botSendMessage = ( debouncedSentMessage(); }; +// const botSendMessage = ( +// ticket: Ticket, +// contact: Contact, +// wbot: Session, +// msg: string +// ) => { +// const debouncedSentMessage = debounce( +// async () => { +// const sentMessage = await wbot.sendMessage( +// `${contact.number}@c.us`, +// `${msg}` +// ); +// verifyMessage(sentMessage, ticket, contact); +// }, +// 3000, +// ticket.id +// ); + +// debouncedSentMessage(); +// }; + const _clear_lst = () => { console.log("THE lst.length: ", lst.length); @@ -434,14 +457,8 @@ const handleMessage = async (msg: any, wbot: any): Promise => { return; } - // console.log('LIST OF ID MESSAGE lst: ', lst) - - console.log( - "PASSOU.................................FROM: ", - msg.from.split("@")[0], - " | ID: ", - msg.id.id - ); + // console.log('LIST OF ID MESSAGE lst: ', lst) + } if (!isValidMsg(msg)) { @@ -453,6 +470,19 @@ const handleMessage = async (msg: any, wbot: any): Promise => { // let groupContact: Contact | undefined; if (msg.fromMe) { + const ticketExpiration = await SettingTicket.findOne({ + where: { key: "ticketExpiration" } + }); + + if ( + ticketExpiration && + ticketExpiration.value == "enabled" && + ticketExpiration?.message.trim() == msg.body.trim() + ) { + console.log("*********** TICKET EXPIRATION"); + return; + } + // console.log('FROM ME: ', msg.fromMe, ' | /\u200e/.test(msg.body[0]: ', (/\u200e/.test(msg.body[0]))) // messages sent automatically by wbot have a special character in front of it @@ -490,7 +520,6 @@ const handleMessage = async (msg: any, wbot: any): Promise => { msg.from: ${msg.from} msg.to: ${msg.to}\n`); } - // const chat = await msg.getChat(); const chat = wbot.chat; @@ -587,343 +616,61 @@ const handleMessage = async (msg: any, wbot: any): Promise => { //Habilitar esse caso queira usar o bot // const botInfo = await BotIsOnQueue('botqueue') - const botInfo = { isOnQueue: false, botQueueId: 0, userIdBot: 0 }; - - if ( - botInfo.isOnQueue && - !msg.fromMe && - ticket.userId == botInfo.userIdBot - ) { - if (msg.body === "0") { - const queue = await ShowQueueService(ticket.queue.id); - - const greetingMessage = `\u200e${queue.greetingMessage}`; - - let options = ""; - - data_ura.forEach((s, index) => { - options += `*${index + 1}* - ${s.option}\n`; - }); - - botSendMessage( - ticket, - contact, - wbot, - `${greetingMessage}\n\n${options}\n${final_message.msg}` - ); - } else { - // Pega as ultimas 9 opções numericas digitadas pelo cliente em orde DESC - // Consulta apenas mensagens do usuári - - let lastOption = ""; - - let ura_length = data_ura.length; - - let indexAttendant = data_ura.findIndex(u => u.atendente); - - let opt_user_attendant = "-1"; - - if (indexAttendant != -1) { - opt_user_attendant = data_ura[indexAttendant].id; - } - - // - - let ticket_message = await ShowTicketMessage( - ticket.id, - true, - ura_length, - `^[0-${ura_length}}]$` - ); - - if (ticket_message.length > 1) { - lastOption = ticket_message[1].body; - - const queuesWhatsGreetingMessage = await queuesOutBot( - wbot, - botInfo.botQueueId - ); - - const queues = queuesWhatsGreetingMessage.queues; - - if (queues.length > 1) { - const index_opt_user_attendant = ticket_message.findIndex( - q => q.body == opt_user_attendant - ); - const index0 = ticket_message.findIndex(q => q.body == "0"); - - if (index_opt_user_attendant != -1) { - if (index0 > -1 && index0 < index_opt_user_attendant) { - lastOption = ""; - } else { - lastOption = opt_user_attendant; - } - } - } - } - - // - - // - - // È numero - if ( - !Number.isNaN(Number(msg.body.trim())) && - +msg.body >= 0 && - +msg.body <= data_ura.length - ) { - const indexUra = data_ura.findIndex(ura => ura.id == msg.body.trim()); - - if (indexUra != -1) { - if ( - data_ura[indexUra].id != opt_user_attendant && - lastOption != opt_user_attendant - ) { - // test del - let next = true; - - let indexAux = ticket_message.findIndex(e => e.body == "0"); - - let listMessage = null; - - if (indexAux != -1) { - listMessage = ticket_message.slice(0, indexAux); - } else { - listMessage = ticket_message; - } - - let id = ""; - let subUra = null; - - if (listMessage.length > 1) { - id = listMessage[listMessage.length - 1].body; - subUra = data_ura.filter(e => e.id == id)[0]; - - if ( - subUra && - (!subUra.subOptions || subUra.subOptions.length == 0) - ) { - listMessage.pop(); - } - } - - if (listMessage.length > 1) { - id = listMessage[listMessage.length - 1].body; - subUra = data_ura.filter(e => e.id == id)[0]; - - if (subUra.subOptions && subUra.subOptions.length > 0) { - if ( - !Number.isNaN(Number(msg.body.trim())) && - +msg.body >= 0 && - +msg.body <= subUra.subOptions?.length && - subUra.subOptions - ) { - if (subUra.subOptions[+msg.body - 1].responseToClient) { - botSendMessage( - ticket, - contact, - wbot, - `*${subUra.option}*\n\n${ - subUra.subOptions[+msg.body - 1].responseToClient - }` - ); - } else { - botSendMessage( - ticket, - contact, - wbot, - `*${subUra.option}*\n\n${ - subUra.subOptions[+msg.body - 1].subOpt - }` - ); - } - - const queuesWhatsGreetingMessage = await queuesOutBot( - wbot, - botInfo.botQueueId - ); - - const queues = queuesWhatsGreetingMessage.queues; - - if (queues.length > 0) { - await botTransferTicket(queues[0], ticket, contact, wbot); - } else { - console.log("NO QUEUE!"); - } - } else { - let options = ""; - let subOptions: any[] = subUra.subOptions; - - subOptions?.forEach((s, index) => { - options += `*${index + 1}* - ${s.subOpt}\n`; - }); - - botSendMessage( - ticket, - contact, - wbot, - `*${subUra.option}*\n\nDigite um número válido disponível no menu de opções de atendimento abaixo: \n${options}\n\n*0* - Voltar ao menu principal` - ); - } - - next = false; - } - } - - // - if (next) { - if ( - data_ura[indexUra].subOptions && - data_ura[indexUra].subOptions.length > 0 - ) { - let options = ""; - let option = data_ura[indexUra].option; - let subOptions: any[] = data_ura[indexUra].subOptions; - let description = data_ura[indexUra].description; - - subOptions?.forEach((s, index) => { - options += `*${index + 1}* - ${s.subOpt}\n`; - }); - - const body = `\u200e${description}:\n${options}`; - - botSendMessage( - ticket, - contact, - wbot, - `*${option}*\n\n${body}\n\n *0* - Voltar ao menu principal` - ); - } else { - //test del deletar isso (Usar somente na hit) - if (data_ura[indexUra].closeChat) { - const { ticket: res } = await UpdateTicketService({ - ticketData: { - status: "closed", - userId: botInfo.userIdBot - }, - ticketId: ticket.id - }); - - /////////////////////////////// - const whatsapp = await ShowWhatsAppService( - ticket.whatsappId - ); - - const { farewellMessage } = whatsapp; - - if (farewellMessage) { - await SendWhatsAppMessage({ - body: farewellMessage, - ticket: res - }); - } - /////////////////////////////// - } else { - botSendMessage( - ticket, - contact, - wbot, - `${data_ura[indexUra].description}\n\n *0* - Voltar ao menu principal` - ); - } - // - - // botSendMessage(ticket, contact, wbot, `${data_ura[indexUra].description}\n\n *0* - Voltar ao menu principal`) - } - } - } else if (data_ura[indexUra].id == opt_user_attendant) { - const queuesWhatsGreetingMessage = await queuesOutBot( - wbot, - botInfo.botQueueId - ); - - const queues = queuesWhatsGreetingMessage.queues; - - // Se fila for maior que 1 exibi as opções fila para atendimento humano - if (queues.length > 1) { - let options = ""; - - queues.forEach((queue, index) => { - options += `*${index + 1}* - ${queue.name}\n`; - }); - - const body = `\u200eSelecione uma das opções de atendimento abaixo:\n${options}`; - - botSendMessage(ticket, contact, wbot, body); - } // Para situações onde há apenas uma fila com exclusão da fila do bot, já direciona o cliente para essa fila de atendimento humano - else if (queues.length == 1) { - await botTransferTicket(queues[0], ticket, contact, wbot); - - botSendMessage( - ticket, - contact, - wbot, - `${msg_client_transfer.msg}` - ); - } - } else if (lastOption == opt_user_attendant) { - const queuesWhatsGreetingMessage = await queuesOutBot( - wbot, - botInfo.botQueueId - ); - - const queues = queuesWhatsGreetingMessage.queues; - - // É numero - if ( - !Number.isNaN(Number(msg.body.trim())) && - +msg.body >= 0 && - +msg.body <= queues.length - ) { - await botTransferTicket( - queues[+msg.body - 1], - ticket, - contact, - wbot - ); - - botSendMessage( - ticket, - contact, - wbot, - `${msg_client_transfer.msg}` - ); - } else { - botSendMessage( - ticket, - contact, - wbot, - `Digite um número válido disponível no menu de opções de atendimento\n\n*0* - Voltar ao menu principal` - ); - } - } - } - } else { - // É numero - if (!Number.isNaN(Number(msg.body.trim()))) { - botSendMessage( - ticket, - contact, - wbot, - `Opção numérica inválida!\nDigite um dos números mostrados no menu de opções\n\n*0* - Voltar ao menu principal` - ); - } else { - botSendMessage( - ticket, - contact, - wbot, - `Digite um número válido disponível no menu de opções\n\n*0* - Voltar ao menu principal` - ); - } - } - } - } + // const botInfo = { isOnQueue: false, botQueueId: 0, userIdBot: 0 }; if (msg && !msg.fromMe && ticket.status == "pending") { await setMessageAsRead(ticket); } + + let ticketHasQueue = false; + + if (ticket?.queueId) { + ticketHasQueue = true; + } + + if (ticketHasQueue) { + // MESSAGE TO HOLIDAY + const holiday: any = await isHoliday(); + + if (holiday.set) { + if (msg.fromMe && holiday.msg == msg.body) { + console.log("HOLIDAY MESSAGE IGNORED"); + return; + } + + botSendMessage(ticket, holiday.msg); + return; + } + + // MESSAGES TO SATURDAY OR SUNDAY + const weekend: any = await isWeekend(); + + if (weekend.set) { + if (msg.fromMe && weekend.msg == msg.body) { + console.log("WEEKEND MESSAGE IGNORED"); + return; + } + + botSendMessage(ticket, weekend.msg); + return; + } + + // MESSAGE TO BUSINESS TIME + const businessTime = await isOutBusinessTime(); + + if (businessTime.set) { + if (msg.fromMe && businessTime.msg == msg.body) { + console.log("BUSINESS TIME MESSAGE IGNORED"); + return; + } + + botSendMessage(ticket, businessTime.msg); + return; + } + } } catch (err) { Sentry.captureException(err); + console.log("Error handling whatsapp message: Err: ", err); logger.error(`Error handling whatsapp message: Err: ${err}`); } }; diff --git a/frontend/package.json b/frontend/package.json index a077e60..681cd4b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,13 +4,13 @@ "private": true, "dependencies": { "@date-io/date-fns": "^1.3.13", - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", "@material-ui/core": "^4.12.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.56", "@material-ui/pickers": "^3.3.10", - "@mui/material": "^5.3.0", + "@mui/material": "^5.14.4", "@mui/x-data-grid": "^5.3.0", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.0.4", @@ -20,6 +20,7 @@ "dotenv": "^16.0.1", "emoji-mart": "^3.0.1", "formik": "^2.2.0", + "formik-material-ui-pickers": "^1.0.0-alpha.1", "i18next": "^19.8.2", "i18next-browser-languagedetector": "^6.0.1", "js-file-download": "^0.4.12", @@ -30,6 +31,7 @@ "react": "^17.0.2", "react-color": "^2.19.3", "react-csv": "^2.2.2", + "react-datepicker": "^4.16.0", "react-dom": "^17.0.2", "react-modal-image": "^2.5.0", "react-router-dom": "^5.2.0", diff --git a/frontend/src/components/ConfigModal/index.js b/frontend/src/components/ConfigModal/index.js new file mode 100644 index 0000000..c221455 --- /dev/null +++ b/frontend/src/components/ConfigModal/index.js @@ -0,0 +1,449 @@ +import React, { useState, useEffect, } from 'react' +// import * as Yup from 'yup' +import { Formik, Form, Field, } from 'formik' +import { toast } from 'react-toastify' + +import { makeStyles } from '@material-ui/core/styles' +import { green } from '@material-ui/core/colors' + +import { TimePicker, DatePicker } from 'formik-material-ui-pickers' + +import DateFnsUtils from '@date-io/date-fns' + +import ptBrLocale from "date-fns/locale/pt-BR" + + + +import { + MuiPickersUtilsProvider, +} from '@material-ui/pickers' + +import { + Dialog, + DialogContent, + DialogTitle, + Button, + DialogActions, + CircularProgress, + TextField, + Switch, + FormControlLabel, +} from '@material-ui/core' + +import api from '../../services/api' +import { i18n } from '../../translate/i18n' +import toastError from '../../errors/toastError' + +const useStyles = makeStyles((theme) => ({ + root: { + display: 'flex', + flexWrap: 'wrap', + }, + + multFieldLine: { + display: 'flex', + '& > *:not(:last-child)': { + marginRight: theme.spacing(1), + }, + }, + + btnWrapper: { + position: 'relative', + }, + + buttonProgress: { + color: green[500], + position: 'absolute', + top: '50%', + left: '50%', + marginTop: -12, + marginLeft: -12, + }, +})) + +// const SessionSchema = Yup.object().shape({ +// name: Yup.string() +// .min(2, 'Too Short!') +// .max(100, 'Too Long!') +// .required('Required'), +// }) + +const ConfigModal = ({ open, onClose, change }) => { + const classes = useStyles() + const initialState = { + startTimeBus: new Date(), + endTimeBus: new Date(), + messageBus: '', + businessTimeEnable: false, + ticketTimeExpiration: new Date(), + ticketExpirationMsg: '', + ticketExpirationEnable: false, + holidayDate: new Date(), + holidayDateEnable: false, + holidayDateMessage: '', + checkboxSundayValue: false, + checkboxSaturdayValue: false, + weekendMessage: '', + enableWeekendMessage: false + } + + const [config, setConfig] = useState(initialState) + + useEffect(() => { + const fetchSession = async () => { + + try { + const { data } = await api.get('/settings') + + const outBusinessHours = data.config.find((c) => c.key === "outBusinessHours") + const ticketExpiration = data.config.find((c) => c.key === "ticketExpiration") + const saturday = data.config.find((c) => c.key === "saturday") + const sunday = data.config.find((c) => c.key === "sunday") + const weekend = data.config.find((c) => c.key === "weekend") + const holiday = data.config.find((c) => c.key === "holiday") + + setConfig({ + startTimeBus: outBusinessHours.startTime, + endTimeBus: outBusinessHours.endTime, + messageBus: outBusinessHours.message, + businessTimeEnable: outBusinessHours.value === 'enabled' ? true : false, + + ticketTimeExpiration: ticketExpiration.startTime, + ticketExpirationMsg: ticketExpiration.message, + ticketExpirationEnable: ticketExpiration.value === 'enabled' ? true : false, + + checkboxSaturdayValue: saturday.value === 'enabled' ? true : false, + checkboxSundayValue: sunday.value === 'enabled' ? true : false, + weekendMessage: weekend.message, + enableWeekendMessage: weekend.value === 'enabled' ? true : false, + + holidayDate: holiday.startTime, + holidayDateMessage: holiday.message, + holidayDateEnable: holiday.value === 'enabled' ? true : false, + }) + + } catch (err) { + toastError(err) + } + } + fetchSession() + }, [change]) + + const handleSaveConfig = async (values) => { + + values = { + outBusinessHours: { + startTime: values.startTimeBus, + endTime: values.endTimeBus, + message: values.messageBus, + value: values.businessTimeEnable ? 'enabled' : 'disabled' + }, + ticketExpiration: { + startTime: values.ticketTimeExpiration, + message: values.ticketExpirationMsg, + value: values.ticketExpirationEnable ? 'enabled' : 'disabled' + }, + weekend: { + message: values.weekendMessage, + value: values.enableWeekendMessage ? 'enabled' : 'disabled' + }, + saturday:{ + value: values.checkboxSaturdayValue ? 'enabled' : 'disabled' + }, + sunday: { + value: values.checkboxSundayValue ? 'enabled' : 'disabled' + }, + holiday: { + startTime: values.holidayDate, + message: values.holidayDateMessage, + value: values.holidayDateEnable ? 'enabled' : 'disabled' + } + + } + + + try { + + await api.put(`/settings/ticket`, values) + + toast.success('Atualização realizada com sucesso!') + handleClose() + + } catch (err) { + toastError(err) + } + } + + const handleClose = () => { + onClose() + // setConfig(initialState) + } + + return ( +
+ + + Configurações + + + + { + + setTimeout(() => { + handleSaveConfig(values) + actions.setSubmitting(false) + }, 100) + }} + > + {({ values, touched, errors, isSubmitting }) => ( + +
+ + + +
+ + {' '} + + + + } + label={'Ativar/Desativar'} /> +
+ +
+ +
+ + +
+ + + {/* Saturday and Sunday date */} +
+
+ + + +
+ + + } + label={'Ativar/Desativar'} + /> +
+
+ +
+ +
+ + {/* Holiday date */} +
+ + + + + } + label={'Ativar/Desativar'} + /> +
+
+ +
+ + + + + + +
+
+ + + + } + label={'Ativar/Desativar'} + /> +
+
+ +
+ +
+ + + + +
+
+ )} +
+
+
+ ) +} + + + + + +export default React.memo(ConfigModal) diff --git a/frontend/src/hooks/useAuth.js/index.js b/frontend/src/hooks/useAuth.js/index.js index 70ffe21..6e26b05 100644 --- a/frontend/src/hooks/useAuth.js/index.js +++ b/frontend/src/hooks/useAuth.js/index.js @@ -76,7 +76,7 @@ const useAuth = () => { const fetchSession = async () => { try { const { data } = await api.get('/settings') - setSetting(data) + setSetting(data.settings) } catch (err) { toastError(err) } diff --git a/frontend/src/pages/Connections/index.js b/frontend/src/pages/Connections/index.js index a8c5fe4..3e188ee 100644 --- a/frontend/src/pages/Connections/index.js +++ b/frontend/src/pages/Connections/index.js @@ -6,6 +6,9 @@ import openSocket from 'socket.io-client' import { makeStyles } from '@material-ui/core/styles' import { green } from '@material-ui/core/colors' + +import Settings from "@material-ui/icons/Settings"; + import { Button, TableBody, @@ -47,6 +50,7 @@ import toastError from '../../errors/toastError' //-------- import { AuthContext } from '../../context/Auth/AuthContext' import { Can } from '../../components/Can' +import ConfigModal from '../../components/ConfigModal' const useStyles = makeStyles((theme) => ({ mainPaper: { @@ -107,6 +111,7 @@ const Connections = () => { const { whatsApps, loading } = useContext(WhatsAppsContext) const [whatsAppModalOpen, setWhatsAppModalOpen] = useState(false) + const [configModalOpen, setConfigModalOpen] = useState(false) const [qrModalOpen, setQrModalOpen] = useState(false) const [selectedWhatsApp, setSelectedWhatsApp] = useState(null) const [confirmModalOpen, setConfirmModalOpen] = useState(false) @@ -134,7 +139,7 @@ const Connections = () => { const fetchSession = async () => { try { const { data } = await api.get('/settings') - setSettings(data) + setSettings(data.settings) } catch (err) { toastError(err) } @@ -205,6 +210,13 @@ const Connections = () => { setWhatsAppModalOpen(true) } + const handleOpenConfigModal = () => { + setConfigModalOpen(true) + } + + const handleCloseConfigModal = () => { + setConfigModalOpen(false) + } const handleCloseWhatsAppModal = useCallback(() => { setWhatsAppModalOpen(false) setSelectedWhatsApp(null) @@ -307,17 +319,17 @@ const Connections = () => { {(whatsApp.status === 'CONNECTED' || whatsApp.status === 'PAIRING' || whatsApp.status === 'TIMEOUT') && ( - - )} + + )} {whatsApp.status === 'OPENING' && ( { settings.length > 0 && getSettingValue('editURA') && getSettingValue('editURA') === - 'enabled') | - (user.profile === 'master') ? ( + 'enabled') | + (user.profile === 'master') ? ( diff --git a/frontend/src/pages/Queues/index.js b/frontend/src/pages/Queues/index.js index 99b6eab..85144e2 100644 --- a/frontend/src/pages/Queues/index.js +++ b/frontend/src/pages/Queues/index.js @@ -121,7 +121,7 @@ const Queues = () => { const fetchSession = async () => { try { const { data } = await api.get('/settings') - setSettings(data) + setSettings(data.settings) } catch (err) { toastError(err) } diff --git a/frontend/src/pages/Settings/index.js b/frontend/src/pages/Settings/index.js index 459e11d..d11254d 100644 --- a/frontend/src/pages/Settings/index.js +++ b/frontend/src/pages/Settings/index.js @@ -52,7 +52,7 @@ const Settings = () => { const fetchSession = async () => { try { const { data } = await api.get('/settings') - setSettings(data) + setSettings(data.settings) } catch (err) { toastError(err) }