From 1bdd4adaba1d5644a0877ca9a14120da8597721b Mon Sep 17 00:00:00 2001 From: adriano Date: Tue, 8 Nov 2022 17:23:13 -0300 Subject: [PATCH] =?UTF-8?q?Finaliza=C3=A7=C3=A3o=20da=20Implementa=C3=A7ao?= =?UTF-8?q?=20do=20dialogflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 3 + backend/src/controllers/ContactController.ts | 57 +- .../src/controllers/DialogflowController.ts | 85 +++ backend/src/database/index.ts | 3 + .../20221108130505-create-dialogflow.ts | 44 ++ ...20221108130835-add-dialogflow-to-queues.ts | 16 + ...08131003-add-use-dialogflow-to-contacts.ts | 15 + ...221108152518-add-use-queues-to-contacts.ts | 15 + backend/src/helpers/BotIsOnQueue.ts | 89 ++- backend/src/helpers/EndpointQuery.ts | 33 ++ backend/src/helpers/deleteFileFromTMP.ts | 20 + backend/src/models/Contact.ts | 8 + backend/src/models/Dialogflow.ts | 45 ++ backend/src/models/Queue.ts | 12 +- backend/src/routes/contactRoutes.ts | 4 + backend/src/routes/dialogflowRoutes.ts | 20 + backend/src/routes/index.ts | 2 + .../ContactServices/CreateContactService.ts | 9 +- .../ContactServices/GetContactService.ts | 38 ++ .../ToggleUseDialogflowContactService.ts | 34 ++ .../ToggleUseQueuesContactService.ts | 34 ++ .../CreateDialogflowService.ts | 76 +++ .../CreateSessionDialogflow.ts | 33 ++ .../DeleteDialogflowService.ts | 16 + .../ListDialogflowService.ts | 9 + .../DialogflowServices/QueryDialogflow.ts | 64 ++ .../ShowDialogflowService.ts | 15 + .../TestSessionDialogflowService.ts | 70 +++ .../UpdateDialogflowService.ts | 55 ++ .../QueueService/CreateQueueService.ts | 3 + .../QueueService/DeleteQueueService.ts | 3 + .../QueueService/UpdateQueueService.ts | 3 + .../FindOrCreateTicketService.ts | 4 +- .../TicketServices/ShowTicketService.ts | 5 +- .../UserServices/CreateUserService.ts | 6 +- .../UserServices/DeleteUserService.ts | 3 + .../UserServices/UpdateUserService.ts | 4 + .../src/services/WbotServices/BotActions.ts | 15 + .../WbotServices/wbotMessageListener.ts | 545 ++++++++---------- frontend/src/components/ContactModal/index.js | 1 + .../src/components/DialogflowModal/index.js | 285 +++++++++ frontend/src/components/QueueModal/index.js | 59 +- .../components/TicketActionButtons/index.js | 143 ++--- frontend/src/config.js | 16 + frontend/src/hooks/useDialogflows/index.js | 12 + frontend/src/layout/MainListItems.js | 18 +- frontend/src/pages/Dialogflow/index.js | 221 +++++++ frontend/src/pages/Queues/useLoadData.js | 22 + frontend/src/pages/Queues/useSocket.js | 24 + frontend/src/routes/index.js | 3 +- frontend/src/services/socket-io.js | 8 + frontend/src/translate/languages/en.js | 54 ++ frontend/src/translate/languages/es.js | 55 ++ frontend/src/translate/languages/pt.js | 61 +- 54 files changed, 2076 insertions(+), 421 deletions(-) create mode 100644 backend/src/controllers/DialogflowController.ts create mode 100644 backend/src/database/migrations/20221108130505-create-dialogflow.ts create mode 100644 backend/src/database/migrations/20221108130835-add-dialogflow-to-queues.ts create mode 100644 backend/src/database/migrations/20221108131003-add-use-dialogflow-to-contacts.ts create mode 100644 backend/src/database/migrations/20221108152518-add-use-queues-to-contacts.ts create mode 100644 backend/src/helpers/EndpointQuery.ts create mode 100644 backend/src/helpers/deleteFileFromTMP.ts create mode 100644 backend/src/models/Dialogflow.ts create mode 100644 backend/src/routes/dialogflowRoutes.ts create mode 100644 backend/src/services/ContactServices/GetContactService.ts create mode 100644 backend/src/services/ContactServices/ToggleUseDialogflowContactService.ts create mode 100644 backend/src/services/ContactServices/ToggleUseQueuesContactService.ts create mode 100644 backend/src/services/DialogflowServices/CreateDialogflowService.ts create mode 100644 backend/src/services/DialogflowServices/CreateSessionDialogflow.ts create mode 100644 backend/src/services/DialogflowServices/DeleteDialogflowService.ts create mode 100644 backend/src/services/DialogflowServices/ListDialogflowService.ts create mode 100644 backend/src/services/DialogflowServices/QueryDialogflow.ts create mode 100644 backend/src/services/DialogflowServices/ShowDialogflowService.ts create mode 100644 backend/src/services/DialogflowServices/TestSessionDialogflowService.ts create mode 100644 backend/src/services/DialogflowServices/UpdateDialogflowService.ts create mode 100644 backend/src/services/WbotServices/BotActions.ts create mode 100644 frontend/src/components/DialogflowModal/index.js create mode 100644 frontend/src/config.js create mode 100644 frontend/src/hooks/useDialogflows/index.js create mode 100644 frontend/src/pages/Dialogflow/index.js create mode 100644 frontend/src/pages/Queues/useLoadData.js create mode 100644 frontend/src/pages/Queues/useSocket.js create mode 100644 frontend/src/services/socket-io.js diff --git a/backend/package.json b/backend/package.json index 43e4787..d4890c7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,6 +15,9 @@ "author": "", "license": "MIT", "dependencies": { + "@google-cloud/dialogflow": "^4.6.0", + "actions-on-google": "^3.0.0", + "axios": "^0.27.2", "@sentry/node": "^5.29.2", "@types/pino": "^6.3.4", "bcryptjs": "^2.4.3", diff --git a/backend/src/controllers/ContactController.ts b/backend/src/controllers/ContactController.ts index 159d093..b6b4c91 100644 --- a/backend/src/controllers/ContactController.ts +++ b/backend/src/controllers/ContactController.ts @@ -13,10 +13,12 @@ import CheckIsValidContact from "../services/WbotServices/CheckIsValidContact"; import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl"; import AppError from "../errors/AppError"; - import { searchContactCache } from '../helpers/ContactsCache' import { off } from "process"; +import GetContactService from "../services/ContactServices/GetContactService"; +import ToggleUseQueuesContactService from "../services/ContactServices/ToggleUseQueuesContactService"; +import ToggleUseDialogflowContactService from "../services/ContactServices/ToggleUseDialogflowContactService"; type IndexQuery = { @@ -32,9 +34,15 @@ interface ContactData { name: string; number: string; email?: string; + useDialogflow: boolean; extraInfo?: ExtraInfo[]; } +type IndexGetContactQuery = { + name: string; + number: string; +}; + export const index = async (req: Request, res: Response): Promise => { let { searchParam, pageNumber } = req.query as IndexQuery; @@ -68,6 +76,17 @@ export const index = async (req: Request, res: Response): Promise => { return res.json({ contacts, count, hasMore }); }; +export const getContact = async (req: Request, res: Response): Promise => { + const { name, number } = req.body as IndexGetContactQuery; + + const contact = await GetContactService({ + name, + number + }); + + return res.status(200).json(contact); +}; + export const store = async (req: Request, res: Response): Promise => { const newContact: ContactData = req.body; newContact.number = newContact.number.replace("-", "").replace(" ", ""); @@ -94,11 +113,13 @@ export const store = async (req: Request, res: Response): Promise => { let number = validNumber let email = newContact.email let extraInfo = newContact.extraInfo + let useDialogflow = newContact.useDialogflow const contact = await CreateContactService({ name, number, email, + useDialogflow, extraInfo, profilePicUrl }); @@ -173,6 +194,40 @@ export const remove = async ( }; +export const toggleUseQueue = async ( + req: Request, + res: Response +): Promise => { + const { contactId } = req.params; + + const contact = await ToggleUseQueuesContactService({ contactId }); + + const io = getIO(); + io.emit("contact", { + action: "update", + contact + }); + + return res.status(200).json(contact); +}; + +export const toggleUseDialogflow = async ( + req: Request, + res: Response +): Promise => { + const { contactId } = req.params; + + const contact = await ToggleUseDialogflowContactService({ contactId }); + + const io = getIO(); + io.emit("contact", { + action: "update", + contact + }); + + return res.status(200).json(contact); +}; + export const contacsBulkInsertOnQueue = async (req: Request, res: Response): Promise => { diff --git a/backend/src/controllers/DialogflowController.ts b/backend/src/controllers/DialogflowController.ts new file mode 100644 index 0000000..805efa5 --- /dev/null +++ b/backend/src/controllers/DialogflowController.ts @@ -0,0 +1,85 @@ +import { Request, Response } from "express"; +import { getIO } from "../libs/socket"; +import CreateDialogflowService from "../services/DialogflowServices/CreateDialogflowService"; +import DeleteDialogflowService from "../services/DialogflowServices/DeleteDialogflowService"; +import ListDialogflowsService from "../services/DialogflowServices/ListDialogflowService"; +import ShowDialogflowService from "../services/DialogflowServices/ShowDialogflowService"; +import TestSessionDialogflowService from "../services/DialogflowServices/TestSessionDialogflowService"; +import UpdateDialogflowService from "../services/DialogflowServices/UpdateDialogflowService"; + +export const index = async (req: Request, res: Response): Promise => { + const dialogflows = await ListDialogflowsService(); + + return res.status(200).json(dialogflows); +}; + +export const store = async (req: Request, res: Response): Promise => { + const { name, projectName, jsonContent, language } = req.body; + + const dialogflow = await CreateDialogflowService({ name, projectName, jsonContent, language }); + + const io = getIO(); + io.emit("dialogflow", { + action: "update", + dialogflow + }); + + return res.status(200).json(dialogflow); +}; + +export const show = async (req: Request, res: Response): Promise => { + const { dialogflowId } = req.params; + + const dialogflow = await ShowDialogflowService(dialogflowId); + + return res.status(200).json(dialogflow); +}; + +export const update = async ( + req: Request, + res: Response +): Promise => { + const { dialogflowId } = req.params; + const dialogflowData = req.body; + + const dialogflow = await UpdateDialogflowService({dialogflowData, dialogflowId }); + + const io = getIO(); + io.emit("dialogflow", { + action: "update", + dialogflow + }); + + return res.status(201).json(dialogflow); +}; + +export const remove = async ( + req: Request, + res: Response +): Promise => { + const { dialogflowId } = req.params; + + await DeleteDialogflowService(dialogflowId); + + const io = getIO(); + io.emit("dialogflow", { + action: "delete", + dialogflowId: +dialogflowId + }); + + return res.status(200).send(); +}; + +export const testSession = async (req: Request, res: Response): Promise => { + const { projectName, jsonContent, language } = req.body; + + const response = await TestSessionDialogflowService({ projectName, jsonContent, language }); + + const io = getIO(); + io.emit("dialogflow", { + action: "testSession", + response + }); + + return res.status(200).json(response); +}; diff --git a/backend/src/database/index.ts b/backend/src/database/index.ts index 9749831..b06b14c 100644 --- a/backend/src/database/index.ts +++ b/backend/src/database/index.ts @@ -14,6 +14,8 @@ import QuickAnswer from "../models/QuickAnswer"; import SchedulingNotify from "../models/SchedulingNotify"; import StatusChatEnd from "../models/StatusChatEnd"; import UserOnlineTime from "../models/UserOnlineTime"; + +import Dialogflow from "../models/Dialogflow"; // eslint-disable-next-line const dbConfig = require("../config/database"); // import dbConfig from "../config/database"; @@ -36,6 +38,7 @@ const models = [ SchedulingNotify, StatusChatEnd, UserOnlineTime, + Dialogflow, ]; sequelize.addModels(models); diff --git a/backend/src/database/migrations/20221108130505-create-dialogflow.ts b/backend/src/database/migrations/20221108130505-create-dialogflow.ts new file mode 100644 index 0000000..151d1fa --- /dev/null +++ b/backend/src/database/migrations/20221108130505-create-dialogflow.ts @@ -0,0 +1,44 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.createTable("Dialogflows", { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, + projectName: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, + jsonContent: { + type: DataTypes.TEXT, + allowNull: false, + }, + language: { + type: DataTypes.STRING, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false + } + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.dropTable("Dialogflows"); + } +}; diff --git a/backend/src/database/migrations/20221108130835-add-dialogflow-to-queues.ts b/backend/src/database/migrations/20221108130835-add-dialogflow-to-queues.ts new file mode 100644 index 0000000..aa6d9eb --- /dev/null +++ b/backend/src/database/migrations/20221108130835-add-dialogflow-to-queues.ts @@ -0,0 +1,16 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Queues", "dialogflowId", { + type: DataTypes.INTEGER, + references: { model: "Dialogflows", key: "id" }, + onUpdate: "CASCADE", + onDelete: "SET NULL" + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Queues", "dialogflowId"); + } +}; diff --git a/backend/src/database/migrations/20221108131003-add-use-dialogflow-to-contacts.ts b/backend/src/database/migrations/20221108131003-add-use-dialogflow-to-contacts.ts new file mode 100644 index 0000000..6231a55 --- /dev/null +++ b/backend/src/database/migrations/20221108131003-add-use-dialogflow-to-contacts.ts @@ -0,0 +1,15 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Contacts", "useDialogflow", { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Contacts", "useDialogflow"); + } +}; diff --git a/backend/src/database/migrations/20221108152518-add-use-queues-to-contacts.ts b/backend/src/database/migrations/20221108152518-add-use-queues-to-contacts.ts new file mode 100644 index 0000000..e291726 --- /dev/null +++ b/backend/src/database/migrations/20221108152518-add-use-queues-to-contacts.ts @@ -0,0 +1,15 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Contacts", "useQueues", { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Contacts", "useQueues"); + } +}; diff --git a/backend/src/helpers/BotIsOnQueue.ts b/backend/src/helpers/BotIsOnQueue.ts index beb8747..793ee59 100644 --- a/backend/src/helpers/BotIsOnQueue.ts +++ b/backend/src/helpers/BotIsOnQueue.ts @@ -1,37 +1,70 @@ const fsPromises = require("fs/promises"); -const fs = require('fs') +import dir from 'path'; +import fs from 'fs'; +import os from 'os'; -import ListUsersService from "../services/UserServices/ListUsersService" +import ListUsersService from "../services/UserServices/ListUsersService" const _botIsOnQueue = async (botName: string) => { - const { users, count, hasMore } = await ListUsersService({searchParam:`${botName}`,pageNumber:1}); - let botIsOnQueue = false - let userIdBot = null - let queueId = null + const botInfoFile = dir.join(os.tmpdir(), `botInfo.json`); + + console.log('The bot botInfoFile: ', botInfoFile) + + + try { + + if (fs.existsSync(botInfoFile)) { + + console.log('botInfo.json file exists'); - if(users.length > 0){ - - try { - - console.log('----------------- bot queue id: ', Object(users)[0]["queues"][0].id) - queueId = Object(users)[0]["queues"][0].id; - userIdBot = Object(users)[0].id - botIsOnQueue = true - - }catch(err){ - - console.log('O usuário botqueue não está em nenhuma fila err: ',err) - - } - - } - else{ - console.log('Usuário botqueue não existe!') - } - - return { userIdBot: userIdBot, botQueueId: queueId, isOnQueue: botIsOnQueue } + const botInfo = fs.readFileSync(botInfoFile, {encoding:'utf8', flag:'r'}); + + return JSON.parse(botInfo) + } else { + console.log('botInfo.json file not found!'); + } + + + } catch (error) { + console.log('There was an error on try to read the botInfo.json file: ',error) + } + + console.log('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ') + + const { users, count, hasMore } = await ListUsersService({ searchParam: `${botName}`, pageNumber: 1 }); + let botIsOnQueue = false + let userIdBot = null + let queueId = null + + if (users.length > 0) { + + try { + + console.log('----------------- bot queue id: ', Object(users)[0]["queues"][0].id) + queueId = Object(users)[0]["queues"][0].id; + userIdBot = Object(users)[0].id + botIsOnQueue = true + + fs.writeFileSync(botInfoFile, JSON.stringify({ userIdBot: userIdBot, botQueueId: queueId, isOnQueue: botIsOnQueue }), "utf8"); + + } catch (err) { + + console.log('O usuário botqueue não está em nenhuma fila err: ', err) + + } + + } + else { + console.log('Usuário botqueue não existe!') + + fs.writeFileSync(botInfoFile, JSON.stringify({ isOnQueue: false, botQueueId: 0, userIdBot: 0 }), "utf8"); + } - export default _botIsOnQueue; \ No newline at end of file + return { userIdBot: userIdBot, botQueueId: queueId, isOnQueue: botIsOnQueue } + +} + +export default _botIsOnQueue; \ No newline at end of file diff --git a/backend/src/helpers/EndpointQuery.ts b/backend/src/helpers/EndpointQuery.ts new file mode 100644 index 0000000..d2fea36 --- /dev/null +++ b/backend/src/helpers/EndpointQuery.ts @@ -0,0 +1,33 @@ +const fsPromises = require("fs/promises"); +const fs = require('fs') +import axios from 'axios'; +import * as https from "https"; + +const endPointQuery = async (url: string) => { + + let response:any = null + + try { + + const httpsAgent = new https.Agent({ rejectUnauthorized: false, }); + + // const url = 'https://sos.espacolaser.com.br/api/whatsapp/ticket/R32656' + + response = await axios.get(url, { + httpsAgent, + headers: { + 'x-access-token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOnsiaWQiOjEsInJvbGUiOiJjbGllbnQiLCJob3N0Ijoic29zLmVzcGFjb2xhc2VyLmNvbS5iciIsInRlbmFudCI6ImVzcGFjb2xhc2VyIiwibmFtZSI6IlNFTlNSLklUIiwiY29tcGFueSI6eyJpZCI6NDR9fSwiZGF0ZSI6MTY2MTI2MjY0MywiaWF0IjoxNjYxMjYyNjQzLCJleHAiOjE3NDc2NjI2NDN9.zf91OmRs4_C7B8OlVpLLrQMiRBYc7edP4qAdH_hqxpk', + 'Origin': 'espacolaser' + } + }); + console.log(`TEST URL CLIENT GET ROUTE: ${url} | STATUS CODE: ${response.status}`); + + } catch (error) { + console.error(error); + } + + return response + + } + + export default endPointQuery; \ No newline at end of file diff --git a/backend/src/helpers/deleteFileFromTMP.ts b/backend/src/helpers/deleteFileFromTMP.ts new file mode 100644 index 0000000..4673e23 --- /dev/null +++ b/backend/src/helpers/deleteFileFromTMP.ts @@ -0,0 +1,20 @@ + +import dir from 'path'; +import fs from 'fs'; +import os from 'os'; + +const deleteFileFromTMP = (name_ext: string) =>{ + + const botInfoFile = dir.join(os.tmpdir(), name_ext); + + try { + + fs.unlinkSync(botInfoFile); + console.log(`${name_ext} file deleted!`); + + } catch (error) { + console.log(`Can't delete ${name_ext} file: `,error) + } +} + +export default deleteFileFromTMP; \ No newline at end of file diff --git a/backend/src/models/Contact.ts b/backend/src/models/Contact.ts index d7c4c93..d2c399e 100644 --- a/backend/src/models/Contact.ts +++ b/backend/src/models/Contact.ts @@ -41,6 +41,14 @@ class Contact extends Model { @Column isGroup: boolean; + @Default(true) + @Column + useQueues: boolean; + + @Default(true) + @Column + useDialogflow: boolean; + @CreatedAt createdAt: Date; diff --git a/backend/src/models/Dialogflow.ts b/backend/src/models/Dialogflow.ts new file mode 100644 index 0000000..e33f0a7 --- /dev/null +++ b/backend/src/models/Dialogflow.ts @@ -0,0 +1,45 @@ +import { + Table, + Column, + CreatedAt, + UpdatedAt, + Model, + DataType, + PrimaryKey, + HasMany, + AutoIncrement +} from "sequelize-typescript"; +import Queue from "./Queue"; + +@Table +class Dialogflow extends Model { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @Column(DataType.TEXT) + name: string; + + @Column(DataType.TEXT) + projectName: string; + + @Column(DataType.TEXT) + jsonContent: string; + + @Column(DataType.TEXT) + language: string; + + @CreatedAt + @Column(DataType.DATE(6)) + createdAt: Date; + + @UpdatedAt + @Column(DataType.DATE(6)) + updatedAt: Date; + + @HasMany(() => Queue) + queues: Queue[] +} + +export default Dialogflow; diff --git a/backend/src/models/Queue.ts b/backend/src/models/Queue.ts index c5c06d9..e7b0564 100644 --- a/backend/src/models/Queue.ts +++ b/backend/src/models/Queue.ts @@ -8,13 +8,16 @@ import { AutoIncrement, AllowNull, Unique, - BelongsToMany + BelongsToMany, + BelongsTo, + ForeignKey } from "sequelize-typescript"; import User from "./User"; import UserQueue from "./UserQueue"; import Whatsapp from "./Whatsapp"; import WhatsappQueue from "./WhatsappQueue"; +import Dialogflow from "./Dialogflow"; @Table class Queue extends Model { @@ -36,6 +39,13 @@ class Queue extends Model { @Column greetingMessage: string; + @ForeignKey(() => Dialogflow) + @Column + dialogflowId: number; + + @BelongsTo(() => Dialogflow) + dialogflow: Dialogflow; + @CreatedAt createdAt: Date; diff --git a/backend/src/routes/contactRoutes.ts b/backend/src/routes/contactRoutes.ts index 158159c..6a328e9 100644 --- a/backend/src/routes/contactRoutes.ts +++ b/backend/src/routes/contactRoutes.ts @@ -18,6 +18,10 @@ contactRoutes.post("/contacts", isAuth, ContactController.store); contactRoutes.put("/contacts/:contactId", isAuth, ContactController.update); +contactRoutes.put("/contacts/toggleUseQueues/:contactId", isAuth, ContactController.toggleUseQueue); + +contactRoutes.put("/contacts/toggleUseDialogflow/:contactId", isAuth, ContactController.toggleUseDialogflow); + contactRoutes.delete("/contacts/:contactId", isAuth, ContactController.remove); export default contactRoutes; diff --git a/backend/src/routes/dialogflowRoutes.ts b/backend/src/routes/dialogflowRoutes.ts new file mode 100644 index 0000000..086e372 --- /dev/null +++ b/backend/src/routes/dialogflowRoutes.ts @@ -0,0 +1,20 @@ +import { Router } from "express"; +import isAuth from "../middleware/isAuth"; + +import * as DialogflowController from "../controllers/DialogflowController"; + +const dialogflowRoutes = Router(); + +dialogflowRoutes.get("/dialogflow", isAuth, DialogflowController.index); + +dialogflowRoutes.post("/dialogflow", isAuth, DialogflowController.store); + +dialogflowRoutes.get("/dialogflow/:dialogflowId", isAuth, DialogflowController.show); + +dialogflowRoutes.put("/dialogflow/:dialogflowId", isAuth, DialogflowController.update); + +dialogflowRoutes.delete("/dialogflow/:dialogflowId", isAuth, DialogflowController.remove); + +dialogflowRoutes.post("/dialogflow/testsession", isAuth, DialogflowController.testSession); + +export default dialogflowRoutes; \ No newline at end of file diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index e52ded1..3093b52 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -13,6 +13,7 @@ import quickAnswerRoutes from "./quickAnswerRoutes"; import reportRoutes from "./reportRoutes"; import schedulingNotifiyRoutes from "./SchedulingNotifyRoutes"; import statusChatEndRoutes from "./statusChatEndRoutes"; +import dialogflowRoutes from "./dialogflowRoutes"; const routes = Router(); @@ -31,5 +32,6 @@ routes.use(quickAnswerRoutes); routes.use(schedulingNotifiyRoutes); routes.use(reportRoutes); routes.use(statusChatEndRoutes); +routes.use(dialogflowRoutes); export default routes; diff --git a/backend/src/services/ContactServices/CreateContactService.ts b/backend/src/services/ContactServices/CreateContactService.ts index 172b1a6..c4daa2f 100644 --- a/backend/src/services/ContactServices/CreateContactService.ts +++ b/backend/src/services/ContactServices/CreateContactService.ts @@ -1,7 +1,8 @@ import AppError from "../../errors/AppError"; import Contact from "../../models/Contact"; -import { createOrUpdateContactCache } from '../../helpers/ContactsCache' +import { createOrUpdateContactCache } from '../../helpers/ContactsCache' + interface ExtraInfo { name: string; @@ -12,6 +13,7 @@ interface Request { name: string; number: string; email?: string; + useDialogflow?: boolean; profilePicUrl?: string; extraInfo?: ExtraInfo[]; } @@ -20,6 +22,7 @@ const CreateContactService = async ({ name, number, email = "", + useDialogflow, extraInfo = [] }: Request): Promise => { const numberExists = await Contact.findOne({ @@ -35,6 +38,7 @@ const CreateContactService = async ({ name, number, email, + useDialogflow, extraInfo }, { @@ -45,8 +49,7 @@ const CreateContactService = async ({ // TEST DEL await createOrUpdateContactCache(`contact:${contact.id}`, {id: contact.id, name, number, profilePicUrl:'', isGroup:'false', extraInfo, email }) - // - + // return contact; }; diff --git a/backend/src/services/ContactServices/GetContactService.ts b/backend/src/services/ContactServices/GetContactService.ts new file mode 100644 index 0000000..09121f3 --- /dev/null +++ b/backend/src/services/ContactServices/GetContactService.ts @@ -0,0 +1,38 @@ +import AppError from "../../errors/AppError"; +import Contact from "../../models/Contact"; +import CreateContactService from "./CreateContactService"; + +interface ExtraInfo { + name: string; + value: string; +} + +interface Request { + name: string; + number: string; + email?: string; + profilePicUrl?: string; + extraInfo?: ExtraInfo[]; +} + +const GetContactService = async ({ name, number }: Request): Promise => { + const numberExists = await Contact.findOne({ + where: { number } + }); + + if (!numberExists) { + const contact = await CreateContactService({ + name, + number, + }) + + if (contact == null) + throw new AppError("CONTACT_NOT_FOUND") + else + return contact + } + + return numberExists +}; + +export default GetContactService; \ No newline at end of file diff --git a/backend/src/services/ContactServices/ToggleUseDialogflowContactService.ts b/backend/src/services/ContactServices/ToggleUseDialogflowContactService.ts new file mode 100644 index 0000000..0034617 --- /dev/null +++ b/backend/src/services/ContactServices/ToggleUseDialogflowContactService.ts @@ -0,0 +1,34 @@ +import AppError from "../../errors/AppError"; +import Contact from "../../models/Contact"; + +interface Request { + contactId: string; +} + +const ToggleUseDialogflowContactService = async ({ + contactId +}: Request): Promise => { + const contact = await Contact.findOne({ + where: { id: contactId }, + attributes: ["id", "useDialogflow"] + }); + + if (!contact) { + throw new AppError("ERR_NO_CONTACT_FOUND", 404); + } + + const useDialogflow = contact.useDialogflow ? false : true; + + await contact.update({ + useDialogflow + }); + + await contact.reload({ + attributes: ["id", "name", "number", "email", "profilePicUrl", "useQueues", "useDialogflow"], + include: ["extraInfo"] + }); + + return contact; +}; + +export default ToggleUseDialogflowContactService; diff --git a/backend/src/services/ContactServices/ToggleUseQueuesContactService.ts b/backend/src/services/ContactServices/ToggleUseQueuesContactService.ts new file mode 100644 index 0000000..b7d5127 --- /dev/null +++ b/backend/src/services/ContactServices/ToggleUseQueuesContactService.ts @@ -0,0 +1,34 @@ +import AppError from "../../errors/AppError"; +import Contact from "../../models/Contact"; + +interface Request { + contactId: string; +} + +const ToggleUseQueuesContactService = async ({ + contactId +}: Request): Promise => { + const contact = await Contact.findOne({ + where: { id: contactId }, + attributes: ["id", "useQueues"] + }); + + if (!contact) { + throw new AppError("ERR_NO_CONTACT_FOUND", 404); + } + + const useQueues = contact.useQueues ? false : true; + + await contact.update({ + useQueues + }); + + await contact.reload({ + attributes: ["id", "name", "number", "email", "profilePicUrl", "useQueues", "useDialogflow"], + include: ["extraInfo"] + }); + + return contact; +}; + +export default ToggleUseQueuesContactService; diff --git a/backend/src/services/DialogflowServices/CreateDialogflowService.ts b/backend/src/services/DialogflowServices/CreateDialogflowService.ts new file mode 100644 index 0000000..6150bb8 --- /dev/null +++ b/backend/src/services/DialogflowServices/CreateDialogflowService.ts @@ -0,0 +1,76 @@ +import * as Yup from "yup"; + +import AppError from "../../errors/AppError"; +import Dialogflow from "../../models/Dialogflow"; + + +interface Request { + name: string; + projectName: string; + jsonContent: string; + language: string; +} + +const CreateDialogflowService = async ({ + name, + projectName, + jsonContent, + language +}: Request): Promise => { + const schema = Yup.object().shape({ + name: Yup.string() + .required() + .min(2) + .test( + "Check-name", + "This DialogFlow name is already used.", + async value => { + if (!value) return false; + const nameExists = await Dialogflow.findOne({ + where: { name: value } + }); + return !nameExists; + } + ), + projectName: Yup.string() + .required() + .min(2) + .test( + "Check-name", + "This DialogFlow projectName is already used.", + async value => { + if (!value) return false; + const nameExists = await Dialogflow.findOne({ + where: { projectName: value } + }); + return !nameExists; + } + ), + jsonContent: Yup.string() + .required() + , + language: Yup.string() + .required() + .min(2) + }); + + try { + await schema.validate({ name, projectName, jsonContent, language }); + } catch (err) { + throw new AppError(err.message); + } + + + const dialogflow = await Dialogflow.create( + { + name, + projectName, + jsonContent, + language + } + ); + + return dialogflow; +}; + +export default CreateDialogflowService; diff --git a/backend/src/services/DialogflowServices/CreateSessionDialogflow.ts b/backend/src/services/DialogflowServices/CreateSessionDialogflow.ts new file mode 100644 index 0000000..2457b84 --- /dev/null +++ b/backend/src/services/DialogflowServices/CreateSessionDialogflow.ts @@ -0,0 +1,33 @@ +import { SessionsClient } from "@google-cloud/dialogflow"; +import Dialogflow from "../../models/Dialogflow"; +import dir from 'path'; +import fs from 'fs'; +import os from 'os'; +import { logger } from "../../utils/logger"; + +const sessions : Map = new Map(); + +const createDialogflowSession = async (id:number, projectName:string, jsonContent:string) : Promise => { + if(sessions.has(id)) { + return sessions.get(id); + } + + const keyFilename = dir.join(os.tmpdir(), `whaticket_${id}.json`); + + console.log('keyFilename: ',keyFilename) + + logger.info(`Openig new dialogflow session #${projectName} in '${keyFilename}'`) + + await fs.writeFileSync(keyFilename, jsonContent); + const session = new SessionsClient({ keyFilename }); + + sessions.set(id, session); + + return session; +} + +const createDialogflowSessionWithModel = async (model: Dialogflow) : Promise => { + return createDialogflowSession(model.id, model.projectName, model.jsonContent); +} + +export { createDialogflowSession, createDialogflowSessionWithModel }; \ No newline at end of file diff --git a/backend/src/services/DialogflowServices/DeleteDialogflowService.ts b/backend/src/services/DialogflowServices/DeleteDialogflowService.ts new file mode 100644 index 0000000..0536186 --- /dev/null +++ b/backend/src/services/DialogflowServices/DeleteDialogflowService.ts @@ -0,0 +1,16 @@ +import Dialogflow from "../../models/Dialogflow"; +import AppError from "../../errors/AppError"; + +const DeleteDialogflowService = async (id: string): Promise => { + const dialogflow = await Dialogflow.findOne({ + where: { id } + }); + + if (!dialogflow) { + throw new AppError("ERR_NO_DIALOG_FOUND", 404); + } + + await dialogflow.destroy(); +}; + +export default DeleteDialogflowService; diff --git a/backend/src/services/DialogflowServices/ListDialogflowService.ts b/backend/src/services/DialogflowServices/ListDialogflowService.ts new file mode 100644 index 0000000..982c16e --- /dev/null +++ b/backend/src/services/DialogflowServices/ListDialogflowService.ts @@ -0,0 +1,9 @@ +import DialogFLow from "../../models/Dialogflow"; + +const ListDialogflowService = async (): Promise => { + const dialogFLows = await DialogFLow.findAll(); + + return dialogFLows; +}; + +export default ListDialogflowService; diff --git a/backend/src/services/DialogflowServices/QueryDialogflow.ts b/backend/src/services/DialogflowServices/QueryDialogflow.ts new file mode 100644 index 0000000..8f42796 --- /dev/null +++ b/backend/src/services/DialogflowServices/QueryDialogflow.ts @@ -0,0 +1,64 @@ +import * as Sentry from "@sentry/node"; +import { SessionsClient } from "@google-cloud/dialogflow"; +import { logger } from "../../utils/logger"; + +async function detectIntent( + sessionClient:SessionsClient, + projectId:string, + sessionId:string, + query:string, + languageCode:string +) { + const sessionPath = sessionClient.projectAgentSessionPath( + projectId, + sessionId + ); + + const request = { + session: sessionPath, + queryInput: { + text: { + text: query, + languageCode: languageCode, + }, + }, + }; + + const responses = await sessionClient.detectIntent(request); + return responses[0]; +} + +async function queryDialogFlow( + sessionClient:SessionsClient, + projectId:string, + sessionId:string, + query:string, + languageCode:string +) : Promise { + let intentResponse; + + try { + intentResponse = await detectIntent( + sessionClient, + projectId, + sessionId, + query, + languageCode + ); + + const responses = intentResponse?.queryResult?.fulfillmentMessages; + + if (responses?.length === 0) { + return null; + } else { + return responses; + } + } catch (error) { + Sentry.captureException(error); + logger.error(`Error handling whatsapp message: Err: ${error}`); + } + + return null; +} + +export {queryDialogFlow} \ No newline at end of file diff --git a/backend/src/services/DialogflowServices/ShowDialogflowService.ts b/backend/src/services/DialogflowServices/ShowDialogflowService.ts new file mode 100644 index 0000000..f02181a --- /dev/null +++ b/backend/src/services/DialogflowServices/ShowDialogflowService.ts @@ -0,0 +1,15 @@ +import Dialogflow from "../../models/Dialogflow"; +import AppError from "../../errors/AppError"; + + +const ShowDialogflowService = async (id: string | number): Promise => { + const dialogflow = await Dialogflow.findByPk(id); + + if (!dialogflow) { + throw new AppError("ERR_NO_DIALOG_FOUND", 404); + } + + return dialogflow; +}; + +export default ShowDialogflowService; diff --git a/backend/src/services/DialogflowServices/TestSessionDialogflowService.ts b/backend/src/services/DialogflowServices/TestSessionDialogflowService.ts new file mode 100644 index 0000000..06e54ef --- /dev/null +++ b/backend/src/services/DialogflowServices/TestSessionDialogflowService.ts @@ -0,0 +1,70 @@ +import * as Yup from "yup"; + +import AppError from "../../errors/AppError"; + +import { queryDialogFlow } from "./QueryDialogflow"; +import { createDialogflowSession } from "./CreateSessionDialogflow"; + + +interface Request { + projectName: string; + jsonContent: string; + language: string; +} + +interface Response { + messages: string[]; +} + + +const TestDialogflowSession = async ({ + projectName, + jsonContent, + language +}: Request): Promise => { + const schema = Yup.object().shape({ + projectName: Yup.string() + .required() + .min(2), + jsonContent: Yup.string() + .required(), + language: Yup.string() + .required() + .min(2) + }); + + try { + await schema.validate({ projectName, jsonContent, language }); + } catch (err) { + throw new AppError(err.message); + } + + const session = await createDialogflowSession(999, projectName, jsonContent); + + if (!session) { + throw new AppError("ERR_TEST_SESSION_DIALOG", 400); + } + + let dialogFlowReply = await queryDialogFlow( + session, + projectName, + "TestSeesion", + "Ola", + language, + ); + + await session.close(); + + if (!dialogFlowReply) { + throw new AppError("ERR_TEST_REPLY_DIALOG", 400); + } + + const messages = []; + for (let message of dialogFlowReply) { + messages.push(message.text.text[0]); + } + + return { messages }; +} + +export default TestDialogflowSession; diff --git a/backend/src/services/DialogflowServices/UpdateDialogflowService.ts b/backend/src/services/DialogflowServices/UpdateDialogflowService.ts new file mode 100644 index 0000000..476054b --- /dev/null +++ b/backend/src/services/DialogflowServices/UpdateDialogflowService.ts @@ -0,0 +1,55 @@ +import * as Yup from "yup"; + +import AppError from "../../errors/AppError"; +import Dialogflow from "../../models/Dialogflow"; +import ShowDialogflowService from "./ShowDialogflowService"; + +interface DialogflowData { + name?: string; + projectName?: string; + jsonContent?: string; + language?: string; +} + +interface Request { + dialogflowData: DialogflowData; + dialogflowId: string; +} + +const UpdateDialogflowService = async ({ + dialogflowData, + dialogflowId +}: Request): Promise => { + const schema = Yup.object().shape({ + name: Yup.string().min(2), + projectName: Yup.string().min(2), + jsonContent: Yup.string().min(2), + language: Yup.string().min(2) + }); + + const { + name, + projectName, + jsonContent, + language + } = dialogflowData; + + try { + await schema.validate({ name, projectName, jsonContent, language }); + } catch (err) { + throw new AppError(err.message); + } + + const dialogflow = await ShowDialogflowService(dialogflowId); + + await dialogflow.update({ + name, + projectName, + jsonContent, + language + }); + + return dialogflow; +}; + +export default UpdateDialogflowService; diff --git a/backend/src/services/QueueService/CreateQueueService.ts b/backend/src/services/QueueService/CreateQueueService.ts index 57881e1..d06773b 100644 --- a/backend/src/services/QueueService/CreateQueueService.ts +++ b/backend/src/services/QueueService/CreateQueueService.ts @@ -1,5 +1,6 @@ import * as Yup from "yup"; import AppError from "../../errors/AppError"; +import deleteFileFromTMP from "../../helpers/deleteFileFromTMP"; import Queue from "../../models/Queue"; interface QueueData { @@ -61,6 +62,8 @@ const CreateQueueService = async (queueData: QueueData): Promise => { const queue = await Queue.create(queueData); + deleteFileFromTMP(`botInfo.json`) + return queue; }; diff --git a/backend/src/services/QueueService/DeleteQueueService.ts b/backend/src/services/QueueService/DeleteQueueService.ts index 59201d7..b4e0458 100644 --- a/backend/src/services/QueueService/DeleteQueueService.ts +++ b/backend/src/services/QueueService/DeleteQueueService.ts @@ -5,6 +5,7 @@ import UserQueue from "../../models/UserQueue"; import ListTicketsServiceCache from "../TicketServices/ListTicketServiceCache"; import { deleteTicketsFieldsCache } from '../../helpers/TicketCache' +import deleteFileFromTMP from "../../helpers/deleteFileFromTMP"; const DeleteQueueService = async (queueId: number | string): Promise => { @@ -29,6 +30,8 @@ const DeleteQueueService = async (queueId: number | string): Promise => { } await queue.destroy(); + + deleteFileFromTMP(`botInfo.json`) }; export default DeleteQueueService; diff --git a/backend/src/services/QueueService/UpdateQueueService.ts b/backend/src/services/QueueService/UpdateQueueService.ts index 8aa2a23..ad95f87 100644 --- a/backend/src/services/QueueService/UpdateQueueService.ts +++ b/backend/src/services/QueueService/UpdateQueueService.ts @@ -1,6 +1,7 @@ import { Op } from "sequelize"; import * as Yup from "yup"; import AppError from "../../errors/AppError"; +import deleteFileFromTMP from "../../helpers/deleteFileFromTMP"; import Queue from "../../models/Queue"; import ShowQueueService from "./ShowQueueService"; @@ -67,6 +68,8 @@ const UpdateQueueService = async ( await queue.update(queueData); + deleteFileFromTMP(`botInfo.json`) + return queue; }; diff --git a/backend/src/services/TicketServices/FindOrCreateTicketService.ts b/backend/src/services/TicketServices/FindOrCreateTicketService.ts index 94e7d9b..6d011e5 100644 --- a/backend/src/services/TicketServices/FindOrCreateTicketService.ts +++ b/backend/src/services/TicketServices/FindOrCreateTicketService.ts @@ -26,8 +26,8 @@ const FindOrCreateTicketService = async ( //Habilitar esse caso queira usar o bot - // const botInfo = await BotIsOnQueue('botqueue') - const botInfo = { isOnQueue: false } + const botInfo = await BotIsOnQueue('botqueue') + // const botInfo = { isOnQueue: false } diff --git a/backend/src/services/TicketServices/ShowTicketService.ts b/backend/src/services/TicketServices/ShowTicketService.ts index 5efab0c..c6bf50f 100644 --- a/backend/src/services/TicketServices/ShowTicketService.ts +++ b/backend/src/services/TicketServices/ShowTicketService.ts @@ -10,7 +10,7 @@ const ShowTicketService = async (id: string | number): Promise => { { model: Contact, as: "contact", - attributes: ["id", "name", "number", "profilePicUrl"], + attributes: ["id", "name", "number", "profilePicUrl", "useDialogflow", "useQueues"], include: ["extraInfo"] }, { @@ -21,7 +21,8 @@ const ShowTicketService = async (id: string | number): Promise => { { model: Queue, as: "queue", - attributes: ["id", "name", "color"] + attributes: ["id", "name", "color"], + include: ["dialogflow"] } ] }); diff --git a/backend/src/services/UserServices/CreateUserService.ts b/backend/src/services/UserServices/CreateUserService.ts index fd31b80..ae3f7b8 100644 --- a/backend/src/services/UserServices/CreateUserService.ts +++ b/backend/src/services/UserServices/CreateUserService.ts @@ -2,7 +2,9 @@ import * as Yup from "yup"; import AppError from "../../errors/AppError"; import { SerializeUser } from "../../helpers/SerializeUser"; -import User from "../../models/User"; +import User from "../../models/User"; + +import deleteFileFromTMP from "../../helpers/deleteFileFromTMP"; interface Request { email: string; @@ -78,6 +80,8 @@ const CreateUserService = async ({ const serializedUser = SerializeUser(user); + deleteFileFromTMP(`botInfo.json`) + return serializedUser; }; diff --git a/backend/src/services/UserServices/DeleteUserService.ts b/backend/src/services/UserServices/DeleteUserService.ts index ef454e3..e702e56 100644 --- a/backend/src/services/UserServices/DeleteUserService.ts +++ b/backend/src/services/UserServices/DeleteUserService.ts @@ -3,6 +3,7 @@ import AppError from "../../errors/AppError"; import Ticket from "../../models/Ticket"; import UpdateDeletedUserOpenTicketsStatus from "../../helpers/UpdateDeletedUserOpenTicketsStatus"; +import deleteFileFromTMP from "../../helpers/deleteFileFromTMP"; const DeleteUserService = async (id: string | number): Promise => { const user = await User.findOne({ @@ -24,6 +25,8 @@ const DeleteUserService = async (id: string | number): Promise => { await user.destroy(); + deleteFileFromTMP(`botInfo.json`) + }; export default DeleteUserService; diff --git a/backend/src/services/UserServices/UpdateUserService.ts b/backend/src/services/UserServices/UpdateUserService.ts index 6d0638c..0a116be 100644 --- a/backend/src/services/UserServices/UpdateUserService.ts +++ b/backend/src/services/UserServices/UpdateUserService.ts @@ -3,6 +3,8 @@ import * as Yup from "yup"; import AppError from "../../errors/AppError"; import ShowUserService from "./ShowUserService"; +import deleteFileFromTMP from "../../helpers/deleteFileFromTMP"; + interface UserData { email?: string; password?: string; @@ -67,6 +69,8 @@ const UpdateUserService = async ({ queues: user.queues }; + deleteFileFromTMP(`botInfo.json`) + return serializedUser; }; diff --git a/backend/src/services/WbotServices/BotActions.ts b/backend/src/services/WbotServices/BotActions.ts new file mode 100644 index 0000000..3c38400 --- /dev/null +++ b/backend/src/services/WbotServices/BotActions.ts @@ -0,0 +1,15 @@ + + const data:any[] = [ + { + "id":"1", + "action":"transfer_to_attendant", + }, + { + "id":"2", + "action":"request_endpoint", + }, + + ] + + + export default data; \ No newline at end of file diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 18fe020..6224f9e 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -16,7 +16,9 @@ import { Contact as WbotContact, Message as WbotMessage, MessageAck, - Client + Client, + Chat, + MessageMedia } from "whatsapp-web.js"; import Contact from "../../models/Contact"; @@ -35,7 +37,7 @@ import { date } from "faker"; import ShowQueueService from "../QueueService/ShowQueueService"; import ShowTicketMessage from "../TicketServices/ShowTicketMessage" -import BotIsOnQueue from "../../helpers/BotIsOnQueue" +import BotIsOnQueue from "../../helpers/BotIsOnQueue" import Queue from "../../models/Queue"; import fs from 'fs'; @@ -44,16 +46,22 @@ import { StartWhatsAppSession } from "../../services/WbotServices/StartWhatsAppS import { removeWbot } from '../../libs/wbot' import { restartWhatsSession } from "../../helpers/RestartWhatsSession"; -// test del + import data_ura from './ura' import msg_client_transfer from './ura_msg_transfer' import final_message from "./ura_final_message"; import SendWhatsAppMessage from "./SendWhatsAppMessage"; import Whatsapp from "../../models/Whatsapp"; import { splitDateTime } from "../../helpers/SplitDateTime"; -// + +import { queryDialogFlow } from "../DialogflowServices/QueryDialogflow"; +import { createDialogflowSessionWithModel } from "../DialogflowServices/CreateSessionDialogflow"; +import bot_actions from './BotActions' +import ShowTicketService from "../TicketServices/ShowTicketService"; import { updateTicketCacheByTicketId } from '../../helpers/TicketCache' + +import endPointQuery from "../../helpers/EndpointQuery"; @@ -171,6 +179,171 @@ const verifyMessage = async ( }; +async function sendDelayedMessages(wbot: Session, ticket: Ticket, contact: Contact, message: string) { + const body = message.replace(/\\n/g, '\n'); + + if (body.search('dialog_actions') != -1) { + + let msgAction = botMsgActions(body) + + console.log('gggggggggggggggggggggggggggggggggg msgAction: ', msgAction) + + if (msgAction.actions[0] == 'request_endpoint') { + + const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, msgAction.msgBody); + await verifyMessage(sentMessage, ticket, contact); + await new Promise(f => setTimeout(f, 1000)); + + // const url = 'https://sos.espacolaser.com.br/api/whatsapps/ticket/R32656' + const endPointResponse = await endPointQuery(msgAction.actions[1]) + + + if (endPointResponse) { + + const response = Object.entries(endPointResponse.data); + let msg_endpoint_response = '' + for (let i = 0; i < response.length; i++) { + + msg_endpoint_response += `*${response[i][0]}*: ${response[i][1]}\n` + + } + + + + if (endPointResponse.data.status == 'EM ATENDIMENTO') { + + // const msg = await wbot.sendMessage(`${contact.number}@c.us`, `Seu chamado está em atendimento pelo analista ${endPointResponse.data.tecnico} + Última informação do CHAT + // Verificar pelo “ID do solicitante” se existe chamado na HIT -> Se houver, informar status (Vamos alinhar os detalhes)`); + + // const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n Seu chamado está em atendimento\n\n *Analista:* ${endPointResponse.data.tecnico}\n *Chat:* ${endPointResponse.data.chat ? endPointResponse.data.chat : ""}\n\n_Digite *0* para voltar ao menu principal._`); + const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n Seu chamado está em atendimento\n\n${msg_endpoint_response}\n_Digite *0* para voltar ao menu principal._`); + await verifyMessage(msg, ticket, contact); + await new Promise(f => setTimeout(f, 1000)); + + } + else if (endPointResponse.data.categoria == 'ELOS' || (endPointResponse.data.subcategoria == 'VENDA' || endPointResponse.data.subcategoria == 'INDISPONIBILIDADE')) { + + // const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n Vi que está com um problema no ELOS\n\n *Status:* ${endPointResponse.data.status}\n\nSe seu caso for urgente para concluir uma venda, digite “URGENTE”\n_Digite *0* para voltar ao menu principal._`); + const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n Vi que está com um problema no ELOS\n\n${msg_endpoint_response}\nSe seu caso for urgente para concluir uma venda, digite “URGENTE”\n_Digite *0* para voltar ao menu principal._`); + await verifyMessage(msg, ticket, contact); + await new Promise(f => setTimeout(f, 1000)); + + } + else if ((endPointResponse.data.categoria == 'INFRAESTRUTURA' || endPointResponse.data.subcategoria == 'INTERNET' || + endPointResponse.data.terceiro_nivel == 'QUEDA TOTAL' || endPointResponse.data.terceiro_nivel == 'PROBLEMA DE LENTIDÃO') || + (endPointResponse.data.terceiro_nivel == 'PROBLEMA DE LENTIDÃO' || endPointResponse.data.terceiro_nivel == 'ABERTO')) { + + const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n${msg_endpoint_response}\n Estamos direcionando seu atendimento para o Suporte. Em breve você será atendido por um de nossos atendentes!`); + await transferTicket(0, wbot, ticket, contact) + + } + else { + + // const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n *Status:* ${endPointResponse.data.status}\n *Data:* ${endPointResponse.data.data_chat ? endPointResponse.data.data_chat : ""}\n *Hora:* ${endPointResponse.data.hora_chat ? endPointResponse.data.hora_chat : ""} \n\n Por favor, aguarde atendimento e acompanhe sua solicitação no SOS.\n_Digite *0* para voltar ao menu principal._`); + const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n${msg_endpoint_response}\n Por favor, aguarde atendimento e acompanhe sua solicitação no SOS.\n_Digite *0* para voltar ao menu principal._`); + await verifyMessage(msg, ticket, contact); + await new Promise(f => setTimeout(f, 1000)); + } + + } + else { + botSendMessage(ticket, contact, wbot, `Desculpe, nao foi possível realizar a consulta!\n _Digite *0* para voltar ao menu principal._`) + } + + } else if (msgAction.actions[0] == 'queue_transfer') { + + console.log('>>>>>>>>>>>>>>> msgAction: ', msgAction, ' | msgAction.actions[1]: ', msgAction.actions[1]) + + const msg = await wbot.sendMessage(`${contact.number}@c.us`, msgAction.msgBody); + await verifyMessage(msg, ticket, contact); + await new Promise(f => setTimeout(f, 1000)); + + await transferTicket(+msgAction.actions[1], wbot, ticket, contact) + } + else if (msgAction.actions[0] == 'send_file'){ + + const sourcePath = path.join(__dirname,`../../../public/bot`) + + const msg = await wbot.sendMessage(`${contact.number}@c.us`, msgAction.msgBody); + await verifyMessage(msg, ticket, contact); + await new Promise(f => setTimeout(f, 1000)); + + await botSendMedia(ticket,contact,wbot,sourcePath, msgAction.actions[1]) + + } + + } + else { + // const linesOfBody = body.split('\n'); + const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body); + await verifyMessage(sentMessage, ticket, contact); + await new Promise(f => setTimeout(f, 1000)); + + // for(let message of linesOfBody) { + // const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, message); + // await verifyMessage(sentMessage, ticket, contact); + // await new Promise(f => setTimeout(f, 1000)); + // } + } + + +} + + +const extractCallCode = (str: string) => { + if (str.includes('*') && str.indexOf('*') < str.lastIndexOf('*')) { + return (str.substring(str.indexOf('*'), str.lastIndexOf('*') + 1)).split('*').join('') + } + return '' +} + +const sendDialogflowAwswer = async ( + wbot: Session, + ticket: Ticket, + msg: WbotMessage, + contact: Contact, + chat: Chat +) => { + const session = await createDialogflowSessionWithModel(ticket.queue.dialogflow); + if (session === undefined) { + return; + } + + wbot.sendPresenceAvailable(); + + // console.log('typeof(msg.type): ', typeof (msg.type), ' | msg.type: ', msg.type) + + if (msg.type != 'chat') { + botSendMessage(ticket, contact, wbot, `Desculpe, nao compreendi!\nEnvie apenas texto quando estiver interagindo com o bot!\n _Digite *0* para voltar ao menu principal._`) + return + } + + if (msg.type == 'chat' && String(msg.body).length > 120) { + botSendMessage(ticket, contact, wbot, `Desculpe, nao compreendi!\nTexto acima de 120 caracteres!\n _Digite *0* para voltar ao menu principal._`) + return + } + + let dialogFlowReply = await queryDialogFlow( + session, + ticket.queue.dialogflow.projectName, + msg.from, + msg.body, + ticket.queue.dialogflow.language + ); + if (dialogFlowReply === null) { + return; + } + + chat.sendStateTyping(); + + await new Promise(f => setTimeout(f, 1000)); + + for (let message of dialogFlowReply) { + await sendDelayedMessages(wbot, ticket, contact, message.text.text[0]); + } +} + + const verifyQueue = async ( wbot: Session, @@ -195,8 +368,8 @@ const verifyQueue = async ( let choosenQueue = null //Habilitar esse caso queira usar o bot - // const botInfo = await BotIsOnQueue('botqueue') - const botInfo = { isOnQueue: false, botQueueId: 0, userIdBot: 0 } + const botInfo = await BotIsOnQueue('botqueue') + // const botInfo = { isOnQueue: false, botQueueId: 0, userIdBot: 0 } if (botInfo.isOnQueue) { @@ -247,7 +420,12 @@ const verifyQueue = async ( ticketId: ticket.id }); - data_ura.forEach((s, index) => { botOptions += `*${index + 1}* - ${s.option}\n` }); + const _ticket = await ShowTicketService(ticket.id); + const chat = await msg.getChat(); + await sendDialogflowAwswer(wbot, _ticket, msg, contact, chat); + return + + } // @@ -302,6 +480,27 @@ const verifyQueue = async ( } }; +const transferTicket = async (queueIndex: number, wbot: Session, ticket: Ticket, contact: Contact) => { + + const botInfo = await BotIsOnQueue('botqueue') + + const queuesWhatsGreetingMessage = await queuesOutBot(wbot, botInfo.botQueueId) + + const queues = queuesWhatsGreetingMessage.queues + + await botTransferTicket(queues[queueIndex], ticket, contact, wbot) + +} + +const botMsgActions = (params: string) => { + + let lstActions = params.split('dialog_actions=') + let bodyMsg = lstActions[0].trim() + let actions = lstActions[1].split("=") + + return { msgBody: bodyMsg, 'actions': [actions[0].trim(), actions[1].trim()] }; +} + const isValidMsg = (msg: WbotMessage): boolean => { if (msg.from === "status@broadcast") return false; if ( @@ -341,6 +540,37 @@ const botTransferTicket = async (queues: Queue, ticket: Ticket, contact: Contact } +const botSendMedia = async (ticket: Ticket, contact: Contact, wbot: Session, mediaPath: string, fileNameExtension: string) => { + + const debouncedSentMessage = debounce( + + async () => { + + // const sentMessage = await wbot.sendMessage(`${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`, newMedia, { sendAudioAsVoice: true }); + + const newMedia = MessageMedia.fromFilePath(`${mediaPath}/${fileNameExtension}`); + + const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, newMedia, {caption: 'this is my caption'}); + + + + // client.sendMessage("xxxxxxxx@c.us", media, {caption: "some caption"}); })(); + + // client.sendMessage(msg.from, attachmentData, { caption: 'Here\'s your requested media.' }); + + await ticket.update({ lastMessage: fileNameExtension }); + + verifyMessage(sentMessage, ticket, contact); + + }, + 3000, + ticket.id + ); + + debouncedSentMessage(); + +} + const botSendMessage = (ticket: Ticket, contact: Contact, wbot: Session, msg: string) => { @@ -469,307 +699,14 @@ const handleMessage = async ( // O bot interage com o cliente e encaminha o atendimento para fila de atendende quando o usuário escolhe a opção falar com atendente //Habilitar esse caso queira usar o bot - // const botInfo = await BotIsOnQueue('botqueue') - const botInfo = { isOnQueue: false, botQueueId: 0, userIdBot: 0 } - + 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`) - - } - - } - - } - + await sendDialogflowAwswer(wbot, ticket, msg, contact, chat); } - // - - - // test del + // if (msg.body.trim() == 'broken') { // throw new Error('Throw makes it go boom!') diff --git a/frontend/src/components/ContactModal/index.js b/frontend/src/components/ContactModal/index.js index 8305524..d67f01c 100644 --- a/frontend/src/components/ContactModal/index.js +++ b/frontend/src/components/ContactModal/index.js @@ -73,6 +73,7 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => { name: "", number: "", email: "", + useDialogflow: true, }; const [contact, setContact] = useState(initialState); diff --git a/frontend/src/components/DialogflowModal/index.js b/frontend/src/components/DialogflowModal/index.js new file mode 100644 index 0000000..557c3c8 --- /dev/null +++ b/frontend/src/components/DialogflowModal/index.js @@ -0,0 +1,285 @@ +import React, { useState, useEffect } from "react"; + +import * as Yup from "yup"; +import { Formik, Form, Field } from "formik"; +import { toast } from "react-toastify"; + +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + CircularProgress, + Select, + InputLabel, + MenuItem, + FormControl, + TextField, +} from '@material-ui/core' + +import { makeStyles } from "@material-ui/core/styles"; +import { green } from "@material-ui/core/colors"; + + +import { i18n } from "../../translate/i18n"; + +import api from "../../services/api"; +import toastError from "../../errors/toastError"; + +const useStyles = makeStyles(theme => ({ + root: { + display: "flex", + flexWrap: "wrap", + }, + textField: { + marginRight: theme.spacing(1), + flex: 1, + }, + + btnWrapper: { + position: "relative", + }, + + buttonProgress: { + color: green[500], + position: "absolute", + top: "50%", + left: "50%", + marginTop: -12, + marginLeft: -12, + }, + btnLeft: { + display: "flex", + marginRight: "auto", + marginLeft: 12, + }, + formControl: { + margin: theme.spacing(1), + minWidth: 240, + }, + colorAdorment: { + width: 20, + height: 20, + }, +})); + +const DialogflowSchema = Yup.object().shape({ + name: Yup.string() + .min(2, "Too Short!") + .max(50, "Too Long!") + .required("Required"), + projectName: Yup.string().min(3, "Too Short!").max(100, "Too Long!").required(), + jsonContent: Yup.string().min(3, "Too Short!").required(), + language: Yup.string().min(2, "Too Short!").max(50, "Too Long!").required(), + +}); + +const DialogflowModal = ({ open, onClose, dialogflowId }) => { + const classes = useStyles(); + + const initialState = { + name: "", + projectName: "", + jsonContent: "", + language: "", + }; + + const [dialogflow, setDialogflow] = useState(initialState); + + useEffect(() => { + (async () => { + if (!dialogflowId) return; + try { + const { data } = await api.get(`/dialogflow/${dialogflowId}`); + setDialogflow(prevState => { + return { ...prevState, ...data }; + }); + } catch (err) { + toastError(err); + } + })(); + + return () => { + setDialogflow({ + name: "", + projectName: "", + jsonContent: "", + language: "", + }); + }; + }, [dialogflowId, open]); + + const handleClose = () => { + onClose(); + setDialogflow(initialState); + }; + + const handleTestSession = async (event, values) => { + try { + const {projectName, jsonContent, language } = values + + await api.post(`/dialogflow/testSession`, {projectName, jsonContent, language }); + + toast.success( i18n.t("dialogflowModal.messages.testSuccess") ); + } catch (err) { + toastError(err); + } + }; + + const handleSaveDialogflow = async values => { + try { + console.log(values) + if (dialogflowId) { + await api.put(`/dialogflow/${dialogflowId}`, values); + toast.success( i18n.t("dialogflowModal.messages.editSuccess") ); + } else { + await api.post("/dialogflow", values); + toast.success( i18n.t("dialogflowModal.messages.addSuccess") ); + } + handleClose(); + } catch (err) { + toastError(err); + } + }; + + return ( +
+ + + + {dialogflowId + ? `${i18n.t("dialogflowModal.title.edit")}` + : `${i18n.t("dialogflowModal.title.add")}`} + + { + setTimeout(() => { + console.log(actions); + handleSaveDialogflow(values); + actions.setSubmitting(false); + }, 400); + }} + > + {({ touched, errors, isSubmitting, values }) => ( +
+ + + + + {i18n.t("dialogflowModal.form.language")} + + + + Portugues + Inglês + Espanhol + + +
+ +
+
+ +
+
+ + + + + + +
+ )} +
+
+
+ ); +}; + +export default DialogflowModal; diff --git a/frontend/src/components/QueueModal/index.js b/frontend/src/components/QueueModal/index.js index b86580a..e067b23 100644 --- a/frontend/src/components/QueueModal/index.js +++ b/frontend/src/components/QueueModal/index.js @@ -13,14 +13,23 @@ import DialogActions from "@material-ui/core/DialogActions"; import DialogContent from "@material-ui/core/DialogContent"; import DialogTitle from "@material-ui/core/DialogTitle"; import CircularProgress from "@material-ui/core/CircularProgress"; +import { + FormControl, + InputLabel, + Select, + MenuItem, + IconButton, + InputAdornment +} from "@material-ui/core" + import { i18n } from "../../translate/i18n"; import api from "../../services/api"; import toastError from "../../errors/toastError"; import ColorPicker from "../ColorPicker"; -import { IconButton, InputAdornment } from "@material-ui/core"; import { Colorize } from "@material-ui/icons"; +import useDialogflows from "../../hooks/useDialogflows"; const useStyles = makeStyles(theme => ({ root: { @@ -45,8 +54,8 @@ const useStyles = makeStyles(theme => ({ marginLeft: -12, }, formControl: { - margin: theme.spacing(1), - minWidth: 120, + marginRight: theme.spacing(1), + minWidth: 200, }, colorAdorment: { width: 20, @@ -61,6 +70,7 @@ const QueueSchema = Yup.object().shape({ .required("Required"), color: Yup.string().min(3, "Too Short!").max(9, "Too Long!").required(), greetingMessage: Yup.string(), + dialogflowId: Yup.number(), }); const QueueModal = ({ open, onClose, queueId }) => { @@ -70,10 +80,14 @@ const QueueModal = ({ open, onClose, queueId }) => { name: "", color: "", greetingMessage: "", + dialogflowId: "", }; const [colorPickerModalOpen, setColorPickerModalOpen] = useState(false); const [queue, setQueue] = useState(initialState); + const [dialogflows, setDialogflows] = useState([]); + const { findAll: findAllDialogflows } = useDialogflows(); + const greetingRef = useRef(); useEffect(() => { @@ -81,6 +95,10 @@ const QueueModal = ({ open, onClose, queueId }) => { if (!queueId) return; try { const { data } = await api.get(`/queue/${queueId}`); + if(data.dialogflowId === null) { + data.dialogflowId = ""; + } + setQueue(prevState => { return { ...prevState, ...data }; }); @@ -94,10 +112,20 @@ const QueueModal = ({ open, onClose, queueId }) => { name: "", color: "", greetingMessage: "", + dialogflowId: "", }); }; }, [queueId, open]); + useEffect(() => { + const loadDialogflows = async () => { + const list = await findAllDialogflows(); + setDialogflows(list); + } + loadDialogflows(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const handleClose = () => { onClose(); setQueue(initialState); @@ -105,6 +133,10 @@ const QueueModal = ({ open, onClose, queueId }) => { const handleSaveQueue = async values => { try { + if(values.dialogflowId === "") { + values.dialogflowId = null; + } + if (queueId) { await api.put(`/queue/${queueId}`, values); } else { @@ -213,6 +245,27 @@ const QueueModal = ({ open, onClose, queueId }) => { margin="dense" /> + + + {i18n.t("queueModal.form.dialogflow")} + + +   + {dialogflows.map((dialogflow) => ( + {dialogflow.name} + ))} + + + + + + + + + + {i18n.t("dialogflows.table.name")} + + + {i18n.t("dialogflows.table.projectName")} + + + {i18n.t("dialogflows.table.language")} + + + {i18n.t("dialogflows.table.lastUpdate")} + + + {i18n.t("dialogflows.table.actions")} + + + + + <> + {dialogflows.map((dialogflow) => ( + + {dialogflow.name} + { dialogflow.projectName} + {dialogflow.language} + + {format(parseISO(dialogflow.updatedAt), "dd/MM/yy HH:mm")} + + + handleEditDialogflow(dialogflow)} + > + + { + setSelectedDialogflow(dialogflow); + setConfirmModalOpen(true); + }} + > + + + + + ))} + {loading && } + + +
+
+ + ); +}; + +export default Dialogflows; diff --git a/frontend/src/pages/Queues/useLoadData.js b/frontend/src/pages/Queues/useLoadData.js new file mode 100644 index 0000000..1d812de --- /dev/null +++ b/frontend/src/pages/Queues/useLoadData.js @@ -0,0 +1,22 @@ +import { useEffect } from "react"; +import toastError from "../../errors/toastError"; +import api from "../../services/api"; + +const useLoadData = (setLoading, dispatch, route, dispatchType) => { + useEffect(() => { + (async () => { + setLoading(true); + try { + const { data } = await api.get(route); + dispatch({ type: dispatchType, payload: data }); + + setLoading(false); + } catch (err) { + toastError(err); + setLoading(false); + } + })(); + }, []); +} + +export default useLoadData; \ No newline at end of file diff --git a/frontend/src/pages/Queues/useSocket.js b/frontend/src/pages/Queues/useSocket.js new file mode 100644 index 0000000..b37c652 --- /dev/null +++ b/frontend/src/pages/Queues/useSocket.js @@ -0,0 +1,24 @@ +import { useEffect } from "react"; +import openSocket from "../../services/socket-io"; + +const useSocket = (dispatch, socketEvent, typeUpdate, typeDelete, payloadUpdate, payloadDelete) => { + useEffect(() => { + const socket = openSocket(); + + socket.on(socketEvent, (data) => { + if (data.action === "update" || data.action === "create") { + dispatch({ type: typeUpdate, payload: data[payloadUpdate] }); + } + + if (data.action === "delete") { + dispatch({ type: typeDelete, payload: data[payloadDelete] }); + } + }); + + return () => { + socket.disconnect(); + }; + }, []); +} + +export default useSocket; \ No newline at end of file diff --git a/frontend/src/routes/index.js b/frontend/src/routes/index.js index 1043761..5a996f3 100644 --- a/frontend/src/routes/index.js +++ b/frontend/src/routes/index.js @@ -21,7 +21,7 @@ import { AuthProvider } from "../context/Auth/AuthContext"; import { WhatsAppsProvider } from "../context/WhatsApp/WhatsAppsContext"; import Route from "./Route"; - +import Dialogflows from "../pages/Dialogflow/"; const Routes = () => { return ( @@ -47,6 +47,7 @@ const Routes = () => { + diff --git a/frontend/src/services/socket-io.js b/frontend/src/services/socket-io.js new file mode 100644 index 0000000..e892b26 --- /dev/null +++ b/frontend/src/services/socket-io.js @@ -0,0 +1,8 @@ +import openSocket from "socket.io-client"; +import { getBackendUrl } from "../config"; + +function connectToSocket() { + return openSocket(getBackendUrl()); +} + +export default connectToSocket; \ No newline at end of file diff --git a/frontend/src/translate/languages/en.js b/frontend/src/translate/languages/en.js index 0391037..af526bb 100644 --- a/frontend/src/translate/languages/en.js +++ b/frontend/src/translate/languages/en.js @@ -155,6 +155,7 @@ const messages = { email: "Email", extraName: "Field name", extraValue: "Value", + dialogflow: "Dialogflow", }, buttons: { addExtraInfo: "Add information", @@ -188,6 +189,7 @@ const messages = { form: { name: "Name", color: "Color", + dialogflow: "Dialogflow", greetingMessage: "Greeting Message", }, buttons: { @@ -196,6 +198,29 @@ const messages = { cancel: "Cancel", }, }, + dialogflowModal: { + title: { + add: "Add Project", + edit: "Edit Project", + }, + form: { + name: "Name", + projectName: "Project Name", + language: "Language", + jsonContent: "JsonContent" + }, + buttons: { + okAdd: "Add", + okEdit: "Save", + cancel: "Cancel", + test: "Bot Test", + }, + messages: { + testSuccess: "Dialogflow test successfully", + addSuccess: "Dialogflow added successfully", + editSuccess: "Dialogflow edited successfully", + } + }, userModal: { title: { add: "Add user", @@ -206,6 +231,7 @@ const messages = { email: "Email", password: "Password", profile: "Profile", + whatsapp: "Default Connection", }, buttons: { okAdd: "Add", @@ -248,7 +274,9 @@ const messages = { title: "Transfer Ticket", fieldLabel: "Type to search for users", fieldQueueLabel: "Transfer to queue", + fieldConnectionLabel: "Transfer to connection", fieldQueuePlaceholder: "Please select a queue", + fieldConnectionPlaceholder: "Please select a connection", noOptions: "No user found with this name", buttons: { ok: "Transfer", @@ -260,6 +288,7 @@ const messages = { assignedHeader: "Working on", noTicketsTitle: "Nothing here!", noTicketsMessage: "No tickets found with this status or search term.", + connectionTitle: "Connection that is currently being used.", buttons: { accept: "Accept", }, @@ -284,6 +313,7 @@ const messages = { administration: "Administration", users: "Users", settings: "Settings", + dialogflow: "Dialogflow", }, appBar: { user: { @@ -312,6 +342,24 @@ const messages = { "Are you sure? It cannot be reverted! Tickets in this queue will still exist, but will not have any queues assigned.", }, }, + dialogflows: { + title: "Dialogflow", + table: { + name: "Name", + projectName: "Project Name", + lamguage: "Language", + lastUpdate:"Last Update", + actions: "Actions", + }, + buttons: { + add: "Add Project", + }, + confirmationModal: { + deleteTitle: "Delete", + deleteMessage: + "Are you sure? It cannot be reverted!", + }, + }, queueSelect: { inputLabel: "Queues", }, @@ -340,6 +388,7 @@ const messages = { name: "Name", email: "Email", profile: "Profile", + whatsapp: "Default Connection", actions: "Actions", }, buttons: { @@ -371,6 +420,7 @@ const messages = { header: { assignedTo: "Assigned to:", buttons: { + dialogflow: "Dialogflow", return: "Return", resolve: "Resolve", reopen: "Reopen", @@ -392,6 +442,7 @@ const messages = { }, ticketOptionsMenu: { delete: "Delete", + useQueues: "¿Usar colas?", transfer: "Transfer", confirmationModal: { title: "Delete ticket #", @@ -453,6 +504,9 @@ const messages = { "This color is already in use, pick another one.", ERR_WAPP_GREETING_REQUIRED: "Greeting message is required if there is more than one queue.", + ERR_NO_DIALOG_FOUND: "No Dialogflow found with this ID.", + ERR_TEST_SESSION_DIALOG: "Error creating DialogFlow session", + ERR_TEST_REPLY_DIALOG: "Error testing DialogFlow configuration", }, }, }, diff --git a/frontend/src/translate/languages/es.js b/frontend/src/translate/languages/es.js index b4f7d5e..a9a1fbb 100644 --- a/frontend/src/translate/languages/es.js +++ b/frontend/src/translate/languages/es.js @@ -158,6 +158,7 @@ const messages = { email: "Correo Electrónico", extraName: "Nombre del Campo", extraValue: "Valor", + dialogflow: "Dialogflow", }, buttons: { addExtraInfo: "Agregar información", @@ -191,6 +192,7 @@ const messages = { form: { name: "Nombre", color: "Color", + dialogflow: "Dialogflow", greetingMessage: "Mensaje de saludo", }, buttons: { @@ -199,6 +201,29 @@ const messages = { cancel: "Cancelar", }, }, + dialogflowModal: { + title: { + add: "Agregar cola", + edit: "Editar cola", + }, + form: { + name: "Nombre", + projectName: "Nombre del proyecto", + language: "Idioma", + jsonContent: "JsonContent", + }, + buttons: { + okAdd: "Añadir", + okEdit: "Ahorrar", + cancel: "Cancelar", + test: "Testar Bot", + }, + messages: { + testSuccess: "Prueba Dialogflow con éxito", + addSuccess: "Dialogflow agregado con éxito", + editSuccess: "Dialogflow editado con éxito", + } + }, userModal: { title: { add: "Agregar usuario", @@ -209,6 +234,7 @@ const messages = { email: "Correo Electrónico", password: "Contraseña", profile: "Perfil", + whatsapp: "Conexión estándar", }, buttons: { okAdd: "Agregar", @@ -251,7 +277,9 @@ const messages = { title: "Transferir Ticket", fieldLabel: "Escriba para buscar usuarios", fieldQueueLabel: "Transferir a la cola", + fieldConnectionLabel: "Transferir to conexión", fieldQueuePlaceholder: "Seleccione una cola", + fieldConnectionPlaceholder: "Seleccione una conexión", noOptions: "No se encontraron usuarios con ese nombre", buttons: { ok: "Transferir", @@ -262,6 +290,7 @@ const messages = { pendingHeader: "Cola", assignedHeader: "Trabajando en", noTicketsTitle: "¡Nada acá!", + connectionTitle: "Conexión que se está utilizando actualmente.", noTicketsMessage: "No se encontraron tickets con este estado o término de búsqueda", buttons: { @@ -280,6 +309,7 @@ const messages = { mainDrawer: { listItems: { dashboard: "Dashboard", + connections: "Conexiones", tickets: "Tickets", contacts: "Contactos", @@ -288,6 +318,7 @@ const messages = { administration: "Administración", users: "Usuarios", settings: "Configuración", + dialogflow: "Dialogflow", }, appBar: { user: { @@ -316,6 +347,24 @@ const messages = { "¿Estás seguro? ¡Esta acción no se puede revertir! Los tickets en esa cola seguirán existiendo, pero ya no tendrán ninguna cola asignada.", }, }, + dialoflows: { + title: "Dialogflow", + table: { + name: "Nombre", + projectName: "Nombre del proyecto", + language: "Idioma", + lastUpdate: "Última Actualización", + actions: "Comportamiento", + }, + buttons: { + add: "Agregar proyecto", + }, + confirmationModal: { + deleteTitle: "Eliminar", + deleteMessage: + "¿Estás seguro? ¡Esta acción no se puede revertir!", + }, + }, queueSelect: { inputLabel: "Linhas", }, @@ -345,6 +394,7 @@ const messages = { name: "Nombre", email: "Correo Electrónico", profile: "Perfil", + whatsapp: "Conexión estándar", actions: "Acciones", }, buttons: { @@ -376,6 +426,7 @@ const messages = { header: { assignedTo: "Asignado a:", buttons: { + dialogflow: "Dialogflow", return: "Devolver", resolve: "Resolver", reopen: "Reabrir", @@ -398,6 +449,7 @@ const messages = { }, ticketOptionsMenu: { delete: "Borrar", + useQueues: "Use Queues", transfer: "Transferir", confirmationModal: { title: "¿Borrar ticket #", @@ -460,6 +512,9 @@ const messages = { "Este color ya está en uso, elija otro.", ERR_WAPP_GREETING_REQUIRED: "El mensaje de saludo es obligatorio cuando hay más de una cola.", + ERR_NO_DIALOG_FOUND: "No se encontró Dialogflow con este ID.", + ERR_TEST_SESSION_DIALOG: "Error al crear la sesión de dialogflow", + ERR_TEST_REPLY_DIALOG: "Error al probar la configuración de DialogFlow", }, }, }, diff --git a/frontend/src/translate/languages/pt.js b/frontend/src/translate/languages/pt.js index d3b7835..45ff145 100644 --- a/frontend/src/translate/languages/pt.js +++ b/frontend/src/translate/languages/pt.js @@ -80,8 +80,7 @@ const messages = { qrcode: { title: "Esperando leitura do QR Code", content: - // "Clique no botão 'QR CODE' e leia o QR Code com o seu celular para iniciar a sessão", - "Entre em contato com o suporte para realizar a leitura do QR code", + "Clique no botão 'QR CODE' e leia o QR Code com o seu celular para iniciar a sessão", }, connected: { title: "Conexão estabelecida!", @@ -158,6 +157,7 @@ const messages = { email: "Email", extraName: "Nome do campo", extraValue: "Valor", + dialogflow: "Dialogflow", }, buttons: { addExtraInfo: "Adicionar informação", @@ -191,6 +191,7 @@ const messages = { form: { name: "Nome", color: "Cor", + dialogflow: "Dialogflow", greetingMessage: "Mensagem de saudação", }, buttons: { @@ -199,6 +200,29 @@ const messages = { cancel: "Cancelar", }, }, + dialogflowModal: { + title: { + add: "Adicionar projeto", + edit: "Editar projeto", + }, + form: { + name: "Nome", + projectName: "Nome do Projeto", + language: "Linguagem", + jsonContent: "JsonContent", + }, + buttons: { + okAdd: "Adicionar", + okEdit: "Salvar", + cancel: "Cancelar", + test: "Testar Bot", + }, + messages: { + testSuccess: "Dialogflow testado com sucesso!", + addSuccess: "Dialogflow adicionado com sucesso.", + editSuccess: "Dialogflow editado com sucesso.", + } + }, userModal: { title: { add: "Adicionar usuário", @@ -209,6 +233,7 @@ const messages = { email: "Email", password: "Senha", profile: "Perfil", + whatsapp: "Conexão Padrão", }, buttons: { okAdd: "Adicionar", @@ -241,7 +266,7 @@ const messages = { search: { title: "Busca" }, }, search: { - placeholder: "Busca telefone/nome", + placeholder: "Buscar tickets e mensagens", }, buttons: { showAll: "Todos", @@ -251,7 +276,9 @@ const messages = { title: "Transferir Ticket", fieldLabel: "Digite para buscar usuários", fieldQueueLabel: "Transferir para fila", + fieldConnectionLabel: "Transferir para conexão", fieldQueuePlaceholder: "Selecione uma fila", + fieldConnectionPlaceholder: "Selecione uma conexão", noOptions: "Nenhum usuário encontrado com esse nome", buttons: { ok: "Transferir", @@ -264,6 +291,7 @@ const messages = { noTicketsTitle: "Nada aqui!", noTicketsMessage: "Nenhum ticket encontrado com esse status ou termo pesquisado", + connectionTitle: "Conexão que está sendo utilizada atualmente.", buttons: { accept: "Aceitar", }, @@ -279,7 +307,7 @@ const messages = { }, mainDrawer: { listItems: { - dashboard: "Dashboard", + dashboard: "Dashboard", connections: "Conexões", tickets: "Tickets", contacts: "Contatos", @@ -288,6 +316,7 @@ const messages = { administration: "Administração", users: "Usuários", settings: "Configurações", + dialogflow: "Dialogflow", }, appBar: { user: { @@ -316,6 +345,24 @@ const messages = { "Você tem certeza? Essa ação não pode ser revertida! Os tickets dessa fila continuarão existindo, mas não terão mais nenhuma fila atribuída.", }, }, + dialogflows: { + title: "Dialogflow", + table: { + name: "Nome", + projectName: "Nome do Projeto", + language: "Linguagem", + lastUpdate: "Ultima atualização", + actions: "Ações", + }, + buttons: { + add: "Adicionar Projeto", + }, + confirmationModal: { + deleteTitle: "Excluir", + deleteMessage: + "Você tem certeza? Essa ação não pode ser revertida! e será removida das filas e conexões vinculadas", + }, + }, queueSelect: { inputLabel: "Filas", }, @@ -345,6 +392,7 @@ const messages = { name: "Nome", email: "Email", profile: "Perfil", + whatsapp: "Conexão Padrão", actions: "Ações", }, buttons: { @@ -376,6 +424,7 @@ const messages = { header: { assignedTo: "Atribuído à:", buttons: { + dialogflow: "Dialogflow", return: "Retornar", resolve: "Resolver", reopen: "Reabrir", @@ -398,6 +447,7 @@ const messages = { }, ticketOptionsMenu: { delete: "Deletar", + useQueues: "Usar fila?", transfer: "Transferir", confirmationModal: { title: "Deletar o ticket do contato", @@ -458,6 +508,9 @@ const messages = { "Esta cor já está em uso, escolha outra.", ERR_WAPP_GREETING_REQUIRED: "A mensagem de saudação é obrigatório quando há mais de uma fila.", + ERR_NO_DIALOG_FOUND: "Nenhuma Dialogflow encontrado com este ID", + ERR_TEST_SESSION_DIALOG: "Erro ao criar sessão do DialogFlow", + ERR_TEST_REPLY_DIALOG: "Erro ao testar configuração do DialogFlow", }, }, },