From 9f99245dc4959ff17e7e6ebf9a3cd8f5e699b4b0 Mon Sep 17 00:00:00 2001 From: canove Date: Fri, 8 Jan 2021 17:41:08 -0300 Subject: [PATCH 01/29] feat: add queue and assoc with whatsapps --- backend/src/controllers/QueueController.ts | 19 ++++++++ backend/src/controllers/WhatsAppController.ts | 8 ++-- backend/src/database/index.ts | 6 ++- .../20210108164404-create-queues.ts | 36 +++++++++++++++ .../20210108164504-add-queueId-to-tickets.ts | 16 +++++++ ...20210108174594-associate-whatsapp-queue.ts | 28 ++++++++++++ backend/src/models/Queue.ts | 44 +++++++++++++++++++ backend/src/models/Whatsapp.ts | 8 +++- backend/src/models/WhatsappQueue.ts | 29 ++++++++++++ backend/src/routes/index.ts | 2 + backend/src/routes/queueRoutes.ts | 12 +++++ .../WhatsappService/CreateWhatsAppService.ts | 4 ++ .../WhatsappService/ListWhatsAppsService.ts | 7 ++- .../WhatsappService/UpdateWhatsAppService.ts | 5 ++- 14 files changed, 217 insertions(+), 7 deletions(-) create mode 100644 backend/src/controllers/QueueController.ts create mode 100644 backend/src/database/migrations/20210108164404-create-queues.ts create mode 100644 backend/src/database/migrations/20210108164504-add-queueId-to-tickets.ts create mode 100644 backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts create mode 100644 backend/src/models/Queue.ts create mode 100644 backend/src/models/WhatsappQueue.ts create mode 100644 backend/src/routes/queueRoutes.ts diff --git a/backend/src/controllers/QueueController.ts b/backend/src/controllers/QueueController.ts new file mode 100644 index 0000000..412840f --- /dev/null +++ b/backend/src/controllers/QueueController.ts @@ -0,0 +1,19 @@ +import { Request, Response } from "express"; +import Queue from "../models/Queue"; + +// import { getIO } from "../libs/socket"; +// import AppError from "../errors/AppError"; + +export const index = async (req: Request, res: Response): Promise => { + const queues = await Queue.findAll(); + + return res.status(200).json(queues); +}; + +export const store = async (req: Request, res: Response): Promise => { + const { name, color } = req.body; + + const queue = await Queue.create({ name, color }); + + return res.status(200).json(queue); +}; diff --git a/backend/src/controllers/WhatsAppController.ts b/backend/src/controllers/WhatsAppController.ts index 531bfe7..2ccad1f 100644 --- a/backend/src/controllers/WhatsAppController.ts +++ b/backend/src/controllers/WhatsAppController.ts @@ -11,6 +11,7 @@ import UpdateWhatsAppService from "../services/WhatsappService/UpdateWhatsAppSer interface WhatsappData { name: string; + queueIds: number[]; status?: string; isDefault?: boolean; } @@ -22,15 +23,16 @@ export const index = async (req: Request, res: Response): Promise => { }; export const store = async (req: Request, res: Response): Promise => { - const { name, status, isDefault }: WhatsappData = req.body; + const { name, status, isDefault, queueIds }: WhatsappData = req.body; const { whatsapp, oldDefaultWhatsapp } = await CreateWhatsAppService({ name, status, - isDefault + isDefault, + queueIds }); - StartWhatsAppSession(whatsapp); + // StartWhatsAppSession(whatsapp); const io = getIO(); io.emit("whatsapp", { diff --git a/backend/src/database/index.ts b/backend/src/database/index.ts index 9fb351d..9903d7e 100644 --- a/backend/src/database/index.ts +++ b/backend/src/database/index.ts @@ -6,6 +6,8 @@ import Ticket from "../models/Ticket"; import Whatsapp from "../models/Whatsapp"; import ContactCustomField from "../models/ContactCustomField"; import Message from "../models/Message"; +import Queue from "../models/Queue"; +import WhatsappQueue from "../models/WhatsappQueue"; // eslint-disable-next-line const dbConfig = require("../config/database"); @@ -20,7 +22,9 @@ const models = [ Message, Whatsapp, ContactCustomField, - Setting + Setting, + Queue, + WhatsappQueue ]; sequelize.addModels(models); diff --git a/backend/src/database/migrations/20210108164404-create-queues.ts b/backend/src/database/migrations/20210108164404-create-queues.ts new file mode 100644 index 0000000..41681d5 --- /dev/null +++ b/backend/src/database/migrations/20210108164404-create-queues.ts @@ -0,0 +1,36 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.createTable("Queues", { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, + color: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false + } + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.dropTable("Queues"); + } +}; diff --git a/backend/src/database/migrations/20210108164504-add-queueId-to-tickets.ts b/backend/src/database/migrations/20210108164504-add-queueId-to-tickets.ts new file mode 100644 index 0000000..6122b32 --- /dev/null +++ b/backend/src/database/migrations/20210108164504-add-queueId-to-tickets.ts @@ -0,0 +1,16 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Tickets", "queueId", { + type: DataTypes.INTEGER, + references: { model: "Queues", key: "id" }, + onUpdate: "CASCADE", + onDelete: "SET NULL" + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Tickets", "queueId"); + } +}; diff --git a/backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts b/backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts new file mode 100644 index 0000000..0e08f71 --- /dev/null +++ b/backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts @@ -0,0 +1,28 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.createTable("WhatsappQueues", { + whatsappId: { + type: DataTypes.INTEGER, + primaryKey: true + }, + queueId: { + type: DataTypes.INTEGER, + primaryKey: true + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false + } + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.dropTable("WhatsappQueues"); + } +}; diff --git a/backend/src/models/Queue.ts b/backend/src/models/Queue.ts new file mode 100644 index 0000000..1178329 --- /dev/null +++ b/backend/src/models/Queue.ts @@ -0,0 +1,44 @@ +import { + Table, + Column, + CreatedAt, + UpdatedAt, + Model, + PrimaryKey, + AutoIncrement, + AllowNull, + Unique, + BelongsToMany +} from "sequelize-typescript"; + +import Whatsapp from "./Whatsapp"; +import WhatsappQueue from "./WhatsappQueue"; + +@Table +class Queue extends Model { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @AllowNull(false) + @Unique + @Column + name: string; + + @AllowNull(false) + @Unique + @Column + color: string; + + @CreatedAt + createdAt: Date; + + @UpdatedAt + updatedAt: Date; + + @BelongsToMany(() => Whatsapp, () => WhatsappQueue) + whatsapps: Array; +} + +export default Queue; diff --git a/backend/src/models/Whatsapp.ts b/backend/src/models/Whatsapp.ts index 099b0db..ba6febb 100644 --- a/backend/src/models/Whatsapp.ts +++ b/backend/src/models/Whatsapp.ts @@ -10,9 +10,12 @@ import { Default, AllowNull, HasMany, - Unique + Unique, + BelongsToMany } from "sequelize-typescript"; +import Queue from "./Queue"; import Ticket from "./Ticket"; +import WhatsappQueue from "./WhatsappQueue"; @Table class Whatsapp extends Model { @@ -57,6 +60,9 @@ class Whatsapp extends Model { @HasMany(() => Ticket) tickets: Ticket[]; + + @BelongsToMany(() => Queue, () => WhatsappQueue) + queues: Array; } export default Whatsapp; diff --git a/backend/src/models/WhatsappQueue.ts b/backend/src/models/WhatsappQueue.ts new file mode 100644 index 0000000..17479cc --- /dev/null +++ b/backend/src/models/WhatsappQueue.ts @@ -0,0 +1,29 @@ +import { + Table, + Column, + CreatedAt, + UpdatedAt, + Model, + ForeignKey +} from "sequelize-typescript"; +import Queue from "./Queue"; +import Whatsapp from "./Whatsapp"; + +@Table +class WhatsappQueue extends Model { + @ForeignKey(() => Whatsapp) + @Column + whatsappId: number; + + @ForeignKey(() => Queue) + @Column + queueId: number; + + @CreatedAt + createdAt: Date; + + @UpdatedAt + updatedAt: Date; +} + +export default WhatsappQueue; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 2abafa8..7043f61 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -8,6 +8,7 @@ import ticketRoutes from "./ticketRoutes"; import whatsappRoutes from "./whatsappRoutes"; import messageRoutes from "./messageRoutes"; import whatsappSessionRoutes from "./whatsappSessionRoutes"; +import queueRoutes from "./queueRoutes"; const routes = Router(); @@ -20,5 +21,6 @@ routes.use(whatsappRoutes); routes.use(messageRoutes); routes.use(messageRoutes); routes.use(whatsappSessionRoutes); +routes.use(queueRoutes); export default routes; diff --git a/backend/src/routes/queueRoutes.ts b/backend/src/routes/queueRoutes.ts new file mode 100644 index 0000000..51e793d --- /dev/null +++ b/backend/src/routes/queueRoutes.ts @@ -0,0 +1,12 @@ +import { Router } from "express"; +import isAuth from "../middleware/isAuth"; + +import * as QueueController from "../controllers/QueueController"; + +const queueRoutes = Router(); + +queueRoutes.get("/queue", isAuth, QueueController.index); + +queueRoutes.post("/queue", isAuth, QueueController.store); + +export default queueRoutes; diff --git a/backend/src/services/WhatsappService/CreateWhatsAppService.ts b/backend/src/services/WhatsappService/CreateWhatsAppService.ts index 118343e..fc2f1b2 100644 --- a/backend/src/services/WhatsappService/CreateWhatsAppService.ts +++ b/backend/src/services/WhatsappService/CreateWhatsAppService.ts @@ -5,6 +5,7 @@ import Whatsapp from "../../models/Whatsapp"; interface Request { name: string; + queueIds: number[]; status?: string; isDefault?: boolean; } @@ -17,6 +18,7 @@ interface Response { const CreateWhatsAppService = async ({ name, status = "OPENING", + queueIds, isDefault = false }: Request): Promise => { const schema = Yup.object().shape({ @@ -68,6 +70,8 @@ const CreateWhatsAppService = async ({ isDefault }); + await whatsapp.$set("queues", queueIds); + return { whatsapp, oldDefaultWhatsapp }; }; diff --git a/backend/src/services/WhatsappService/ListWhatsAppsService.ts b/backend/src/services/WhatsappService/ListWhatsAppsService.ts index 2213530..5a1817a 100644 --- a/backend/src/services/WhatsappService/ListWhatsAppsService.ts +++ b/backend/src/services/WhatsappService/ListWhatsAppsService.ts @@ -1,7 +1,12 @@ +import Queue from "../../models/Queue"; import Whatsapp from "../../models/Whatsapp"; const ListWhatsAppsService = async (): Promise => { - const whatsapps = await Whatsapp.findAll(); + const whatsapps = await Whatsapp.findAll({ + include: [ + { model: Queue, as: "queues", attributes: ["id", "name", "color"] } + ] + }); return whatsapps; }; diff --git a/backend/src/services/WhatsappService/UpdateWhatsAppService.ts b/backend/src/services/WhatsappService/UpdateWhatsAppService.ts index 2fe79cc..04c3c72 100644 --- a/backend/src/services/WhatsappService/UpdateWhatsAppService.ts +++ b/backend/src/services/WhatsappService/UpdateWhatsAppService.ts @@ -9,6 +9,7 @@ interface WhatsappData { status?: string; session?: string; isDefault?: boolean; + queueIds?: number[]; } interface Request { @@ -30,7 +31,7 @@ const UpdateWhatsAppService = async ({ isDefault: Yup.boolean() }); - const { name, status, isDefault, session } = whatsappData; + const { name, status, isDefault, session, queueIds = [] } = whatsappData; try { await schema.validate({ name, status, isDefault }); @@ -63,6 +64,8 @@ const UpdateWhatsAppService = async ({ isDefault }); + await whatsapp.$set("queues", queueIds); + return { whatsapp, oldDefaultWhatsapp }; }; From 3c6d0660d1c4996c408f0a5618a6d044cad8e30e Mon Sep 17 00:00:00 2001 From: canove Date: Fri, 8 Jan 2021 20:18:11 -0300 Subject: [PATCH 02/29] feat: added user association to queues --- backend/src/config/auth.ts | 2 +- backend/src/controllers/UserController.ts | 5 ++-- backend/src/database/index.ts | 4 ++- .../20210108204708-associate-users-queue.ts | 28 ++++++++++++++++++ backend/src/models/Queue.ts | 5 ++++ backend/src/models/Ticket.ts | 8 +++++ backend/src/models/User.ts | 8 ++++- backend/src/models/UserQueue.ts | 29 +++++++++++++++++++ .../UserServices/CreateUserService.ts | 24 ++++++++++----- .../services/UserServices/ListUsersService.ts | 12 +++++--- .../services/UserServices/ShowUserService.ts | 6 +++- .../UserServices/UpdateUserService.ts | 21 +++++++------- .../WhatsappService/CreateWhatsAppService.ts | 17 +++++++---- .../WhatsappService/ShowWhatsAppService.ts | 7 ++++- .../WhatsappService/UpdateWhatsAppService.ts | 10 +++---- 15 files changed, 145 insertions(+), 41 deletions(-) create mode 100644 backend/src/database/migrations/20210108204708-associate-users-queue.ts create mode 100644 backend/src/models/UserQueue.ts diff --git a/backend/src/config/auth.ts b/backend/src/config/auth.ts index 6f8c5fd..706f3d5 100644 --- a/backend/src/config/auth.ts +++ b/backend/src/config/auth.ts @@ -1,6 +1,6 @@ export default { secret: process.env.JWT_SECRET || "mysecret", - expiresIn: "15m", + expiresIn: "15d", refreshSecret: process.env.JWT_REFRESH_SECRET || "myanothersecret", refreshExpiresIn: "7d" }; diff --git a/backend/src/controllers/UserController.ts b/backend/src/controllers/UserController.ts index e1c8a19..06d329d 100644 --- a/backend/src/controllers/UserController.ts +++ b/backend/src/controllers/UserController.ts @@ -27,7 +27,7 @@ export const index = async (req: Request, res: Response): Promise => { }; export const store = async (req: Request, res: Response): Promise => { - const { email, password, name, profile } = req.body; + const { email, password, name, profile, queueIds } = req.body; if ( req.url === "/signup" && @@ -42,7 +42,8 @@ export const store = async (req: Request, res: Response): Promise => { email, password, name, - profile + profile, + queueIds }); const io = getIO(); diff --git a/backend/src/database/index.ts b/backend/src/database/index.ts index 9903d7e..b62e840 100644 --- a/backend/src/database/index.ts +++ b/backend/src/database/index.ts @@ -8,6 +8,7 @@ import ContactCustomField from "../models/ContactCustomField"; import Message from "../models/Message"; import Queue from "../models/Queue"; import WhatsappQueue from "../models/WhatsappQueue"; +import UserQueue from "../models/UserQueue"; // eslint-disable-next-line const dbConfig = require("../config/database"); @@ -24,7 +25,8 @@ const models = [ ContactCustomField, Setting, Queue, - WhatsappQueue + WhatsappQueue, + UserQueue ]; sequelize.addModels(models); diff --git a/backend/src/database/migrations/20210108204708-associate-users-queue.ts b/backend/src/database/migrations/20210108204708-associate-users-queue.ts new file mode 100644 index 0000000..d92496a --- /dev/null +++ b/backend/src/database/migrations/20210108204708-associate-users-queue.ts @@ -0,0 +1,28 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.createTable("UserQueues", { + userId: { + type: DataTypes.INTEGER, + primaryKey: true + }, + queueId: { + type: DataTypes.INTEGER, + primaryKey: true + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false + } + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.dropTable("UserQueues"); + } +}; diff --git a/backend/src/models/Queue.ts b/backend/src/models/Queue.ts index 1178329..65d3548 100644 --- a/backend/src/models/Queue.ts +++ b/backend/src/models/Queue.ts @@ -10,6 +10,8 @@ import { Unique, BelongsToMany } from "sequelize-typescript"; +import User from "./User"; +import UserQueue from "./UserQueue"; import Whatsapp from "./Whatsapp"; import WhatsappQueue from "./WhatsappQueue"; @@ -39,6 +41,9 @@ class Queue extends Model { @BelongsToMany(() => Whatsapp, () => WhatsappQueue) whatsapps: Array; + + @BelongsToMany(() => User, () => UserQueue) + users: Array; } export default Queue; diff --git a/backend/src/models/Ticket.ts b/backend/src/models/Ticket.ts index 80d94cf..8de4375 100644 --- a/backend/src/models/Ticket.ts +++ b/backend/src/models/Ticket.ts @@ -14,6 +14,7 @@ import { import Contact from "./Contact"; import Message from "./Message"; +import Queue from "./Queue"; import User from "./User"; import Whatsapp from "./Whatsapp"; @@ -64,6 +65,13 @@ class Ticket extends Model { @BelongsTo(() => Whatsapp) whatsapp: Whatsapp; + @ForeignKey(() => Queue) + @Column + queueId: number; + + @BelongsTo(() => Queue) + queue: Queue; + @HasMany(() => Message) messages: Message[]; } diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index fe840fc..9be664b 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -10,10 +10,13 @@ import { PrimaryKey, AutoIncrement, Default, - HasMany + HasMany, + BelongsToMany } from "sequelize-typescript"; import { hash, compare } from "bcryptjs"; import Ticket from "./Ticket"; +import Queue from "./Queue"; +import UserQueue from "./UserQueue"; @Table class User extends Model { @@ -51,6 +54,9 @@ class User extends Model { @HasMany(() => Ticket) tickets: Ticket[]; + @BelongsToMany(() => Queue, () => UserQueue) + queues: Queue[]; + @BeforeUpdate @BeforeCreate static hashPassword = async (instance: User): Promise => { diff --git a/backend/src/models/UserQueue.ts b/backend/src/models/UserQueue.ts new file mode 100644 index 0000000..17528c2 --- /dev/null +++ b/backend/src/models/UserQueue.ts @@ -0,0 +1,29 @@ +import { + Table, + Column, + CreatedAt, + UpdatedAt, + Model, + ForeignKey +} from "sequelize-typescript"; +import Queue from "./Queue"; +import User from "./User"; + +@Table +class UserQueue extends Model { + @ForeignKey(() => User) + @Column + userId: number; + + @ForeignKey(() => Queue) + @Column + queueId: number; + + @CreatedAt + createdAt: Date; + + @UpdatedAt + updatedAt: Date; +} + +export default UserQueue; diff --git a/backend/src/services/UserServices/CreateUserService.ts b/backend/src/services/UserServices/CreateUserService.ts index c48b6d0..930036a 100644 --- a/backend/src/services/UserServices/CreateUserService.ts +++ b/backend/src/services/UserServices/CreateUserService.ts @@ -7,6 +7,7 @@ interface Request { email: string; password: string; name: string; + queueIds?: number[]; profile?: string; } @@ -21,6 +22,7 @@ const CreateUserService = async ({ email, password, name, + queueIds = [], profile = "admin" }: Request): Promise => { const schema = Yup.object().shape({ @@ -47,18 +49,26 @@ const CreateUserService = async ({ throw new AppError(err.message); } - const user = await User.create({ - email, - password, - name, - profile - }); + const user = await User.create( + { + email, + password, + name, + profile + }, + { include: ["queues"] } + ); + + await user.$set("queues", queueIds); + + await user.reload(); const serializedUser = { id: user.id, name: user.name, email: user.email, - profile: user.profile + profile: user.profile, + queues: user.queues }; return serializedUser; diff --git a/backend/src/services/UserServices/ListUsersService.ts b/backend/src/services/UserServices/ListUsersService.ts index 24bd8f0..fb8a202 100644 --- a/backend/src/services/UserServices/ListUsersService.ts +++ b/backend/src/services/UserServices/ListUsersService.ts @@ -1,4 +1,5 @@ import { Sequelize, Op } from "sequelize"; +import Queue from "../../models/Queue"; import User from "../../models/User"; interface Request { @@ -19,8 +20,8 @@ const ListUsersService = async ({ const whereCondition = { [Op.or]: [ { - name: Sequelize.where( - Sequelize.fn("LOWER", Sequelize.col("name")), + "$User.name$": Sequelize.where( + Sequelize.fn("LOWER", Sequelize.col("User.name")), "LIKE", `%${searchParam.toLowerCase()}%` ) @@ -33,10 +34,13 @@ const ListUsersService = async ({ const { count, rows: users } = await User.findAndCountAll({ where: whereCondition, - attributes: ["name", "id", "email", "profile"], + attributes: ["name", "id", "email", "profile", "createdAt"], limit, offset, - order: [["createdAt", "DESC"]] + order: [["createdAt", "DESC"]], + include: [ + { model: Queue, as: "queues", attributes: ["id", "name", "color"] } + ] }); const hasMore = count > offset + users.length; diff --git a/backend/src/services/UserServices/ShowUserService.ts b/backend/src/services/UserServices/ShowUserService.ts index bba0242..3052a2e 100644 --- a/backend/src/services/UserServices/ShowUserService.ts +++ b/backend/src/services/UserServices/ShowUserService.ts @@ -1,9 +1,13 @@ import User from "../../models/User"; import AppError from "../../errors/AppError"; +import Queue from "../../models/Queue"; const ShowUserService = async (id: string | number): Promise => { const user = await User.findByPk(id, { - attributes: ["name", "id", "email", "profile", "tokenVersion"] + attributes: ["name", "id", "email", "profile", "tokenVersion"], + include: [ + { model: Queue, as: "queues", attributes: ["id", "name", "color"] } + ] }); if (!user) { diff --git a/backend/src/services/UserServices/UpdateUserService.ts b/backend/src/services/UserServices/UpdateUserService.ts index e548645..114d557 100644 --- a/backend/src/services/UserServices/UpdateUserService.ts +++ b/backend/src/services/UserServices/UpdateUserService.ts @@ -1,13 +1,14 @@ import * as Yup from "yup"; import AppError from "../../errors/AppError"; -import User from "../../models/User"; +import ShowUserService from "./ShowUserService"; interface UserData { email?: string; password?: string; name?: string; profile?: string; + queueIds?: number[]; } interface Request { @@ -26,14 +27,7 @@ const UpdateUserService = async ({ userData, userId }: Request): Promise => { - const user = await User.findOne({ - where: { id: userId }, - attributes: ["name", "id", "email", "profile"] - }); - - if (!user) { - throw new AppError("ERR_NO_USER_FOUND", 404); - } + const user = await ShowUserService(userId); const schema = Yup.object().shape({ name: Yup.string().min(2), @@ -42,7 +36,7 @@ const UpdateUserService = async ({ password: Yup.string() }); - const { email, password, profile, name } = userData; + const { email, password, profile, name, queueIds = [] } = userData; try { await schema.validate({ email, password, profile, name }); @@ -57,11 +51,16 @@ const UpdateUserService = async ({ name }); + await user.$set("queues", queueIds); + + await user.reload(); + const serializedUser = { id: user.id, name: user.name, email: user.email, - profile: user.profile + profile: user.profile, + queues: user.queues }; return serializedUser; diff --git a/backend/src/services/WhatsappService/CreateWhatsAppService.ts b/backend/src/services/WhatsappService/CreateWhatsAppService.ts index fc2f1b2..1c2883b 100644 --- a/backend/src/services/WhatsappService/CreateWhatsAppService.ts +++ b/backend/src/services/WhatsappService/CreateWhatsAppService.ts @@ -18,7 +18,7 @@ interface Response { const CreateWhatsAppService = async ({ name, status = "OPENING", - queueIds, + queueIds = [], isDefault = false }: Request): Promise => { const schema = Yup.object().shape({ @@ -64,14 +64,19 @@ const CreateWhatsAppService = async ({ } } - const whatsapp = await Whatsapp.create({ - name, - status, - isDefault - }); + const whatsapp = await Whatsapp.create( + { + name, + status, + isDefault + }, + { include: ["queues"] } + ); await whatsapp.$set("queues", queueIds); + await whatsapp.reload(); + return { whatsapp, oldDefaultWhatsapp }; }; diff --git a/backend/src/services/WhatsappService/ShowWhatsAppService.ts b/backend/src/services/WhatsappService/ShowWhatsAppService.ts index 6a203a9..ea4a83b 100644 --- a/backend/src/services/WhatsappService/ShowWhatsAppService.ts +++ b/backend/src/services/WhatsappService/ShowWhatsAppService.ts @@ -1,8 +1,13 @@ import Whatsapp from "../../models/Whatsapp"; import AppError from "../../errors/AppError"; +import Queue from "../../models/Queue"; const ShowWhatsAppService = async (id: string | number): Promise => { - const whatsapp = await Whatsapp.findByPk(id); + const whatsapp = await Whatsapp.findByPk(id, { + include: [ + { model: Queue, as: "queues", attributes: ["id", "name", "color"] } + ] + }); if (!whatsapp) { throw new AppError("ERR_NO_WAPP_FOUND", 404); diff --git a/backend/src/services/WhatsappService/UpdateWhatsAppService.ts b/backend/src/services/WhatsappService/UpdateWhatsAppService.ts index 04c3c72..f24fbe1 100644 --- a/backend/src/services/WhatsappService/UpdateWhatsAppService.ts +++ b/backend/src/services/WhatsappService/UpdateWhatsAppService.ts @@ -3,6 +3,7 @@ import { Op } from "sequelize"; import AppError from "../../errors/AppError"; import Whatsapp from "../../models/Whatsapp"; +import ShowWhatsAppService from "./ShowWhatsAppService"; interface WhatsappData { name?: string; @@ -50,13 +51,8 @@ const UpdateWhatsAppService = async ({ } } - const whatsapp = await Whatsapp.findOne({ - where: { id: whatsappId } - }); + const whatsapp = await ShowWhatsAppService(whatsappId); - if (!whatsapp) { - throw new AppError("ERR_NO_WAPP_FOUND", 404); - } await whatsapp.update({ name, status, @@ -66,6 +62,8 @@ const UpdateWhatsAppService = async ({ await whatsapp.$set("queues", queueIds); + await whatsapp.reload(); + return { whatsapp, oldDefaultWhatsapp }; }; From cca253cd0aa708ec0f3d0978b6483f1f48a52503 Mon Sep 17 00:00:00 2001 From: canove Date: Sat, 9 Jan 2021 18:02:54 -0300 Subject: [PATCH 03/29] feat: start adding auto reply --- backend/src/controllers/QueueController.ts | 4 +- backend/src/controllers/WhatsAppController.ts | 18 +- .../20210108164404-create-queues.ts | 3 + ...20210108174594-associate-whatsapp-queue.ts | 3 + ...9192513-add-greetingMessage-to-whatsapp.ts | 13 ++ backend/src/models/Queue.ts | 6 +- backend/src/models/Whatsapp.ts | 6 + backend/src/models/WhatsappQueue.ts | 9 +- .../QueueService/AssociateWhatsappQueue.ts | 32 ++++ .../FindOrCreateTicketService.ts | 39 ++--- .../TicketServices/ShowTicketService.ts | 6 + .../WbotServices/StartAllWhatsAppsSessions.ts | 4 +- .../WbotServices/wbotMessageListener.ts | 155 ++++++++++++------ .../WhatsappService/CreateWhatsAppService.ts | 14 +- .../WhatsappService/ListWhatsAppsService.ts | 17 +- .../WhatsappService/ShowWhatsAppService.ts | 17 +- .../WhatsappService/UpdateWhatsAppService.ts | 20 ++- 17 files changed, 272 insertions(+), 94 deletions(-) create mode 100644 backend/src/database/migrations/20210109192513-add-greetingMessage-to-whatsapp.ts create mode 100644 backend/src/services/QueueService/AssociateWhatsappQueue.ts diff --git a/backend/src/controllers/QueueController.ts b/backend/src/controllers/QueueController.ts index 412840f..de489c7 100644 --- a/backend/src/controllers/QueueController.ts +++ b/backend/src/controllers/QueueController.ts @@ -11,9 +11,9 @@ export const index = async (req: Request, res: Response): Promise => { }; export const store = async (req: Request, res: Response): Promise => { - const { name, color } = req.body; + const { name, color, greetingMessage } = req.body; - const queue = await Queue.create({ name, color }); + const queue = await Queue.create({ name, color, greetingMessage }); return res.status(200).json(queue); }; diff --git a/backend/src/controllers/WhatsAppController.ts b/backend/src/controllers/WhatsAppController.ts index 2ccad1f..5759109 100644 --- a/backend/src/controllers/WhatsAppController.ts +++ b/backend/src/controllers/WhatsAppController.ts @@ -9,9 +9,14 @@ import ListWhatsAppsService from "../services/WhatsappService/ListWhatsAppsServi import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService"; import UpdateWhatsAppService from "../services/WhatsappService/UpdateWhatsAppService"; +interface QueueData { + id: number; + optionNumber: number; +} interface WhatsappData { name: string; - queueIds: number[]; + queuesData: QueueData[]; + greetingMessage?: string; status?: string; isDefault?: boolean; } @@ -23,13 +28,20 @@ export const index = async (req: Request, res: Response): Promise => { }; export const store = async (req: Request, res: Response): Promise => { - const { name, status, isDefault, queueIds }: WhatsappData = req.body; + const { + name, + status, + isDefault, + greetingMessage, + queuesData + }: WhatsappData = req.body; const { whatsapp, oldDefaultWhatsapp } = await CreateWhatsAppService({ name, status, isDefault, - queueIds + greetingMessage, + queuesData }); // StartWhatsAppSession(whatsapp); diff --git a/backend/src/database/migrations/20210108164404-create-queues.ts b/backend/src/database/migrations/20210108164404-create-queues.ts index 41681d5..4a404d6 100644 --- a/backend/src/database/migrations/20210108164404-create-queues.ts +++ b/backend/src/database/migrations/20210108164404-create-queues.ts @@ -19,6 +19,9 @@ module.exports = { allowNull: false, unique: true }, + greetingMessage: { + type: DataTypes.TEXT + }, createdAt: { type: DataTypes.DATE, allowNull: false diff --git a/backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts b/backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts index 0e08f71..0ea50f0 100644 --- a/backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts +++ b/backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts @@ -3,6 +3,9 @@ import { QueryInterface, DataTypes } from "sequelize"; module.exports = { up: (queryInterface: QueryInterface) => { return queryInterface.createTable("WhatsappQueues", { + optionNumber: { + type: DataTypes.INTEGER + }, whatsappId: { type: DataTypes.INTEGER, primaryKey: true diff --git a/backend/src/database/migrations/20210109192513-add-greetingMessage-to-whatsapp.ts b/backend/src/database/migrations/20210109192513-add-greetingMessage-to-whatsapp.ts new file mode 100644 index 0000000..6d3c3be --- /dev/null +++ b/backend/src/database/migrations/20210109192513-add-greetingMessage-to-whatsapp.ts @@ -0,0 +1,13 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Whatsapps", "greetingMessage", { + type: DataTypes.TEXT + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Whatsapps", "greetingMessage"); + } +}; diff --git a/backend/src/models/Queue.ts b/backend/src/models/Queue.ts index 65d3548..334abb2 100644 --- a/backend/src/models/Queue.ts +++ b/backend/src/models/Queue.ts @@ -8,7 +8,8 @@ import { AutoIncrement, AllowNull, Unique, - BelongsToMany + BelongsToMany, + HasMany } from "sequelize-typescript"; import User from "./User"; import UserQueue from "./UserQueue"; @@ -33,6 +34,9 @@ class Queue extends Model { @Column color: string; + @Column + greetingMessage: string; + @CreatedAt createdAt: Date; diff --git a/backend/src/models/Whatsapp.ts b/backend/src/models/Whatsapp.ts index ba6febb..3c76a0c 100644 --- a/backend/src/models/Whatsapp.ts +++ b/backend/src/models/Whatsapp.ts @@ -47,6 +47,9 @@ class Whatsapp extends Model { @Column retries: number; + @Column(DataType.TEXT) + greetingMessage: string; + @Default(false) @AllowNull @Column @@ -63,6 +66,9 @@ class Whatsapp extends Model { @BelongsToMany(() => Queue, () => WhatsappQueue) queues: Array; + + @HasMany(() => WhatsappQueue) + whatsappQueues: WhatsappQueue[]; } export default Whatsapp; diff --git a/backend/src/models/WhatsappQueue.ts b/backend/src/models/WhatsappQueue.ts index 17479cc..7886618 100644 --- a/backend/src/models/WhatsappQueue.ts +++ b/backend/src/models/WhatsappQueue.ts @@ -4,13 +4,17 @@ import { CreatedAt, UpdatedAt, Model, - ForeignKey + ForeignKey, + BelongsTo } from "sequelize-typescript"; import Queue from "./Queue"; import Whatsapp from "./Whatsapp"; @Table class WhatsappQueue extends Model { + @Column + optionNumber: number; + @ForeignKey(() => Whatsapp) @Column whatsappId: number; @@ -24,6 +28,9 @@ class WhatsappQueue extends Model { @UpdatedAt updatedAt: Date; + + @BelongsTo(() => Queue) + queue: Queue; } export default WhatsappQueue; diff --git a/backend/src/services/QueueService/AssociateWhatsappQueue.ts b/backend/src/services/QueueService/AssociateWhatsappQueue.ts new file mode 100644 index 0000000..45a5a84 --- /dev/null +++ b/backend/src/services/QueueService/AssociateWhatsappQueue.ts @@ -0,0 +1,32 @@ +import Whatsapp from "../../models/Whatsapp"; +import WhatsappQueue from "../../models/WhatsappQueue"; + +interface QueueData { + id: number; + optionNumber: number; +} + +const AssociateWhatsappQueue = async ( + whatsapp: Whatsapp, + queuesData: QueueData[] +): Promise => { + const queueIds = queuesData.map(({ id }) => id); + + await whatsapp.$set("queues", queueIds); + + /* eslint-disable no-restricted-syntax */ + /* eslint-disable no-await-in-loop */ + for (const queueData of queuesData) { + await WhatsappQueue.update( + { optionNumber: queueData.optionNumber }, + { + where: { + whatsappId: whatsapp.id, + queueId: queueData.id + } + } + ); + } +}; + +export default AssociateWhatsappQueue; diff --git a/backend/src/services/TicketServices/FindOrCreateTicketService.ts b/backend/src/services/TicketServices/FindOrCreateTicketService.ts index 9d0ba89..bf4c2b0 100644 --- a/backend/src/services/TicketServices/FindOrCreateTicketService.ts +++ b/backend/src/services/TicketServices/FindOrCreateTicketService.ts @@ -16,23 +16,19 @@ const FindOrCreateTicketService = async ( [Op.or]: ["open", "pending"] }, contactId: groupContact ? groupContact.id : contact.id - }, - include: ["contact"] + } }); if (ticket) { await ticket.update({ unreadMessages }); - - return ticket; } - if (groupContact) { + if (!ticket && groupContact) { ticket = await Ticket.findOne({ where: { contactId: groupContact.id }, - order: [["updatedAt", "DESC"]], - include: ["contact"] + order: [["updatedAt", "DESC"]] }); if (ticket) { @@ -41,10 +37,10 @@ const FindOrCreateTicketService = async ( userId: null, unreadMessages }); - - return ticket; } - } else { + } + + if (!ticket && !groupContact) { ticket = await Ticket.findOne({ where: { updatedAt: { @@ -52,8 +48,7 @@ const FindOrCreateTicketService = async ( }, contactId: contact.id }, - order: [["updatedAt", "DESC"]], - include: ["contact"] + order: [["updatedAt", "DESC"]] }); if (ticket) { @@ -62,20 +57,20 @@ const FindOrCreateTicketService = async ( userId: null, unreadMessages }); - - return ticket; } } - const { id } = await Ticket.create({ - contactId: groupContact ? groupContact.id : contact.id, - status: "pending", - isGroup: !!groupContact, - unreadMessages, - whatsappId - }); + if (!ticket) { + ticket = await Ticket.create({ + contactId: groupContact ? groupContact.id : contact.id, + status: "pending", + isGroup: !!groupContact, + unreadMessages, + whatsappId + }); + } - ticket = await ShowTicketService(id); + ticket = await ShowTicketService(ticket.id); return ticket; }; diff --git a/backend/src/services/TicketServices/ShowTicketService.ts b/backend/src/services/TicketServices/ShowTicketService.ts index dd0cc30..5efab0c 100644 --- a/backend/src/services/TicketServices/ShowTicketService.ts +++ b/backend/src/services/TicketServices/ShowTicketService.ts @@ -2,6 +2,7 @@ import Ticket from "../../models/Ticket"; import AppError from "../../errors/AppError"; import Contact from "../../models/Contact"; import User from "../../models/User"; +import Queue from "../../models/Queue"; const ShowTicketService = async (id: string | number): Promise => { const ticket = await Ticket.findByPk(id, { @@ -16,6 +17,11 @@ const ShowTicketService = async (id: string | number): Promise => { model: User, as: "user", attributes: ["id", "name"] + }, + { + model: Queue, + as: "queue", + attributes: ["id", "name", "color"] } ] }); diff --git a/backend/src/services/WbotServices/StartAllWhatsAppsSessions.ts b/backend/src/services/WbotServices/StartAllWhatsAppsSessions.ts index a472f9f..9e5e935 100644 --- a/backend/src/services/WbotServices/StartAllWhatsAppsSessions.ts +++ b/backend/src/services/WbotServices/StartAllWhatsAppsSessions.ts @@ -1,8 +1,8 @@ -import Whatsapp from "../../models/Whatsapp"; +import ListWhatsAppsService from "../WhatsappService/ListWhatsAppsService"; import { StartWhatsAppSession } from "./StartWhatsAppSession"; export const StartAllWhatsAppsSessions = async (): Promise => { - const whatsapps = await Whatsapp.findAll(); + const whatsapps = await ListWhatsAppsService(); if (whatsapps.length > 0) { whatsapps.forEach(whatsapp => { StartWhatsAppSession(whatsapp); diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index ac687e4..76f8904 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -19,6 +19,7 @@ import CreateMessageService from "../MessageServices/CreateMessageService"; import { logger } from "../../utils/logger"; import CreateOrUpdateContactService from "../ContactServices/CreateOrUpdateContactService"; import FindOrCreateTicketService from "../TicketServices/FindOrCreateTicketService"; +import ShowWhatsAppService from "../WhatsappService/ShowWhatsAppService"; interface Session extends Client { id?: number; @@ -126,6 +127,54 @@ const verifyMessage = async ( await CreateMessageService({ messageData }); }; +const verifyQueue = async ( + wbot: Session, + msg: WbotMessage, + ticket: Ticket, + contact: Contact +) => { + const { whatsappQueues, greetingMessage } = await ShowWhatsAppService( + wbot.id! + ); + + if (whatsappQueues.length === 1) { + await ticket.$set("queue", whatsappQueues[0].queue); + // TODO sendTicketQueueUpdate to frontend + + return; + } + + const selectedOption = msg.body[0]; + + const validOption = whatsappQueues.find( + q => q.optionNumber === +selectedOption + ); + + if (validOption) { + await ticket.$set("queue", validOption.queue); + + const body = `\u200e ${validOption.queue.greetingMessage}`; + + const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body); + + await verifyMessage(sentMessage, ticket, contact); + + // TODO sendTicketQueueUpdate to frontend + } else { + let options = ""; + + whatsappQueues.forEach(whatsQueue => { + options += `*${whatsQueue.optionNumber}* - ${whatsQueue.queue.name}\n`; + }); + + const body = `\u200e ${greetingMessage}\n\n${options}`; + + const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body); + + await verifyMessage(sentMessage, ticket, contact); + } +}; + const isValidMsg = (msg: WbotMessage): boolean => { if (msg.from === "status@broadcast") return false; if ( @@ -146,64 +195,64 @@ const handleMessage = async ( msg: WbotMessage, wbot: Session ): Promise => { - return new Promise((resolve, reject) => { - (async () => { - if (!isValidMsg(msg)) { - return; + if (!isValidMsg(msg)) { + return; + } + + try { + let msgContact: WbotContact; + let groupContact: Contact | undefined; + + if (msg.fromMe) { + // messages sent automatically by wbot have a special character in front of it + // if so, this message was already been stored in database; + if (/\u200e/.test(msg.body[0])) return; + + // media messages sent from me from cell phone, first comes with "hasMedia = false" and type = "image/ptt/etc" + // in this case, return and let this message be handled by "media_uploaded" event, when it will have "hasMedia = true" + + if (!msg.hasMedia && msg.type !== "chat" && msg.type !== "vcard") return; + + msgContact = await wbot.getContactById(msg.to); + } else { + msgContact = await msg.getContact(); + } + + const chat = await msg.getChat(); + + if (chat.isGroup) { + let msgGroupContact; + + if (msg.fromMe) { + msgGroupContact = await wbot.getContactById(msg.to); + } else { + msgGroupContact = await wbot.getContactById(msg.from); } - try { - let msgContact: WbotContact; - let groupContact: Contact | undefined; + groupContact = await verifyContact(msgGroupContact); + } - if (msg.fromMe) { - // media messages sent from me from cell phone, first comes with "hasMedia = false" and type = "image/ptt/etc" - // in this case, return and let this message be handled by "media_uploaded" event, when it will have "hasMedia = true" + const contact = await verifyContact(msgContact); + const ticket = await FindOrCreateTicketService( + contact, + wbot.id!, + chat.unreadCount, + groupContact + ); - if (!msg.hasMedia && msg.type !== "chat" && msg.type !== "vcard") - return; + if (msg.hasMedia) { + await verifyMediaMessage(msg, ticket, contact); + } else { + await verifyMessage(msg, ticket, contact); + } - msgContact = await wbot.getContactById(msg.to); - } else { - msgContact = await msg.getContact(); - } - - const chat = await msg.getChat(); - - if (chat.isGroup) { - let msgGroupContact; - - if (msg.fromMe) { - msgGroupContact = await wbot.getContactById(msg.to); - } else { - msgGroupContact = await wbot.getContactById(msg.from); - } - - groupContact = await verifyContact(msgGroupContact); - } - - const contact = await verifyContact(msgContact); - const ticket = await FindOrCreateTicketService( - contact, - wbot.id!, - chat.unreadCount, - groupContact - ); - - if (msg.hasMedia) { - await verifyMediaMessage(msg, ticket, contact); - resolve(); - } else { - await verifyMessage(msg, ticket, contact); - resolve(); - } - } catch (err) { - Sentry.captureException(err); - logger.error(`Error handling whatsapp message: Err: ${err}`); - reject(err); - } - })(); - }); + if (!ticket.queue && !chat.isGroup && !msg.fromMe) { + await verifyQueue(wbot, msg, ticket, contact); + } + } catch (err) { + Sentry.captureException(err); + logger.error(`Error handling whatsapp message: Err: ${err}`); + } }; const handleMsgAck = async (msg: WbotMessage, ack: MessageAck) => { diff --git a/backend/src/services/WhatsappService/CreateWhatsAppService.ts b/backend/src/services/WhatsappService/CreateWhatsAppService.ts index 1c2883b..f1375e5 100644 --- a/backend/src/services/WhatsappService/CreateWhatsAppService.ts +++ b/backend/src/services/WhatsappService/CreateWhatsAppService.ts @@ -2,10 +2,16 @@ import * as Yup from "yup"; import AppError from "../../errors/AppError"; import Whatsapp from "../../models/Whatsapp"; +import AssociateWhatsappQueue from "../QueueService/AssociateWhatsappQueue"; +interface QueueData { + id: number; + optionNumber: number; +} interface Request { name: string; - queueIds: number[]; + queuesData: QueueData[]; + greetingMessage?: string; status?: string; isDefault?: boolean; } @@ -18,7 +24,8 @@ interface Response { const CreateWhatsAppService = async ({ name, status = "OPENING", - queueIds = [], + queuesData = [], + greetingMessage, isDefault = false }: Request): Promise => { const schema = Yup.object().shape({ @@ -68,12 +75,13 @@ const CreateWhatsAppService = async ({ { name, status, + greetingMessage, isDefault }, { include: ["queues"] } ); - await whatsapp.$set("queues", queueIds); + await AssociateWhatsappQueue(whatsapp, queuesData); await whatsapp.reload(); diff --git a/backend/src/services/WhatsappService/ListWhatsAppsService.ts b/backend/src/services/WhatsappService/ListWhatsAppsService.ts index 5a1817a..0ac127c 100644 --- a/backend/src/services/WhatsappService/ListWhatsAppsService.ts +++ b/backend/src/services/WhatsappService/ListWhatsAppsService.ts @@ -1,11 +1,24 @@ import Queue from "../../models/Queue"; import Whatsapp from "../../models/Whatsapp"; +import WhatsappQueue from "../../models/WhatsappQueue"; const ListWhatsAppsService = async (): Promise => { const whatsapps = await Whatsapp.findAll({ include: [ - { model: Queue, as: "queues", attributes: ["id", "name", "color"] } - ] + { + model: WhatsappQueue, + as: "whatsappQueues", + attributes: ["optionNumber"], + include: [ + { + model: Queue, + as: "queue", + attributes: ["id", "name", "color", "greetingMessage"] + } + ] + } + ], + order: [["whatsappQueues", "optionNumber", "ASC"]] }); return whatsapps; diff --git a/backend/src/services/WhatsappService/ShowWhatsAppService.ts b/backend/src/services/WhatsappService/ShowWhatsAppService.ts index ea4a83b..5643032 100644 --- a/backend/src/services/WhatsappService/ShowWhatsAppService.ts +++ b/backend/src/services/WhatsappService/ShowWhatsAppService.ts @@ -1,12 +1,25 @@ import Whatsapp from "../../models/Whatsapp"; import AppError from "../../errors/AppError"; import Queue from "../../models/Queue"; +import WhatsappQueue from "../../models/WhatsappQueue"; const ShowWhatsAppService = async (id: string | number): Promise => { const whatsapp = await Whatsapp.findByPk(id, { include: [ - { model: Queue, as: "queues", attributes: ["id", "name", "color"] } - ] + { + model: WhatsappQueue, + as: "whatsappQueues", + attributes: ["optionNumber"], + include: [ + { + model: Queue, + as: "queue", + attributes: ["id", "name", "color", "greetingMessage"] + } + ] + } + ], + order: [["whatsappQueues", "optionNumber", "ASC"]] }); if (!whatsapp) { diff --git a/backend/src/services/WhatsappService/UpdateWhatsAppService.ts b/backend/src/services/WhatsappService/UpdateWhatsAppService.ts index f24fbe1..417d8b2 100644 --- a/backend/src/services/WhatsappService/UpdateWhatsAppService.ts +++ b/backend/src/services/WhatsappService/UpdateWhatsAppService.ts @@ -4,13 +4,19 @@ import { Op } from "sequelize"; import AppError from "../../errors/AppError"; import Whatsapp from "../../models/Whatsapp"; import ShowWhatsAppService from "./ShowWhatsAppService"; +import AssociateWhatsappQueue from "../QueueService/AssociateWhatsappQueue"; +interface QueueData { + id: number; + optionNumber: number; +} interface WhatsappData { name?: string; status?: string; session?: string; isDefault?: boolean; - queueIds?: number[]; + greetingMessage?: string; + queuesData?: QueueData[]; } interface Request { @@ -32,7 +38,14 @@ const UpdateWhatsAppService = async ({ isDefault: Yup.boolean() }); - const { name, status, isDefault, session, queueIds = [] } = whatsappData; + const { + name, + status, + isDefault, + session, + greetingMessage, + queuesData = [] + } = whatsappData; try { await schema.validate({ name, status, isDefault }); @@ -57,10 +70,11 @@ const UpdateWhatsAppService = async ({ name, status, session, + greetingMessage, isDefault }); - await whatsapp.$set("queues", queueIds); + await AssociateWhatsappQueue(whatsapp, queuesData); await whatsapp.reload(); From d374c84b605dc1345c503c4527b6db5facb8dd3f Mon Sep 17 00:00:00 2001 From: canove Date: Sun, 10 Jan 2021 10:59:01 -0300 Subject: [PATCH 04/29] feat: finished queues crud services --- backend/src/app.ts | 6 +- backend/src/controllers/QueueController.ts | 43 +++++++++-- backend/src/routes/queueRoutes.ts | 6 ++ .../QueueService/CreateQueueService.ts | 67 +++++++++++++++++ .../QueueService/DeleteQueueService.ts | 9 +++ .../QueueService/ListQueuesService.ts | 9 +++ .../services/QueueService/ShowQueueService.ts | 14 ++++ .../QueueService/UpdateQueueService.ts | 73 +++++++++++++++++++ 8 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 backend/src/services/QueueService/CreateQueueService.ts create mode 100644 backend/src/services/QueueService/DeleteQueueService.ts create mode 100644 backend/src/services/QueueService/ListQueuesService.ts create mode 100644 backend/src/services/QueueService/ShowQueueService.ts create mode 100644 backend/src/services/QueueService/UpdateQueueService.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index 95c5f67..dd8155b 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -32,11 +32,7 @@ app.use(Sentry.Handlers.errorHandler()); app.use(async (err: Error, req: Request, res: Response, _: NextFunction) => { if (err instanceof AppError) { - if (err.statusCode === 403) { - logger.warn(err); - } else { - logger.error(err); - } + logger.warn(err); return res.status(err.statusCode).json({ error: err.message }); } diff --git a/backend/src/controllers/QueueController.ts b/backend/src/controllers/QueueController.ts index de489c7..9dbad1b 100644 --- a/backend/src/controllers/QueueController.ts +++ b/backend/src/controllers/QueueController.ts @@ -1,11 +1,12 @@ import { Request, Response } from "express"; -import Queue from "../models/Queue"; - -// import { getIO } from "../libs/socket"; -// import AppError from "../errors/AppError"; +import CreateQueueService from "../services/QueueService/CreateQueueService"; +import DeleteQueueService from "../services/QueueService/DeleteQueueService"; +import ListQueuesService from "../services/QueueService/ListQueuesService"; +import ShowQueueService from "../services/QueueService/ShowQueueService"; +import UpdateQueueService from "../services/QueueService/UpdateQueueService"; export const index = async (req: Request, res: Response): Promise => { - const queues = await Queue.findAll(); + const queues = await ListQueuesService(); return res.status(200).json(queues); }; @@ -13,7 +14,37 @@ export const index = async (req: Request, res: Response): Promise => { export const store = async (req: Request, res: Response): Promise => { const { name, color, greetingMessage } = req.body; - const queue = await Queue.create({ name, color, greetingMessage }); + const queue = await CreateQueueService({ name, color, greetingMessage }); return res.status(200).json(queue); }; + +export const show = async (req: Request, res: Response): Promise => { + const { queueId } = req.params; + + const queue = await ShowQueueService(queueId); + + return res.status(200).json(queue); +}; + +export const update = async ( + req: Request, + res: Response +): Promise => { + const { queueId } = req.params; + + const queue = await UpdateQueueService(queueId, req.body); + + return res.status(201).json(queue); +}; + +export const remove = async ( + req: Request, + res: Response +): Promise => { + const { queueId } = req.params; + + await DeleteQueueService(queueId); + + return res.status(200).send(); +}; diff --git a/backend/src/routes/queueRoutes.ts b/backend/src/routes/queueRoutes.ts index 51e793d..a85f5e3 100644 --- a/backend/src/routes/queueRoutes.ts +++ b/backend/src/routes/queueRoutes.ts @@ -9,4 +9,10 @@ queueRoutes.get("/queue", isAuth, QueueController.index); queueRoutes.post("/queue", isAuth, QueueController.store); +queueRoutes.get("/queue/:queueId", isAuth, QueueController.show); + +queueRoutes.put("/queue/:queueId", isAuth, QueueController.update); + +queueRoutes.delete("/queue/:queueId", isAuth, QueueController.remove); + export default queueRoutes; diff --git a/backend/src/services/QueueService/CreateQueueService.ts b/backend/src/services/QueueService/CreateQueueService.ts new file mode 100644 index 0000000..57881e1 --- /dev/null +++ b/backend/src/services/QueueService/CreateQueueService.ts @@ -0,0 +1,67 @@ +import * as Yup from "yup"; +import AppError from "../../errors/AppError"; +import Queue from "../../models/Queue"; + +interface QueueData { + name: string; + color: string; + greetingMessage?: string; +} + +const CreateQueueService = async (queueData: QueueData): Promise => { + const { color, name } = queueData; + + const queueSchema = Yup.object().shape({ + name: Yup.string() + .min(2, "ERR_QUEUE_INVALID_NAME") + .required("ERR_QUEUE_INVALID_NAME") + .test( + "Check-unique-name", + "ERR_QUEUE_NAME_ALREADY_EXISTS", + async value => { + if (value) { + const queueWithSameName = await Queue.findOne({ + where: { name: value } + }); + + return !queueWithSameName; + } + return false; + } + ), + color: Yup.string() + .required("ERR_QUEUE_INVALID_COLOR") + .test("Check-color", "ERR_QUEUE_INVALID_COLOR", async value => { + if (value) { + const colorTestRegex = /^#[0-9a-f]{3,6}$/i; + return colorTestRegex.test(value); + } + return false; + }) + .test( + "Check-color-exists", + "ERR_QUEUE_COLOR_ALREADY_EXISTS", + async value => { + if (value) { + const queueWithSameColor = await Queue.findOne({ + where: { color: value } + }); + return !queueWithSameColor; + } + return false; + } + ) + }); + + try { + await queueSchema.validate({ color, name }); + } catch (err) { + throw new AppError(err.message); + } + + const queue = await Queue.create(queueData); + + return queue; +}; + +export default CreateQueueService; diff --git a/backend/src/services/QueueService/DeleteQueueService.ts b/backend/src/services/QueueService/DeleteQueueService.ts new file mode 100644 index 0000000..fcf9ef6 --- /dev/null +++ b/backend/src/services/QueueService/DeleteQueueService.ts @@ -0,0 +1,9 @@ +import ShowQueueService from "./ShowQueueService"; + +const DeleteQueueService = async (queueId: number | string): Promise => { + const queue = await ShowQueueService(queueId); + + await queue.destroy(); +}; + +export default DeleteQueueService; diff --git a/backend/src/services/QueueService/ListQueuesService.ts b/backend/src/services/QueueService/ListQueuesService.ts new file mode 100644 index 0000000..9a9a3a1 --- /dev/null +++ b/backend/src/services/QueueService/ListQueuesService.ts @@ -0,0 +1,9 @@ +import Queue from "../../models/Queue"; + +const ListQueuesService = async (): Promise => { + const queues = await Queue.findAll(); + + return queues; +}; + +export default ListQueuesService; diff --git a/backend/src/services/QueueService/ShowQueueService.ts b/backend/src/services/QueueService/ShowQueueService.ts new file mode 100644 index 0000000..16ade45 --- /dev/null +++ b/backend/src/services/QueueService/ShowQueueService.ts @@ -0,0 +1,14 @@ +import AppError from "../../errors/AppError"; +import Queue from "../../models/Queue"; + +const ShowQueueService = async (queueId: number | string): Promise => { + const queue = await Queue.findByPk(queueId); + + if (!queue) { + throw new AppError("ERR_QUEUE_NOT_FOUND"); + } + + return queue; +}; + +export default ShowQueueService; diff --git a/backend/src/services/QueueService/UpdateQueueService.ts b/backend/src/services/QueueService/UpdateQueueService.ts new file mode 100644 index 0000000..8aa2a23 --- /dev/null +++ b/backend/src/services/QueueService/UpdateQueueService.ts @@ -0,0 +1,73 @@ +import { Op } from "sequelize"; +import * as Yup from "yup"; +import AppError from "../../errors/AppError"; +import Queue from "../../models/Queue"; +import ShowQueueService from "./ShowQueueService"; + +interface QueueData { + name?: string; + color?: string; + greetingMessage?: string; +} + +const UpdateQueueService = async ( + queueId: number | string, + queueData: QueueData +): Promise => { + const { color, name } = queueData; + + const queueSchema = Yup.object().shape({ + name: Yup.string() + .min(2, "ERR_QUEUE_INVALID_NAME") + .test( + "Check-unique-name", + "ERR_QUEUE_NAME_ALREADY_EXISTS", + async value => { + if (value) { + const queueWithSameName = await Queue.findOne({ + where: { name: value, id: { [Op.not]: queueId } } + }); + + return !queueWithSameName; + } + return true; + } + ), + color: Yup.string() + .required("ERR_QUEUE_INVALID_COLOR") + .test("Check-color", "ERR_QUEUE_INVALID_COLOR", async value => { + if (value) { + const colorTestRegex = /^#[0-9a-f]{3,6}$/i; + return colorTestRegex.test(value); + } + return true; + }) + .test( + "Check-color-exists", + "ERR_QUEUE_COLOR_ALREADY_EXISTS", + async value => { + if (value) { + const queueWithSameColor = await Queue.findOne({ + where: { color: value, id: { [Op.not]: queueId } } + }); + return !queueWithSameColor; + } + return true; + } + ) + }); + + try { + await queueSchema.validate({ color, name }); + } catch (err) { + throw new AppError(err.message); + } + + const queue = await ShowQueueService(queueId); + + await queue.update(queueData); + + return queue; +}; + +export default UpdateQueueService; From 8ef50a76ca212307836bd0e5cc02d1fb44258f41 Mon Sep 17 00:00:00 2001 From: canove Date: Sun, 10 Jan 2021 11:07:10 -0300 Subject: [PATCH 05/29] improvement: rejoin websocket channels after reconnecting --- frontend/src/components/MessagesList/index.js | 3 ++- .../src/components/NotificationsPopOver/index.js | 2 +- frontend/src/components/Ticket/index.js | 3 ++- frontend/src/components/TicketsList/index.js | 13 ++++++++----- frontend/src/pages/Contacts/index.js | 1 + frontend/src/pages/Settings/index.js | 1 + frontend/src/pages/Users/index.js | 1 + 7 files changed, 16 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/MessagesList/index.js b/frontend/src/components/MessagesList/index.js index 3cb168e..d3fa2a0 100644 --- a/frontend/src/components/MessagesList/index.js +++ b/frontend/src/components/MessagesList/index.js @@ -354,7 +354,8 @@ const MessagesList = ({ ticketId, isGroup, setReplyingMessage }) => { useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL); - socket.emit("joinChatBox", ticketId); + + socket.on("connect", () => socket.emit("joinChatBox", ticketId)); socket.on("appMessage", data => { if (data.action === "create") { diff --git a/frontend/src/components/NotificationsPopOver/index.js b/frontend/src/components/NotificationsPopOver/index.js index 38aa822..4c408aa 100644 --- a/frontend/src/components/NotificationsPopOver/index.js +++ b/frontend/src/components/NotificationsPopOver/index.js @@ -77,7 +77,7 @@ const NotificationsPopOver = () => { useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL); - socket.emit("joinNotification"); + socket.on("connect", () => socket.emit("joinNotification")); socket.on("ticket", data => { if (data.action === "updateUnread" || data.action === "delete") { diff --git a/frontend/src/components/Ticket/index.js b/frontend/src/components/Ticket/index.js index 87c04e7..bf694ee 100644 --- a/frontend/src/components/Ticket/index.js +++ b/frontend/src/components/Ticket/index.js @@ -86,7 +86,8 @@ const Ticket = () => { useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL); - socket.emit("joinChatBox", ticketId); + + socket.on("connect", () => socket.emit("joinChatBox", ticketId)); socket.on("ticket", data => { if (data.action === "updateStatus") { diff --git a/frontend/src/components/TicketsList/index.js b/frontend/src/components/TicketsList/index.js index 458081d..addd137 100644 --- a/frontend/src/components/TicketsList/index.js +++ b/frontend/src/components/TicketsList/index.js @@ -175,11 +175,14 @@ const TicketsList = ({ status, searchParam, showAll }) => { useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL); - if (status) { - socket.emit("joinTickets", status); - } else { - socket.emit("joinNotification"); - } + + socket.on("connect", () => { + if (status) { + socket.emit("joinTickets", status); + } else { + socket.emit("joinNotification"); + } + }); socket.on("ticket", data => { if (data.action === "updateUnread") { diff --git a/frontend/src/pages/Contacts/index.js b/frontend/src/pages/Contacts/index.js index 6bb0f24..302b846 100644 --- a/frontend/src/pages/Contacts/index.js +++ b/frontend/src/pages/Contacts/index.js @@ -128,6 +128,7 @@ const Contacts = () => { useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + socket.on("contact", data => { if (data.action === "update" || data.action === "create") { dispatch({ type: "UPDATE_CONTACTS", payload: data.contact }); diff --git a/frontend/src/pages/Settings/index.js b/frontend/src/pages/Settings/index.js index 4fd64ab..2fc4097 100644 --- a/frontend/src/pages/Settings/index.js +++ b/frontend/src/pages/Settings/index.js @@ -52,6 +52,7 @@ const Settings = () => { useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + socket.on("settings", data => { if (data.action === "update") { setSettings(prevState => { diff --git a/frontend/src/pages/Users/index.js b/frontend/src/pages/Users/index.js index 7f4d440..d8817a3 100644 --- a/frontend/src/pages/Users/index.js +++ b/frontend/src/pages/Users/index.js @@ -124,6 +124,7 @@ const Users = () => { useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + socket.on("user", data => { if (data.action === "update" || data.action === "create") { dispatch({ type: "UPDATE_USERS", payload: data.user }); From e34305b976d370713118f0b8af1a6618ab26fc10 Mon Sep 17 00:00:00 2001 From: canove Date: Sun, 10 Jan 2021 11:12:44 -0300 Subject: [PATCH 06/29] chore: moved layout to src folder --- .../{components/_layout => layout}/MainListItems.js | 8 +++----- frontend/src/{components/_layout => layout}/index.js | 10 +++++----- frontend/src/pages/Queues/index.js | 7 +++++++ frontend/src/routes/index.js | 4 +++- 4 files changed, 18 insertions(+), 11 deletions(-) rename frontend/src/{components/_layout => layout}/MainListItems.js (92%) rename frontend/src/{components/_layout => layout}/index.js (91%) create mode 100644 frontend/src/pages/Queues/index.js diff --git a/frontend/src/components/_layout/MainListItems.js b/frontend/src/layout/MainListItems.js similarity index 92% rename from frontend/src/components/_layout/MainListItems.js rename to frontend/src/layout/MainListItems.js index 2b1e4b4..cec6a3c 100644 --- a/frontend/src/components/_layout/MainListItems.js +++ b/frontend/src/layout/MainListItems.js @@ -6,18 +6,16 @@ import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemText from "@material-ui/core/ListItemText"; import ListSubheader from "@material-ui/core/ListSubheader"; import Divider from "@material-ui/core/Divider"; +import { Badge } from "@material-ui/core"; import DashboardIcon from "@material-ui/icons/Dashboard"; import WhatsAppIcon from "@material-ui/icons/WhatsApp"; import SyncAltIcon from "@material-ui/icons/SyncAlt"; import SettingsIcon from "@material-ui/icons/Settings"; import GroupIcon from "@material-ui/icons/Group"; - import ContactPhoneIcon from "@material-ui/icons/ContactPhone"; -import { i18n } from "../../translate/i18n"; -import { Badge } from "@material-ui/core"; - -import { WhatsAppsContext } from "../../context/WhatsApp/WhatsAppsContext"; +import { i18n } from "../translate/i18n"; +import { WhatsAppsContext } from "../context/WhatsApp/WhatsAppsContext"; function ListItemLink(props) { const { icon, primary, to, className } = props; diff --git a/frontend/src/components/_layout/index.js b/frontend/src/layout/index.js similarity index 91% rename from frontend/src/components/_layout/index.js rename to frontend/src/layout/index.js index bff2c59..5693c11 100644 --- a/frontend/src/components/_layout/index.js +++ b/frontend/src/layout/index.js @@ -19,11 +19,11 @@ import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; import AccountCircle from "@material-ui/icons/AccountCircle"; import MainListItems from "./MainListItems"; -import NotificationsPopOver from "../NotificationsPopOver"; -import UserModal from "../UserModal"; -import { AuthContext } from "../../context/Auth/AuthContext"; -import BackdropLoading from "../BackdropLoading"; -import { i18n } from "../../translate/i18n"; +import NotificationsPopOver from "../components/NotificationsPopOver"; +import UserModal from "../components/UserModal"; +import { AuthContext } from "../context/Auth/AuthContext"; +import BackdropLoading from "../components/BackdropLoading"; +import { i18n } from "../translate/i18n"; const drawerWidth = 240; diff --git a/frontend/src/pages/Queues/index.js b/frontend/src/pages/Queues/index.js new file mode 100644 index 0000000..b7e1581 --- /dev/null +++ b/frontend/src/pages/Queues/index.js @@ -0,0 +1,7 @@ +import React from "react"; + +const Queues = () => { + return
; +}; + +export default Queues; diff --git a/frontend/src/routes/index.js b/frontend/src/routes/index.js index 852e0c9..46b6cad 100644 --- a/frontend/src/routes/index.js +++ b/frontend/src/routes/index.js @@ -2,7 +2,7 @@ import React from "react"; import { BrowserRouter, Switch } from "react-router-dom"; import { ToastContainer } from "react-toastify"; -import LoggedInLayout from "../components/_layout"; +import LoggedInLayout from "../layout"; import Dashboard from "../pages/Dashboard/"; import Tickets from "../pages/Tickets/"; import Signup from "../pages/Signup/"; @@ -11,6 +11,7 @@ import Connections from "../pages/Connections/"; import Settings from "../pages/Settings/"; import Users from "../pages/Users"; import Contacts from "../pages/Contacts/"; +import Queues from "../pages/Queues/"; import { AuthProvider } from "../context/Auth/AuthContext"; import { WhatsAppsProvider } from "../context/WhatsApp/WhatsAppsContext"; import Route from "./Route"; @@ -40,6 +41,7 @@ const Routes = () => { + From e64a77513f1efb3bdf21af420f8f45fdb2837e09 Mon Sep 17 00:00:00 2001 From: canove Date: Sun, 10 Jan 2021 16:18:23 -0300 Subject: [PATCH 07/29] feat: start adding queues page --- frontend/package.json | 1 + frontend/src/components/ColorPicker/index.js | 25 ++ .../src/components/ConfirmationModal/index.js | 8 +- .../components/MessageOptionsMenu/index.js | 2 +- frontend/src/components/QueueModal/index.js | 238 ++++++++++++++++++ .../src/components/TicketOptionsMenu/index.js | 2 +- frontend/src/layout/MainListItems.js | 26 +- frontend/src/pages/Connections/index.js | 2 +- frontend/src/pages/Contacts/index.js | 2 +- frontend/src/pages/Queues/index.js | 203 ++++++++++++++- frontend/src/pages/Users/index.js | 2 +- frontend/src/translate/languages/en.js | 34 +++ frontend/src/translate/languages/es.js | 34 +++ frontend/src/translate/languages/pt.js | 34 +++ 14 files changed, 594 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/ColorPicker/index.js create mode 100644 frontend/src/components/QueueModal/index.js diff --git a/frontend/package.json b/frontend/package.json index 078c767..9c27aa8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "mic-recorder-to-mp3": "^2.2.2", "qrcode.react": "^1.0.0", "react": "^16.13.1", + "react-color": "^2.19.3", "react-dom": "^16.13.1", "react-modal-image": "^2.5.0", "react-router-dom": "^5.2.0", diff --git a/frontend/src/components/ColorPicker/index.js b/frontend/src/components/ColorPicker/index.js new file mode 100644 index 0000000..fdd5dbd --- /dev/null +++ b/frontend/src/components/ColorPicker/index.js @@ -0,0 +1,25 @@ +import React, { useState } from "react"; + +import { GithubPicker } from "react-color"; + +const ColorPicker = ({ onChange, currentColor }) => { + const [color, setColor] = useState(currentColor); + + const handleChange = color => { + setColor(color.hex); + }; + + return ( +
+ onChange(color.hex)} + /> +
+ ); +}; + +export default ColorPicker; diff --git a/frontend/src/components/ConfirmationModal/index.js b/frontend/src/components/ConfirmationModal/index.js index ce5681d..ce340f2 100644 --- a/frontend/src/components/ConfirmationModal/index.js +++ b/frontend/src/components/ConfirmationModal/index.js @@ -8,11 +8,11 @@ import Typography from "@material-ui/core/Typography"; import { i18n } from "../../translate/i18n"; -const ConfirmationModal = ({ title, children, open, setOpen, onConfirm }) => { +const ConfirmationModal = ({ title, children, open, onClose, onConfirm }) => { return ( setOpen(false)} + onClose={() => onClose(false)} aria-labelledby="confirm-dialog" > {title} @@ -22,7 +22,7 @@ const ConfirmationModal = ({ title, children, open, setOpen, onConfirm }) => { + + + + )} + + + + ); +}; + +export default QueueModal; diff --git a/frontend/src/components/TicketOptionsMenu/index.js b/frontend/src/components/TicketOptionsMenu/index.js index 8087a89..48d1035 100644 --- a/frontend/src/components/TicketOptionsMenu/index.js +++ b/frontend/src/components/TicketOptionsMenu/index.js @@ -76,7 +76,7 @@ const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { ticket.contact.name }?`} open={confirmationOpen} - setOpen={setConfirmationOpen} + onClose={setConfirmationOpen} onConfirm={handleDeleteTicket} > {i18n.t("ticketOptionsMenu.confirmationModal.message")} diff --git a/frontend/src/layout/MainListItems.js b/frontend/src/layout/MainListItems.js index cec6a3c..dcc22a6 100644 --- a/frontend/src/layout/MainListItems.js +++ b/frontend/src/layout/MainListItems.js @@ -7,12 +7,13 @@ import ListItemText from "@material-ui/core/ListItemText"; import ListSubheader from "@material-ui/core/ListSubheader"; import Divider from "@material-ui/core/Divider"; import { Badge } from "@material-ui/core"; -import DashboardIcon from "@material-ui/icons/Dashboard"; +import DashboardOutlinedIcon from "@material-ui/icons/DashboardOutlined"; import WhatsAppIcon from "@material-ui/icons/WhatsApp"; import SyncAltIcon from "@material-ui/icons/SyncAlt"; -import SettingsIcon from "@material-ui/icons/Settings"; -import GroupIcon from "@material-ui/icons/Group"; -import ContactPhoneIcon from "@material-ui/icons/ContactPhone"; +import SettingsOutlinedIcon from "@material-ui/icons/SettingsOutlined"; +import PeopleAltOutlinedIcon from "@material-ui/icons/PeopleAltOutlined"; +import ContactPhoneOutlinedIcon from "@material-ui/icons/ContactPhoneOutlined"; +import AccountTreeOutlinedIcon from "@material-ui/icons/AccountTreeOutlined"; import { i18n } from "../translate/i18n"; import { WhatsAppsContext } from "../context/WhatsApp/WhatsAppsContext"; @@ -69,7 +70,11 @@ const MainListItems = () => { return (
- } /> + } + /> { } + icon={} /> {userProfile === "admin" && ( <> @@ -99,12 +104,17 @@ const MainListItems = () => { } + icon={} + /> + } /> } + icon={} /> )} diff --git a/frontend/src/pages/Connections/index.js b/frontend/src/pages/Connections/index.js index f1f5fb0..8974ce2 100644 --- a/frontend/src/pages/Connections/index.js +++ b/frontend/src/pages/Connections/index.js @@ -294,7 +294,7 @@ const Connections = () => { {confirmModalInfo.message} diff --git a/frontend/src/pages/Contacts/index.js b/frontend/src/pages/Contacts/index.js index 302b846..7f12e2e 100644 --- a/frontend/src/pages/Contacts/index.js +++ b/frontend/src/pages/Contacts/index.js @@ -229,7 +229,7 @@ const Contacts = () => { : `${i18n.t("contacts.confirmationModal.importTitlte")}` } open={confirmOpen} - setOpen={setConfirmOpen} + onClose={setConfirmOpen} onConfirm={e => deletingContact ? handleDeleteContact(deletingContact.id) diff --git a/frontend/src/pages/Queues/index.js b/frontend/src/pages/Queues/index.js index b7e1581..b9d87ba 100644 --- a/frontend/src/pages/Queues/index.js +++ b/frontend/src/pages/Queues/index.js @@ -1,7 +1,206 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; + +import { + Button, + IconButton, + makeStyles, + Paper, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, +} from "@material-ui/core"; + +import MainContainer from "../../components/MainContainer"; +import MainHeader from "../../components/MainHeader"; +import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper"; +import TableRowSkeleton from "../../components/TableRowSkeleton"; +import Title from "../../components/Title"; +import { i18n } from "../../translate/i18n"; +import toastError from "../../errors/toastError"; +import api from "../../services/api"; +import { DeleteOutline, Edit } from "@material-ui/icons"; +import QueueModal from "../../components/QueueModal"; +import { toast } from "react-toastify"; +import ConfirmationModal from "../../components/ConfirmationModal"; + +const useStyles = makeStyles(theme => ({ + mainPaper: { + flex: 1, + padding: theme.spacing(1), + overflowY: "scroll", + ...theme.scrollbarStyles, + }, + customTableCell: { + display: "flex", + + alignItems: "center", + justifyContent: "center", + }, +})); const Queues = () => { - return
; + const classes = useStyles(); + + const [queues, setQueue] = useState([]); + const [loading, setLoading] = useState(false); + + const [queueModalOpen, setQueueModalOpen] = useState(false); + const [selectedQueue, setSelectedQueue] = useState(null); + const [confirmModalOpen, setConfirmModalOpen] = useState(false); + + useEffect(() => { + (async () => { + setLoading(true); + try { + const { data } = await api.get("/queue"); + + setQueue(data); + setLoading(false); + } catch (err) { + toastError(err); + setLoading(false); + } + })(); + }, []); + + const handleOpenQueueModal = () => { + setQueueModalOpen(true); + setSelectedQueue(null); + }; + + const handleCloseQueueModal = () => { + setQueueModalOpen(false); + setSelectedQueue(null); + }; + + const handleEditQueue = queue => { + setSelectedQueue(queue); + setQueueModalOpen(true); + }; + + const handleCloseConfirmationModal = () => { + setConfirmModalOpen(false); + setSelectedQueue(null); + }; + + const handleDeleteQueue = async queueId => { + try { + await api.delete(`/queue/${queueId}`); + toast.success(i18n.t("Queue deleted successfully!")); + } catch (err) { + toastError(err); + } + setSelectedQueue(null); + }; + + return ( + + handleDeleteQueue(selectedQueue.id)} + > + {i18n.t("queues.confirmationModal.deleteMessage")} + + + + {i18n.t("queues.title")} + + + + + + + + + + {i18n.t("queues.table.name")} + + + {i18n.t("queues.table.color")} + + + {i18n.t("queues.table.greeting")} + + + {i18n.t("queues.table.actions")} + + + + + <> + {queues.map(queue => ( + + {queue.name} + +
+ +
+
+ +
+ + {queue.greetingMessage} + +
+
+ + handleEditQueue(queue)} + > + + + + { + setSelectedQueue(queue); + setConfirmModalOpen(true); + }} + > + + + +
+ ))} + {loading && } + +
+
+
+
+ ); }; export default Queues; diff --git a/frontend/src/pages/Users/index.js b/frontend/src/pages/Users/index.js index d8817a3..a457b5d 100644 --- a/frontend/src/pages/Users/index.js +++ b/frontend/src/pages/Users/index.js @@ -193,7 +193,7 @@ const Users = () => { }?` } open={confirmModalOpen} - setOpen={setConfirmModalOpen} + onClose={setConfirmModalOpen} onConfirm={() => handleDeleteUser(deletingUser.id)} > {i18n.t("users.confirmationModal.deleteMessage")} diff --git a/frontend/src/translate/languages/en.js b/frontend/src/translate/languages/en.js index 0ab8b46..fe73f7d 100644 --- a/frontend/src/translate/languages/en.js +++ b/frontend/src/translate/languages/en.js @@ -153,6 +153,22 @@ const messages = { }, success: "Contact saved successfully.", }, + queueModal: { + title: { + add: "Add queue", + edit: "Edit queue", + }, + form: { + name: "Name", + color: "Color", + greetingMessage: "Greeting Message", + }, + buttons: { + okAdd: "Add", + okEdit: "Save", + cancel: "Cancel", + }, + }, userModal: { title: { add: "Add user", @@ -226,6 +242,7 @@ const messages = { connections: "Connections", tickets: "Tickets", contacts: "Contacts", + queues: "Queues", administration: "Administration", users: "Users", settings: "Settings", @@ -240,6 +257,23 @@ const messages = { notifications: { noTickets: "No notifications.", }, + queues: { + title: "Queues", + table: { + name: "Name", + color: "Color", + greeting: "Greeting message", + actions: "Actions", + }, + buttons: { + add: "Add queue", + }, + confirmationModal: { + deleteTitle: "Delete", + deleteMessage: + "Are you sure? It cannot be reverted! Tickets in this queue will still exist, but will not have any queues assigned.", + }, + }, users: { title: "Users", table: { diff --git a/frontend/src/translate/languages/es.js b/frontend/src/translate/languages/es.js index dfdd70a..48a6519 100644 --- a/frontend/src/translate/languages/es.js +++ b/frontend/src/translate/languages/es.js @@ -156,6 +156,22 @@ const messages = { }, success: "Contacto guardado satisfactoriamente.", }, + queueModal: { + title: { + add: "Agregar cola", + edit: "Editar cola", + }, + form: { + name: "Nombre", + color: "Color", + greetingMessage: "Mensaje de saludo", + }, + buttons: { + okAdd: "Añadir", + okEdit: "Ahorrar", + cancel: "Cancelar", + }, + }, userModal: { title: { add: "Agregar usuario", @@ -230,6 +246,7 @@ const messages = { connections: "Conexiones", tickets: "Tickets", contacts: "Contactos", + queues: "Linhas", administration: "Administración", users: "Usuarios", settings: "Configuración", @@ -244,6 +261,23 @@ const messages = { notifications: { noTickets: "Sin notificaciones.", }, + queues: { + title: "Linhas", + table: { + name: "Nombre", + color: "Color", + greeting: "Mensaje de saludo", + actions: "Comportamiento", + }, + buttons: { + add: "Agregar cola", + }, + confirmationModal: { + deleteTitle: "Eliminar", + deleteMessage: + "¿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.", + }, + }, users: { title: "Usuarios", table: { diff --git a/frontend/src/translate/languages/pt.js b/frontend/src/translate/languages/pt.js index f98eddc..4b04b8c 100644 --- a/frontend/src/translate/languages/pt.js +++ b/frontend/src/translate/languages/pt.js @@ -154,6 +154,22 @@ const messages = { }, success: "Contato salvo com sucesso.", }, + queueModal: { + title: { + add: "Adicionar fila", + edit: "Editar fila", + }, + form: { + name: "Nome", + color: "Cor", + greetingMessage: "Mensagem de saudação", + }, + buttons: { + okAdd: "Adicionar", + okEdit: "Salvar", + cancel: "Cancelar", + }, + }, userModal: { title: { add: "Adicionar usuário", @@ -228,6 +244,7 @@ const messages = { connections: "Conexões", tickets: "Tickets", contacts: "Contatos", + queues: "Filas", administration: "Administração", users: "Usuários", settings: "Configurações", @@ -242,6 +259,23 @@ const messages = { notifications: { noTickets: "Nenhuma notificação.", }, + queues: { + title: "Filas", + table: { + name: "Nome", + color: "Cor", + greeting: "Mensagem de saudação", + actions: "Ações", + }, + buttons: { + add: "Adicionar fila", + }, + confirmationModal: { + deleteTitle: "Excluir", + deleteMessage: + "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.", + }, + }, users: { title: "Usuários", table: { From a75eb49b31c83c63db3d69fb46006f0e103562b8 Mon Sep 17 00:00:00 2001 From: canove Date: Sun, 10 Jan 2021 20:01:05 -0300 Subject: [PATCH 08/29] feat: started whatsapp modal queue selection --- frontend/src/components/QueueModal/index.js | 12 +- .../src/components/QueueSelector/index.js | 91 ++++++++ frontend/src/components/UserModal/index.js | 75 ++++--- .../src/components/WhatsAppModal/index.js | 203 +++++++++++------- 4 files changed, 266 insertions(+), 115 deletions(-) create mode 100644 frontend/src/components/QueueSelector/index.js diff --git a/frontend/src/components/QueueModal/index.js b/frontend/src/components/QueueModal/index.js index 9261cb3..b083993 100644 --- a/frontend/src/components/QueueModal/index.js +++ b/frontend/src/components/QueueModal/index.js @@ -13,10 +13,6 @@ 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 Select from "@material-ui/core/Select"; -import InputLabel from "@material-ui/core/InputLabel"; -import MenuItem from "@material-ui/core/MenuItem"; -import FormControl from "@material-ui/core/FormControl"; import { i18n } from "../../translate/i18n"; @@ -120,7 +116,13 @@ const QueueModal = ({ open, onClose, queueId }) => { return (
- + {queueId ? `${i18n.t("queueModal.title.edit")}` diff --git a/frontend/src/components/QueueSelector/index.js b/frontend/src/components/QueueSelector/index.js new file mode 100644 index 0000000..545131c --- /dev/null +++ b/frontend/src/components/QueueSelector/index.js @@ -0,0 +1,91 @@ +import React, { useEffect, useState } from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import FormControl from "@material-ui/core/FormControl"; +import Select from "@material-ui/core/Select"; +import Chip from "@material-ui/core/Chip"; +import { OutlinedInput } from "@material-ui/core"; +import toastError from "../../errors/toastError"; +import api from "../../services/api"; + +const useStyles = makeStyles(theme => ({ + chips: { + display: "flex", + flexWrap: "wrap", + }, + chip: { + margin: 2, + }, +})); + +const QueueSelector = ({ selectedQueueIds, onChange }) => { + const classes = useStyles(); + // const [selectedQueues, setSelectedQueues] = useState([]); + const [queues, setQueues] = useState([]); + + useEffect(() => { + (async () => { + try { + const { data } = await api.get("/queue"); + setQueues(data); + } catch (err) { + toastError(err); + } + })(); + }, []); + + const handleChange = event => { + onChange(event.target.value); + }; + + return ( +
+ + Filas + + +
+ ); +}; + +export default QueueSelector; diff --git a/frontend/src/components/UserModal/index.js b/frontend/src/components/UserModal/index.js index dffdd6e..a01012c 100644 --- a/frontend/src/components/UserModal/index.js +++ b/frontend/src/components/UserModal/index.js @@ -22,15 +22,18 @@ import { i18n } from "../../translate/i18n"; import api from "../../services/api"; import toastError from "../../errors/toastError"; +import QueueSelector from "../QueueSelector"; const useStyles = makeStyles(theme => ({ root: { display: "flex", flexWrap: "wrap", }, - textField: { - marginRight: theme.spacing(1), - flex: 1, + multFieldLine: { + display: "flex", + "& > *:not(:last-child)": { + marginRight: theme.spacing(1), + }, }, btnWrapper: { @@ -71,6 +74,7 @@ const UserModal = ({ open, onClose, userId }) => { }; const [user, setUser] = useState(initialState); + const [selectedQueueIds, setSelectedQueueIds] = useState([]); useEffect(() => { const fetchUser = async () => { @@ -80,6 +84,8 @@ const UserModal = ({ open, onClose, userId }) => { setUser(prevState => { return { ...prevState, ...data }; }); + const userQueueIds = data.queues?.map(queue => queue.id); + setSelectedQueueIds(userQueueIds); } catch (err) { toastError(err); } @@ -94,11 +100,12 @@ const UserModal = ({ open, onClose, userId }) => { }; const handleSaveUser = async values => { + const userData = { ...values, queueIds: selectedQueueIds }; try { if (userId) { - await api.put(`/users/${userId}`, values); + await api.put(`/users/${userId}`, userData); } else { - await api.post("/users", values); + await api.post("/users", userData); } toast.success(i18n.t("userModal.success")); } catch (err) { @@ -109,7 +116,13 @@ const UserModal = ({ open, onClose, userId }) => { return (
- + {userId ? `${i18n.t("userModal.title.edit")}` @@ -129,27 +142,18 @@ const UserModal = ({ open, onClose, userId }) => { {({ touched, errors, isSubmitting }) => (
- - -
+
+ { helperText={touched.password && errors.password} variant="outlined" margin="dense" + fullWidth + /> +
+
+ {
+ setSelectedQueueIds(values)} + /> - - - - )} - -
+
+
+ +
+ setSelectedQueueIds(values)} + /> + + + + + + + )} + +
+
); }; From be320fa34bd0f731c57dd0d973a69dbe66ad3cd2 Mon Sep 17 00:00:00 2001 From: canove Date: Mon, 11 Jan 2021 06:38:45 -0300 Subject: [PATCH 09/29] feat: removed optionNumber from queues assocs --- backend/src/controllers/WhatsAppController.ts | 10 +++----- ...20210108174594-associate-whatsapp-queue.ts | 3 --- backend/src/models/WhatsappQueue.ts | 3 --- .../QueueService/AssociateWhatsappQueue.ts | 24 ++----------------- .../QueueService/ListQueuesService.ts | 2 +- .../WbotServices/wbotMessageListener.ts | 20 +++++++--------- .../WhatsappService/CreateWhatsAppService.ts | 16 ++++++------- .../WhatsappService/ListWhatsAppsService.ts | 17 ++++--------- .../WhatsappService/ShowWhatsAppService.ts | 16 ++++--------- .../WhatsappService/UpdateWhatsAppService.ts | 18 +++++++------- .../src/components/WhatsAppModal/index.js | 4 ++-- 11 files changed, 39 insertions(+), 94 deletions(-) diff --git a/backend/src/controllers/WhatsAppController.ts b/backend/src/controllers/WhatsAppController.ts index 5759109..13097d3 100644 --- a/backend/src/controllers/WhatsAppController.ts +++ b/backend/src/controllers/WhatsAppController.ts @@ -9,13 +9,9 @@ import ListWhatsAppsService from "../services/WhatsappService/ListWhatsAppsServi import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService"; import UpdateWhatsAppService from "../services/WhatsappService/UpdateWhatsAppService"; -interface QueueData { - id: number; - optionNumber: number; -} interface WhatsappData { name: string; - queuesData: QueueData[]; + queueIds: number[]; greetingMessage?: string; status?: string; isDefault?: boolean; @@ -33,7 +29,7 @@ export const store = async (req: Request, res: Response): Promise => { status, isDefault, greetingMessage, - queuesData + queueIds }: WhatsappData = req.body; const { whatsapp, oldDefaultWhatsapp } = await CreateWhatsAppService({ @@ -41,7 +37,7 @@ export const store = async (req: Request, res: Response): Promise => { status, isDefault, greetingMessage, - queuesData + queueIds }); // StartWhatsAppSession(whatsapp); diff --git a/backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts b/backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts index 0ea50f0..0e08f71 100644 --- a/backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts +++ b/backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts @@ -3,9 +3,6 @@ import { QueryInterface, DataTypes } from "sequelize"; module.exports = { up: (queryInterface: QueryInterface) => { return queryInterface.createTable("WhatsappQueues", { - optionNumber: { - type: DataTypes.INTEGER - }, whatsappId: { type: DataTypes.INTEGER, primaryKey: true diff --git a/backend/src/models/WhatsappQueue.ts b/backend/src/models/WhatsappQueue.ts index 7886618..b68aaa0 100644 --- a/backend/src/models/WhatsappQueue.ts +++ b/backend/src/models/WhatsappQueue.ts @@ -12,9 +12,6 @@ import Whatsapp from "./Whatsapp"; @Table class WhatsappQueue extends Model { - @Column - optionNumber: number; - @ForeignKey(() => Whatsapp) @Column whatsappId: number; diff --git a/backend/src/services/QueueService/AssociateWhatsappQueue.ts b/backend/src/services/QueueService/AssociateWhatsappQueue.ts index 45a5a84..5f840f7 100644 --- a/backend/src/services/QueueService/AssociateWhatsappQueue.ts +++ b/backend/src/services/QueueService/AssociateWhatsappQueue.ts @@ -1,32 +1,12 @@ import Whatsapp from "../../models/Whatsapp"; -import WhatsappQueue from "../../models/WhatsappQueue"; - -interface QueueData { - id: number; - optionNumber: number; -} const AssociateWhatsappQueue = async ( whatsapp: Whatsapp, - queuesData: QueueData[] + queueIds: number[] ): Promise => { - const queueIds = queuesData.map(({ id }) => id); - await whatsapp.$set("queues", queueIds); - /* eslint-disable no-restricted-syntax */ - /* eslint-disable no-await-in-loop */ - for (const queueData of queuesData) { - await WhatsappQueue.update( - { optionNumber: queueData.optionNumber }, - { - where: { - whatsappId: whatsapp.id, - queueId: queueData.id - } - } - ); - } + await whatsapp.reload(); }; export default AssociateWhatsappQueue; diff --git a/backend/src/services/QueueService/ListQueuesService.ts b/backend/src/services/QueueService/ListQueuesService.ts index 9a9a3a1..204d9a1 100644 --- a/backend/src/services/QueueService/ListQueuesService.ts +++ b/backend/src/services/QueueService/ListQueuesService.ts @@ -1,7 +1,7 @@ import Queue from "../../models/Queue"; const ListQueuesService = async (): Promise => { - const queues = await Queue.findAll(); + const queues = await Queue.findAll({ order: [["name", "ASC"]] }); return queues; }; diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 76f8904..3f97218 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -133,12 +133,10 @@ const verifyQueue = async ( ticket: Ticket, contact: Contact ) => { - const { whatsappQueues, greetingMessage } = await ShowWhatsAppService( - wbot.id! - ); + const { queues, greetingMessage } = await ShowWhatsAppService(wbot.id!); - if (whatsappQueues.length === 1) { - await ticket.$set("queue", whatsappQueues[0].queue); + if (queues.length === 1) { + await ticket.$set("queue", queues[0]); // TODO sendTicketQueueUpdate to frontend return; @@ -146,14 +144,12 @@ const verifyQueue = async ( const selectedOption = msg.body[0]; - const validOption = whatsappQueues.find( - q => q.optionNumber === +selectedOption - ); + const validOption = queues[+selectedOption - 1]; if (validOption) { - await ticket.$set("queue", validOption.queue); + await ticket.$set("queue", validOption); - const body = `\u200e ${validOption.queue.greetingMessage}`; + const body = `\u200e ${validOption.greetingMessage}`; const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body); @@ -163,8 +159,8 @@ const verifyQueue = async ( } else { let options = ""; - whatsappQueues.forEach(whatsQueue => { - options += `*${whatsQueue.optionNumber}* - ${whatsQueue.queue.name}\n`; + queues.forEach((queue, index) => { + options += `*${index + 1}* - ${queue.name}\n`; }); const body = `\u200e ${greetingMessage}\n\n${options}`; diff --git a/backend/src/services/WhatsappService/CreateWhatsAppService.ts b/backend/src/services/WhatsappService/CreateWhatsAppService.ts index f1375e5..b2ced1e 100644 --- a/backend/src/services/WhatsappService/CreateWhatsAppService.ts +++ b/backend/src/services/WhatsappService/CreateWhatsAppService.ts @@ -4,13 +4,9 @@ import AppError from "../../errors/AppError"; import Whatsapp from "../../models/Whatsapp"; import AssociateWhatsappQueue from "../QueueService/AssociateWhatsappQueue"; -interface QueueData { - id: number; - optionNumber: number; -} interface Request { name: string; - queuesData: QueueData[]; + queueIds?: number[]; greetingMessage?: string; status?: string; isDefault?: boolean; @@ -24,7 +20,7 @@ interface Response { const CreateWhatsAppService = async ({ name, status = "OPENING", - queuesData = [], + queueIds = [], greetingMessage, isDefault = false }: Request): Promise => { @@ -71,6 +67,10 @@ const CreateWhatsAppService = async ({ } } + if (queueIds.length > 1 && !greetingMessage) { + throw new AppError("ERR_WAPP_GREETING_REQUIRED"); + } + const whatsapp = await Whatsapp.create( { name, @@ -81,9 +81,7 @@ const CreateWhatsAppService = async ({ { include: ["queues"] } ); - await AssociateWhatsappQueue(whatsapp, queuesData); - - await whatsapp.reload(); + await AssociateWhatsappQueue(whatsapp, queueIds); return { whatsapp, oldDefaultWhatsapp }; }; diff --git a/backend/src/services/WhatsappService/ListWhatsAppsService.ts b/backend/src/services/WhatsappService/ListWhatsAppsService.ts index 0ac127c..3d29c2c 100644 --- a/backend/src/services/WhatsappService/ListWhatsAppsService.ts +++ b/backend/src/services/WhatsappService/ListWhatsAppsService.ts @@ -1,24 +1,15 @@ import Queue from "../../models/Queue"; import Whatsapp from "../../models/Whatsapp"; -import WhatsappQueue from "../../models/WhatsappQueue"; const ListWhatsAppsService = async (): Promise => { const whatsapps = await Whatsapp.findAll({ include: [ { - model: WhatsappQueue, - as: "whatsappQueues", - attributes: ["optionNumber"], - include: [ - { - model: Queue, - as: "queue", - attributes: ["id", "name", "color", "greetingMessage"] - } - ] + model: Queue, + as: "queues", + attributes: ["id", "name", "color", "greetingMessage"] } - ], - order: [["whatsappQueues", "optionNumber", "ASC"]] + ] }); return whatsapps; diff --git a/backend/src/services/WhatsappService/ShowWhatsAppService.ts b/backend/src/services/WhatsappService/ShowWhatsAppService.ts index 5643032..235ef17 100644 --- a/backend/src/services/WhatsappService/ShowWhatsAppService.ts +++ b/backend/src/services/WhatsappService/ShowWhatsAppService.ts @@ -1,25 +1,17 @@ import Whatsapp from "../../models/Whatsapp"; import AppError from "../../errors/AppError"; import Queue from "../../models/Queue"; -import WhatsappQueue from "../../models/WhatsappQueue"; const ShowWhatsAppService = async (id: string | number): Promise => { const whatsapp = await Whatsapp.findByPk(id, { include: [ { - model: WhatsappQueue, - as: "whatsappQueues", - attributes: ["optionNumber"], - include: [ - { - model: Queue, - as: "queue", - attributes: ["id", "name", "color", "greetingMessage"] - } - ] + model: Queue, + as: "queues", + attributes: ["id", "name", "color", "greetingMessage"] } ], - order: [["whatsappQueues", "optionNumber", "ASC"]] + order: [["queues", "name", "ASC"]] }); if (!whatsapp) { diff --git a/backend/src/services/WhatsappService/UpdateWhatsAppService.ts b/backend/src/services/WhatsappService/UpdateWhatsAppService.ts index 417d8b2..d7a155c 100644 --- a/backend/src/services/WhatsappService/UpdateWhatsAppService.ts +++ b/backend/src/services/WhatsappService/UpdateWhatsAppService.ts @@ -6,17 +6,13 @@ import Whatsapp from "../../models/Whatsapp"; import ShowWhatsAppService from "./ShowWhatsAppService"; import AssociateWhatsappQueue from "../QueueService/AssociateWhatsappQueue"; -interface QueueData { - id: number; - optionNumber: number; -} interface WhatsappData { name?: string; status?: string; session?: string; isDefault?: boolean; greetingMessage?: string; - queuesData?: QueueData[]; + queueIds?: number[]; } interface Request { @@ -34,7 +30,7 @@ const UpdateWhatsAppService = async ({ whatsappId }: Request): Promise => { const schema = Yup.object().shape({ - name: Yup.string().min(2), + name: Yup.string().min(2).required(), isDefault: Yup.boolean() }); @@ -44,7 +40,7 @@ const UpdateWhatsAppService = async ({ isDefault, session, greetingMessage, - queuesData = [] + queueIds = [] } = whatsappData; try { @@ -53,6 +49,10 @@ const UpdateWhatsAppService = async ({ throw new AppError(err.message); } + if (queueIds.length > 1 && !greetingMessage) { + throw new AppError("ERR_WAPP_GREETING_REQUIRED"); + } + let oldDefaultWhatsapp: Whatsapp | null = null; if (isDefault) { @@ -74,9 +74,7 @@ const UpdateWhatsAppService = async ({ isDefault }); - await AssociateWhatsappQueue(whatsapp, queuesData); - - await whatsapp.reload(); + await AssociateWhatsappQueue(whatsapp, queueIds); return { whatsapp, oldDefaultWhatsapp }; }; diff --git a/frontend/src/components/WhatsAppModal/index.js b/frontend/src/components/WhatsAppModal/index.js index edead91..c525ab2 100644 --- a/frontend/src/components/WhatsAppModal/index.js +++ b/frontend/src/components/WhatsAppModal/index.js @@ -75,7 +75,7 @@ const WhatsAppModal = ({ open, onClose, whatsAppId }) => { const { data } = await api.get(`whatsapp/${whatsAppId}`); setWhatsApp(data); - const whatsQueueIds = data.whatsappQueues?.map(q => q.queue.id); + const whatsQueueIds = data.queues?.map(queue => queue.id); setSelectedQueueIds(whatsQueueIds); } catch (err) { toastError(err); @@ -94,10 +94,10 @@ const WhatsAppModal = ({ open, onClose, whatsAppId }) => { await api.post("/whatsapp", whatsappData); } toast.success(i18n.t("whatsappModal.success")); + handleClose(); } catch (err) { toastError(err); } - handleClose(); }; const handleClose = () => { From f642f2c7886778492bedba9001bbafe1f2dc1ff3 Mon Sep 17 00:00:00 2001 From: canove Date: Mon, 11 Jan 2021 06:53:59 -0300 Subject: [PATCH 10/29] feat: added socket io to queues page --- backend/src/controllers/QueueController.ts | 19 ++++++ frontend/src/pages/Queues/index.js | 70 +++++++++++++++++++++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/backend/src/controllers/QueueController.ts b/backend/src/controllers/QueueController.ts index 9dbad1b..0ffa66c 100644 --- a/backend/src/controllers/QueueController.ts +++ b/backend/src/controllers/QueueController.ts @@ -1,4 +1,5 @@ import { Request, Response } from "express"; +import { getIO } from "../libs/socket"; import CreateQueueService from "../services/QueueService/CreateQueueService"; import DeleteQueueService from "../services/QueueService/DeleteQueueService"; import ListQueuesService from "../services/QueueService/ListQueuesService"; @@ -16,6 +17,12 @@ export const store = async (req: Request, res: Response): Promise => { const queue = await CreateQueueService({ name, color, greetingMessage }); + const io = getIO(); + io.emit("queue", { + action: "update", + queue + }); + return res.status(200).json(queue); }; @@ -35,6 +42,12 @@ export const update = async ( const queue = await UpdateQueueService(queueId, req.body); + const io = getIO(); + io.emit("queue", { + action: "update", + queue + }); + return res.status(201).json(queue); }; @@ -46,5 +59,11 @@ export const remove = async ( await DeleteQueueService(queueId); + const io = getIO(); + io.emit("queue", { + action: "delete", + queueId: +queueId + }); + return res.status(200).send(); }; diff --git a/frontend/src/pages/Queues/index.js b/frontend/src/pages/Queues/index.js index b9d87ba..0734336 100644 --- a/frontend/src/pages/Queues/index.js +++ b/frontend/src/pages/Queues/index.js @@ -1,4 +1,6 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useReducer, useState } from "react"; + +import openSocket from "socket.io-client"; import { Button, @@ -41,10 +43,54 @@ const useStyles = makeStyles(theme => ({ }, })); +const reducer = (state, action) => { + if (action.type === "LOAD_QUEUES") { + const queues = action.payload; + const newQueues = []; + + queues.forEach(queue => { + const queueIndex = state.findIndex(q => q.id === queue.id); + if (queueIndex !== -1) { + state[queueIndex] = queue; + } else { + newQueues.push(queue); + } + }); + + return [...state, ...newQueues]; + } + + if (action.type === "UPDATE_QUEUES") { + const queue = action.payload; + const queueIndex = state.findIndex(u => u.id === queue.id); + + if (queueIndex !== -1) { + state[queueIndex] = queue; + return [...state]; + } else { + return [queue, ...state]; + } + } + + if (action.type === "DELETE_QUEUE") { + const queueId = action.payload; + console.log("QUEUEID", queueId); + const queueIndex = state.findIndex(q => q.id === queueId); + if (queueIndex !== -1) { + state.splice(queueIndex, 1); + } + return [...state]; + } + + if (action.type === "RESET") { + return []; + } +}; + const Queues = () => { const classes = useStyles(); - const [queues, setQueue] = useState([]); + const [queues, dispatch] = useReducer(reducer, []); const [loading, setLoading] = useState(false); const [queueModalOpen, setQueueModalOpen] = useState(false); @@ -56,8 +102,8 @@ const Queues = () => { setLoading(true); try { const { data } = await api.get("/queue"); + dispatch({ type: "LOAD_QUEUES", payload: data }); - setQueue(data); setLoading(false); } catch (err) { toastError(err); @@ -66,6 +112,24 @@ const Queues = () => { })(); }, []); + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + + socket.on("queue", data => { + if (data.action === "update" || data.action === "create") { + dispatch({ type: "UPDATE_QUEUES", payload: data.queue }); + } + + if (data.action === "delete") { + dispatch({ type: "DELETE_QUEUE", payload: data.queueId }); + } + }); + + return () => { + socket.disconnect(); + }; + }, []); + const handleOpenQueueModal = () => { setQueueModalOpen(true); setSelectedQueue(null); From 90b438025bf750a282b6778b6da57d0b1f482e97 Mon Sep 17 00:00:00 2001 From: canove Date: Mon, 11 Jan 2021 07:22:56 -0300 Subject: [PATCH 11/29] feat: add queue color to tickets list item --- .../TicketServices/ListTicketsService.ts | 6 +++++ .../TicketServices/UpdateTicketService.ts | 24 ++----------------- .../WbotServices/wbotMessageListener.ts | 2 +- .../src/components/TicketListItem/index.js | 13 ++++++++++ frontend/src/components/TicketsList/index.js | 2 ++ .../src/components/WhatsAppModal/index.js | 2 +- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/backend/src/services/TicketServices/ListTicketsService.ts b/backend/src/services/TicketServices/ListTicketsService.ts index 7351102..a62e43a 100644 --- a/backend/src/services/TicketServices/ListTicketsService.ts +++ b/backend/src/services/TicketServices/ListTicketsService.ts @@ -4,6 +4,7 @@ import { startOfDay, endOfDay, parseISO } from "date-fns"; import Ticket from "../../models/Ticket"; import Contact from "../../models/Contact"; import Message from "../../models/Message"; +import Queue from "../../models/Queue"; interface Request { searchParam?: string; @@ -40,6 +41,11 @@ const ListTicketsService = async ({ model: Contact, as: "contact", attributes: ["id", "name", "number", "profilePicUrl"] + }, + { + model: Queue, + as: "queue", + attributes: ["id", "name", "color"] } ]; diff --git a/backend/src/services/TicketServices/UpdateTicketService.ts b/backend/src/services/TicketServices/UpdateTicketService.ts index ed61b64..d7f7bfa 100644 --- a/backend/src/services/TicketServices/UpdateTicketService.ts +++ b/backend/src/services/TicketServices/UpdateTicketService.ts @@ -1,9 +1,7 @@ -import AppError from "../../errors/AppError"; import CheckContactOpenTickets from "../../helpers/CheckContactOpenTickets"; import SetTicketMessagesAsRead from "../../helpers/SetTicketMessagesAsRead"; -import Contact from "../../models/Contact"; import Ticket from "../../models/Ticket"; -import User from "../../models/User"; +import ShowTicketService from "./ShowTicketService"; interface TicketData { status?: string; @@ -27,25 +25,7 @@ const UpdateTicketService = async ({ }: Request): Promise => { const { status, userId } = ticketData; - const ticket = await Ticket.findOne({ - where: { id: ticketId }, - include: [ - { - model: Contact, - as: "contact", - attributes: ["id", "name", "number", "profilePicUrl"] - }, - { - model: User, - as: "user", - attributes: ["id", "name"] - } - ] - }); - - if (!ticket) { - throw new AppError("ERR_NO_TICKET_FOUND", 404); - } + const ticket = await ShowTicketService(ticketId); await SetTicketMessagesAsRead(ticket); diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 3f97218..649fb8a 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -163,7 +163,7 @@ const verifyQueue = async ( options += `*${index + 1}* - ${queue.name}\n`; }); - const body = `\u200e ${greetingMessage}\n\n${options}`; + const body = `\u200e ${greetingMessage}\n${options}`; const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body); diff --git a/frontend/src/components/TicketListItem/index.js b/frontend/src/components/TicketListItem/index.js index 01c551e..62f2944 100644 --- a/frontend/src/components/TicketListItem/index.js +++ b/frontend/src/components/TicketListItem/index.js @@ -87,6 +87,15 @@ const useStyles = makeStyles(theme => ({ position: "absolute", left: "50%", }, + + ticketQueueColor: { + flex: "none", + width: "8px", + height: "100%", + position: "absolute", + top: "0%", + left: "0%", + }, })); const TicketListItem = ({ ticket }) => { @@ -138,6 +147,10 @@ const TicketListItem = ({ ticket }) => { [classes.pendingTicket]: ticket.status === "pending", })} > + { if (action.type === "UPDATE_TICKET") { const ticket = action.payload; + console.log("TICKET", ticket); + const ticketIndex = state.findIndex(t => t.id === ticket.id); if (ticketIndex !== -1) { state[ticketIndex] = ticket; diff --git a/frontend/src/components/WhatsAppModal/index.js b/frontend/src/components/WhatsAppModal/index.js index c525ab2..73bdf02 100644 --- a/frontend/src/components/WhatsAppModal/index.js +++ b/frontend/src/components/WhatsAppModal/index.js @@ -86,7 +86,7 @@ const WhatsAppModal = ({ open, onClose, whatsAppId }) => { const handleSaveWhatsApp = async values => { const whatsappData = { ...values, queueIds: selectedQueueIds }; - console.log("SELECTED", whatsappData); + try { if (whatsAppId) { await api.put(`/whatsapp/${whatsAppId}`, whatsappData); From 0a4d5a081eb2719b43d9f6cb2939a91b9b47208c Mon Sep 17 00:00:00 2001 From: canove Date: Mon, 11 Jan 2021 08:17:49 -0300 Subject: [PATCH 12/29] chore: code cleanup --- frontend/src/components/TicketsList/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/components/TicketsList/index.js b/frontend/src/components/TicketsList/index.js index f26e184..addd137 100644 --- a/frontend/src/components/TicketsList/index.js +++ b/frontend/src/components/TicketsList/index.js @@ -100,8 +100,6 @@ const reducer = (state, action) => { if (action.type === "UPDATE_TICKET") { const ticket = action.payload; - console.log("TICKET", ticket); - const ticketIndex = state.findIndex(t => t.id === ticket.id); if (ticketIndex !== -1) { state[ticketIndex] = ticket; From 14c3ebe27e10efcb7ecd31afd9bef32f37170a91 Mon Sep 17 00:00:00 2001 From: canove Date: Mon, 11 Jan 2021 18:10:25 -0300 Subject: [PATCH 13/29] feat: added debounce delay to auto reply reply only last message received within 3 seconds --- backend/src/helpers/Debounce.ts | 41 +++++++++++++++++++ .../WbotServices/wbotMessageListener.ts | 15 ++++++- 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 backend/src/helpers/Debounce.ts diff --git a/backend/src/helpers/Debounce.ts b/backend/src/helpers/Debounce.ts new file mode 100644 index 0000000..80665d9 --- /dev/null +++ b/backend/src/helpers/Debounce.ts @@ -0,0 +1,41 @@ +interface Timeout { + id: number; + timeout: NodeJS.Timeout; +} + +const timeouts: Timeout[] = []; + +const findAndClearTimeout = (ticketId: number) => { + if (timeouts.length > 0) { + const timeoutIndex = timeouts.findIndex(timeout => timeout.id === ticketId); + + if (timeoutIndex !== -1) { + clearTimeout(timeouts[timeoutIndex].timeout); + timeouts.splice(timeoutIndex, 1); + } + } +}; + +const debounce = ( + func: { (): Promise; (...args: never[]): void }, + wait: number, + ticketId: number +) => { + return function executedFunction(...args: never[]): void { + const later = () => { + findAndClearTimeout(ticketId); + func(...args); + }; + + findAndClearTimeout(ticketId); + + const newTimeout = { + id: ticketId, + timeout: setTimeout(later, wait) + }; + + timeouts.push(newTimeout); + }; +}; + +export { debounce }; diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 649fb8a..5a6a968 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -20,6 +20,7 @@ import { logger } from "../../utils/logger"; import CreateOrUpdateContactService from "../ContactServices/CreateOrUpdateContactService"; import FindOrCreateTicketService from "../TicketServices/FindOrCreateTicketService"; import ShowWhatsAppService from "../WhatsappService/ShowWhatsAppService"; +import { debounce } from "../../helpers/Debounce"; interface Session extends Client { id?: number; @@ -165,9 +166,19 @@ const verifyQueue = async ( const body = `\u200e ${greetingMessage}\n${options}`; - const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body); + const debouncedSentMessage = debounce( + async () => { + const sentMessage = await wbot.sendMessage( + `${contact.number}@c.us`, + body + ); + verifyMessage(sentMessage, ticket, contact); + }, + 3000, + ticket.id + ); - await verifyMessage(sentMessage, ticket, contact); + debouncedSentMessage(); } }; From 5c5f58e4feb004dc6e17c50be205d39876b04373 Mon Sep 17 00:00:00 2001 From: canove Date: Mon, 11 Jan 2021 18:23:28 -0300 Subject: [PATCH 14/29] feat: update ticket color on frontend after assign --- .../src/services/MessageServices/CreateMessageService.ts | 2 +- backend/src/services/WbotServices/wbotMessageListener.ts | 8 ++++---- .../AssociateWhatsappQueue.ts | 0 .../src/services/WhatsappService/CreateWhatsAppService.ts | 2 +- .../src/services/WhatsappService/UpdateWhatsAppService.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) rename backend/src/services/{QueueService => WhatsappService}/AssociateWhatsappQueue.ts (100%) diff --git a/backend/src/services/MessageServices/CreateMessageService.ts b/backend/src/services/MessageServices/CreateMessageService.ts index 857ec5c..67fd33b 100644 --- a/backend/src/services/MessageServices/CreateMessageService.ts +++ b/backend/src/services/MessageServices/CreateMessageService.ts @@ -27,7 +27,7 @@ const CreateMessageService = async ({ { model: Ticket, as: "ticket", - include: ["contact"] + include: ["contact", "queue"] }, { model: Message, diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 5a6a968..e5303dc 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -145,12 +145,12 @@ const verifyQueue = async ( const selectedOption = msg.body[0]; - const validOption = queues[+selectedOption - 1]; + const choosenQueue = queues[+selectedOption - 1]; - if (validOption) { - await ticket.$set("queue", validOption); + if (choosenQueue) { + await ticket.$set("queue", choosenQueue); - const body = `\u200e ${validOption.greetingMessage}`; + const body = `\u200e ${choosenQueue.greetingMessage}`; const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body); diff --git a/backend/src/services/QueueService/AssociateWhatsappQueue.ts b/backend/src/services/WhatsappService/AssociateWhatsappQueue.ts similarity index 100% rename from backend/src/services/QueueService/AssociateWhatsappQueue.ts rename to backend/src/services/WhatsappService/AssociateWhatsappQueue.ts diff --git a/backend/src/services/WhatsappService/CreateWhatsAppService.ts b/backend/src/services/WhatsappService/CreateWhatsAppService.ts index b2ced1e..f653d49 100644 --- a/backend/src/services/WhatsappService/CreateWhatsAppService.ts +++ b/backend/src/services/WhatsappService/CreateWhatsAppService.ts @@ -2,7 +2,7 @@ import * as Yup from "yup"; import AppError from "../../errors/AppError"; import Whatsapp from "../../models/Whatsapp"; -import AssociateWhatsappQueue from "../QueueService/AssociateWhatsappQueue"; +import AssociateWhatsappQueue from "./AssociateWhatsappQueue"; interface Request { name: string; diff --git a/backend/src/services/WhatsappService/UpdateWhatsAppService.ts b/backend/src/services/WhatsappService/UpdateWhatsAppService.ts index d7a155c..4d96889 100644 --- a/backend/src/services/WhatsappService/UpdateWhatsAppService.ts +++ b/backend/src/services/WhatsappService/UpdateWhatsAppService.ts @@ -4,7 +4,7 @@ import { Op } from "sequelize"; import AppError from "../../errors/AppError"; import Whatsapp from "../../models/Whatsapp"; import ShowWhatsAppService from "./ShowWhatsAppService"; -import AssociateWhatsappQueue from "../QueueService/AssociateWhatsappQueue"; +import AssociateWhatsappQueue from "./AssociateWhatsappQueue"; interface WhatsappData { name?: string; From 1f76b4f221656bd655badcaa51cba23acab454b3 Mon Sep 17 00:00:00 2001 From: canove Date: Mon, 11 Jan 2021 19:22:46 -0300 Subject: [PATCH 15/29] feat: add tooltip to queue badge --- frontend/public/index.html | 4 ++++ frontend/src/components/TicketListItem/index.js | 15 +++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/frontend/public/index.html b/frontend/public/index.html index dc5f765..f775733 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -11,6 +11,10 @@ + diff --git a/frontend/src/components/TicketListItem/index.js b/frontend/src/components/TicketListItem/index.js index 62f2944..307ac0e 100644 --- a/frontend/src/components/TicketListItem/index.js +++ b/frontend/src/components/TicketListItem/index.js @@ -19,6 +19,7 @@ import { i18n } from "../../translate/i18n"; import api from "../../services/api"; import ButtonWithSpinner from "../ButtonWithSpinner"; import MarkdownWrapper from "../MarkdownWrapper"; +import { Tooltip } from "@material-ui/core"; const useStyles = makeStyles(theme => ({ ticket: { @@ -147,10 +148,16 @@ const TicketListItem = ({ ticket }) => { [classes.pendingTicket]: ticket.status === "pending", })} > - + + + Date: Tue, 12 Jan 2021 08:20:26 -0300 Subject: [PATCH 16/29] chore: changed font import --- frontend/public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/public/index.html b/frontend/public/index.html index f775733..59581ba 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -3,8 +3,8 @@ WhaTicket From 96e92e60796f1c89915e36d0eb94c971f0b464c9 Mon Sep 17 00:00:00 2001 From: canove Date: Tue, 12 Jan 2021 20:13:30 -0300 Subject: [PATCH 17/29] fix: user keep getting refresh tokens after logout --- backend/src/middleware/isAuth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/middleware/isAuth.ts b/backend/src/middleware/isAuth.ts index ae70f05..83cae2a 100644 --- a/backend/src/middleware/isAuth.ts +++ b/backend/src/middleware/isAuth.ts @@ -16,7 +16,7 @@ const isAuth = (req: Request, res: Response, next: NextFunction): void => { const authHeader = req.headers.authorization; if (!authHeader) { - throw new AppError("Token was not provided.", 403); + throw new AppError("ERR_SESSION_EXPIRED", 401); } const [, token] = authHeader.split(" "); From 2bec877e4f776f5060598a73080ad793fe36294a Mon Sep 17 00:00:00 2001 From: canove Date: Tue, 12 Jan 2021 20:30:31 -0300 Subject: [PATCH 18/29] improvement: keep user data on login fails --- frontend/src/context/Auth/useAuth.js | 7 +++---- frontend/src/pages/Login/index.js | 11 ++++++----- frontend/src/routes/Route.js | 21 ++++++++++++++++----- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/frontend/src/context/Auth/useAuth.js b/frontend/src/context/Auth/useAuth.js index b081bc0..68ac844 100644 --- a/frontend/src/context/Auth/useAuth.js +++ b/frontend/src/context/Auth/useAuth.js @@ -63,9 +63,8 @@ const useAuth = () => { setLoading(false); }, []); - const handleLogin = async (e, user) => { + const handleLogin = async user => { setLoading(true); - e.preventDefault(); try { const { data } = await api.post("/auth/login", user); @@ -78,11 +77,11 @@ const useAuth = () => { setIsAuth(true); toast.success(i18n.t("auth.toasts.success")); history.push("/tickets"); + setLoading(false); } catch (err) { toastError(err); + setLoading(false); } - - setLoading(false); }; const handleLogout = e => { diff --git a/frontend/src/pages/Login/index.js b/frontend/src/pages/Login/index.js index 4600680..ebb98d6 100644 --- a/frontend/src/pages/Login/index.js +++ b/frontend/src/pages/Login/index.js @@ -61,6 +61,11 @@ const Login = () => { setUser({ ...user, [e.target.name]: e.target.value }); }; + const handlSubmit = e => { + e.preventDefault(); + handleLogin(user); + }; + return ( @@ -71,11 +76,7 @@ const Login = () => { {i18n.t("login.title")} -
handleLogin(e, user)} - > + { const { isAuth, loading } = useContext(AuthContext); - if (loading) return ; - if (!isAuth && isPrivate) { return ( - + <> + {loading && } + + ); } if (isAuth && !isPrivate) { - return ; + return ( + <> + {loading && } + ; + + ); } - return ; + return ( + <> + {loading && } + + + ); }; export default RouteWrapper; From 3aa287d394332af38169ea6f3681af766224d31a Mon Sep 17 00:00:00 2001 From: canove Date: Wed, 13 Jan 2021 08:08:25 -0300 Subject: [PATCH 19/29] improvement: moved user data from localstorage to context --- backend/src/controllers/SessionController.ts | 10 ++-- backend/src/helpers/SerializeUser.ts | 20 +++++++ .../AuthServices/RefreshTokenService.ts | 5 +- .../services/UserServices/AuthUserSerice.ts | 19 +++++-- .../UserServices/CreateUserService.ts | 9 +--- frontend/src/accessRules.js | 11 ++++ frontend/src/components/Can/index.js | 39 ++++++++++++++ frontend/src/components/MessageInput/index.js | 25 ++++----- frontend/src/components/MessagesList/index.js | 2 +- .../src/components/NewTicketModal/index.js | 7 +-- .../components/NotificationsPopOver/index.js | 11 ++-- .../src/components/QueueSelector/index.js | 2 +- .../components/TicketActionButtons/index.js | 11 ++-- .../src/components/TicketListItem/index.js | 7 +-- frontend/src/components/TicketsList/index.js | 11 ++-- .../src/components/TicketsManager/index.js | 6 +-- frontend/src/context/Auth/AuthContext.js | 4 +- frontend/src/context/Auth/useAuth.js | 33 ++++++------ frontend/src/hooks/useLocalStorage/index.js | 29 ++++++++++ frontend/src/layout/MainListItems.js | 54 ++++++++++--------- frontend/src/layout/index.js | 41 +++++--------- frontend/src/pages/Contacts/index.js | 8 +-- frontend/src/pages/Queues/index.js | 1 - 23 files changed, 231 insertions(+), 134 deletions(-) create mode 100644 backend/src/helpers/SerializeUser.ts create mode 100644 frontend/src/accessRules.js create mode 100644 frontend/src/components/Can/index.js create mode 100644 frontend/src/hooks/useLocalStorage/index.js diff --git a/backend/src/controllers/SessionController.ts b/backend/src/controllers/SessionController.ts index b9dcf8e..3dc32c1 100644 --- a/backend/src/controllers/SessionController.ts +++ b/backend/src/controllers/SessionController.ts @@ -8,7 +8,7 @@ import { RefreshTokenService } from "../services/AuthServices/RefreshTokenServic export const store = async (req: Request, res: Response): Promise => { const { email, password } = req.body; - const { token, user, refreshToken } = await AuthUserService({ + const { token, serializedUser, refreshToken } = await AuthUserService({ email, password }); @@ -17,9 +17,7 @@ export const store = async (req: Request, res: Response): Promise => { return res.status(200).json({ token, - username: user.name, - profile: user.profile, - userId: user.id + user: serializedUser }); }; @@ -33,9 +31,9 @@ export const update = async ( throw new AppError("ERR_SESSION_EXPIRED", 401); } - const { newToken, refreshToken } = await RefreshTokenService(token); + const { user, newToken, refreshToken } = await RefreshTokenService(token); SendRefreshToken(res, refreshToken); - return res.json({ token: newToken }); + return res.json({ token: newToken, user }); }; diff --git a/backend/src/helpers/SerializeUser.ts b/backend/src/helpers/SerializeUser.ts new file mode 100644 index 0000000..3802500 --- /dev/null +++ b/backend/src/helpers/SerializeUser.ts @@ -0,0 +1,20 @@ +import Queue from "../models/Queue"; +import User from "../models/User"; + +interface SerializedUser { + id: number; + name: string; + email: string; + profile: string; + queues: Queue[]; +} + +export const SerializeUser = (user: User): SerializedUser => { + return { + id: user.id, + name: user.name, + email: user.email, + profile: user.profile, + queues: user.queues + }; +}; diff --git a/backend/src/services/AuthServices/RefreshTokenService.ts b/backend/src/services/AuthServices/RefreshTokenService.ts index fa7893c..78ed933 100644 --- a/backend/src/services/AuthServices/RefreshTokenService.ts +++ b/backend/src/services/AuthServices/RefreshTokenService.ts @@ -1,4 +1,6 @@ import { verify } from "jsonwebtoken"; + +import User from "../../models/User"; import AppError from "../../errors/AppError"; import ShowUserService from "../UserServices/ShowUserService"; import authConfig from "../../config/auth"; @@ -13,6 +15,7 @@ interface RefreshTokenPayload { } interface Response { + user: User; newToken: string; refreshToken: string; } @@ -37,5 +40,5 @@ export const RefreshTokenService = async (token: string): Promise => { const newToken = createAccessToken(user); const refreshToken = createRefreshToken(user); - return { newToken, refreshToken }; + return { user, newToken, refreshToken }; }; diff --git a/backend/src/services/UserServices/AuthUserSerice.ts b/backend/src/services/UserServices/AuthUserSerice.ts index 38d18dc..f198a7c 100644 --- a/backend/src/services/UserServices/AuthUserSerice.ts +++ b/backend/src/services/UserServices/AuthUserSerice.ts @@ -4,6 +4,16 @@ import { createAccessToken, createRefreshToken } from "../../helpers/CreateTokens"; +import { SerializeUser } from "../../helpers/SerializeUser"; +import Queue from "../../models/Queue"; + +interface SerializedUser { + id: number; + name: string; + email: string; + profile: string; + queues: Queue[]; +} interface Request { email: string; @@ -11,7 +21,7 @@ interface Request { } interface Response { - user: User; + serializedUser: SerializedUser; token: string; refreshToken: string; } @@ -21,7 +31,8 @@ const AuthUserService = async ({ password }: Request): Promise => { const user = await User.findOne({ - where: { email } + where: { email }, + include: ["queues"] }); if (!user) { @@ -35,8 +46,10 @@ const AuthUserService = async ({ const token = createAccessToken(user); const refreshToken = createRefreshToken(user); + const serializedUser = SerializeUser(user); + return { - user, + serializedUser, token, refreshToken }; diff --git a/backend/src/services/UserServices/CreateUserService.ts b/backend/src/services/UserServices/CreateUserService.ts index 930036a..e277085 100644 --- a/backend/src/services/UserServices/CreateUserService.ts +++ b/backend/src/services/UserServices/CreateUserService.ts @@ -1,6 +1,7 @@ import * as Yup from "yup"; import AppError from "../../errors/AppError"; +import { SerializeUser } from "../../helpers/SerializeUser"; import User from "../../models/User"; interface Request { @@ -63,13 +64,7 @@ const CreateUserService = async ({ await user.reload(); - const serializedUser = { - id: user.id, - name: user.name, - email: user.email, - profile: user.profile, - queues: user.queues - }; + const serializedUser = SerializeUser(user); return serializedUser; }; diff --git a/frontend/src/accessRules.js b/frontend/src/accessRules.js new file mode 100644 index 0000000..938dc32 --- /dev/null +++ b/frontend/src/accessRules.js @@ -0,0 +1,11 @@ +const rules = { + user: { + static: [], + }, + + admin: { + static: ["drawer-admin-items:view"], + }, +}; + +export default rules; diff --git a/frontend/src/components/Can/index.js b/frontend/src/components/Can/index.js new file mode 100644 index 0000000..96837c5 --- /dev/null +++ b/frontend/src/components/Can/index.js @@ -0,0 +1,39 @@ +import rules from "../../accessRules"; + +const check = (rules, role, action, data) => { + const permissions = rules[role]; + if (!permissions) { + // role is not present in the rules + return false; + } + + const staticPermissions = permissions.static; + + if (staticPermissions && staticPermissions.includes(action)) { + // static rule not provided for action + return true; + } + + const dynamicPermissions = permissions.dynamic; + + if (dynamicPermissions) { + const permissionCondition = dynamicPermissions[action]; + if (!permissionCondition) { + // dynamic rule not provided for action + return false; + } + + return permissionCondition(data); + } + return false; +}; + +const Can = ({ role, perform, data, yes, no }) => + check(rules, role, perform, data) ? yes() : no(); + +Can.defaultProps = { + yes: () => null, + no: () => null, +}; + +export default Can; diff --git a/frontend/src/components/MessageInput/index.js b/frontend/src/components/MessageInput/index.js index a5222f7..a6ce895 100644 --- a/frontend/src/components/MessageInput/index.js +++ b/frontend/src/components/MessageInput/index.js @@ -25,6 +25,8 @@ import { i18n } from "../../translate/i18n"; import api from "../../services/api"; import RecordingTimer from "./RecordingTimer"; import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext"; +import { AuthContext } from "../../context/Auth/AuthContext"; +import { useLocalStorage } from "../../hooks/useLocalStorage"; import toastError from "../../errors/toastError"; const Mp3Recorder = new MicRecorder({ bitRate: 128 }); @@ -164,7 +166,6 @@ const useStyles = makeStyles(theme => ({ const MessageInput = ({ ticketStatus }) => { const classes = useStyles(); const { ticketId } = useParams(); - const username = localStorage.getItem("username"); const [medias, setMedias] = useState([]); const [inputMessage, setInputMessage] = useState(""); @@ -175,17 +176,9 @@ const MessageInput = ({ ticketStatus }) => { const { setReplyingMessage, replyingMessage } = useContext( ReplyMessageContext ); + const { user } = useContext(AuthContext); - const [signMessage, setSignMessage] = useState(false); - - useEffect(() => { - const storedSignOption = localStorage.getItem("signOption"); - if (storedSignOption === "true") setSignMessage(true); - }, []); - - useEffect(() => { - localStorage.setItem("signOption", signMessage); - }, [signMessage]); + const [signMessage, setSignMessage] = useLocalStorage("signOption", true); useEffect(() => { inputRef.current.focus(); @@ -255,7 +248,7 @@ const MessageInput = ({ ticketStatus }) => { fromMe: true, mediaUrl: "", body: signMessage - ? `*${username}:*\n${inputMessage.trim()}` + ? `*${user?.name}:*\n${inputMessage.trim()}` : inputMessage.trim(), quotedMsg: replyingMessage, }; @@ -279,7 +272,7 @@ const MessageInput = ({ ticketStatus }) => { setRecording(true); setLoading(false); } catch (err) { - console.log(err); + toastError(err); setLoading(false); } }; @@ -314,7 +307,7 @@ const MessageInput = ({ ticketStatus }) => { await Mp3Recorder.stop().getMp3(); setRecording(false); } catch (err) { - console.log(err); + toastError(err); } }; @@ -428,8 +421,8 @@ const MessageInput = ({ ticketStatus }) => { { - setSignMessage(prevState => !prevState); + onChange={e => { + setSignMessage(e.target.checked); }} name="showAllTickets" color="primary" diff --git a/frontend/src/components/MessagesList/index.js b/frontend/src/components/MessagesList/index.js index d3fa2a0..e22af75 100644 --- a/frontend/src/components/MessagesList/index.js +++ b/frontend/src/components/MessagesList/index.js @@ -301,7 +301,7 @@ const reducer = (state, action) => { } }; -const MessagesList = ({ ticketId, isGroup, setReplyingMessage }) => { +const MessagesList = ({ ticketId, isGroup }) => { const classes = useStyles(); const [messagesList, dispatch] = useReducer(reducer, []); diff --git a/frontend/src/components/NewTicketModal/index.js b/frontend/src/components/NewTicketModal/index.js index 674f2e0..898aa92 100644 --- a/frontend/src/components/NewTicketModal/index.js +++ b/frontend/src/components/NewTicketModal/index.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useContext } from "react"; import { useHistory } from "react-router-dom"; import Button from "@material-ui/core/Button"; @@ -18,6 +18,7 @@ import api from "../../services/api"; import ButtonWithSpinner from "../ButtonWithSpinner"; import ContactModal from "../ContactModal"; import toastError from "../../errors/toastError"; +import { AuthContext } from "../../context/Auth/AuthContext"; const filter = createFilterOptions({ trim: true, @@ -25,7 +26,6 @@ const filter = createFilterOptions({ const NewTicketModal = ({ modalOpen, onClose }) => { const history = useHistory(); - const userId = +localStorage.getItem("userId"); const [options, setOptions] = useState([]); const [loading, setLoading] = useState(false); @@ -33,6 +33,7 @@ const NewTicketModal = ({ modalOpen, onClose }) => { const [selectedContact, setSelectedContact] = useState(null); const [newContact, setNewContact] = useState({}); const [contactModalOpen, setContactModalOpen] = useState(false); + const { user } = useContext(AuthContext); useEffect(() => { if (!modalOpen || searchParam.length < 3) { @@ -71,7 +72,7 @@ const NewTicketModal = ({ modalOpen, onClose }) => { try { const { data: ticket } = await api.post("/tickets", { contactId: contactId, - userId: userId, + userId: user.id, status: "open", }); history.push(`/tickets/${ticket.id}`); diff --git a/frontend/src/components/NotificationsPopOver/index.js b/frontend/src/components/NotificationsPopOver/index.js index 4c408aa..886e622 100644 --- a/frontend/src/components/NotificationsPopOver/index.js +++ b/frontend/src/components/NotificationsPopOver/index.js @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useRef, useEffect, useContext } from "react"; import { useHistory } from "react-router-dom"; import { format } from "date-fns"; @@ -18,6 +18,7 @@ import TicketListItem from "../TicketListItem"; import { i18n } from "../../translate/i18n"; import useTickets from "../../hooks/useTickets"; import alertSound from "../../assets/sound.mp3"; +import { AuthContext } from "../../context/Auth/AuthContext"; const useStyles = makeStyles(theme => ({ tabContainer: { @@ -43,7 +44,7 @@ const NotificationsPopOver = () => { const classes = useStyles(); const history = useHistory(); - const userId = +localStorage.getItem("userId"); + const { user } = useContext(AuthContext); const ticketIdUrl = +history.location.pathname.split("/")[2]; const ticketIdRef = useRef(ticketIdUrl); const anchorEl = useRef(); @@ -108,7 +109,7 @@ const NotificationsPopOver = () => { if ( data.action === "create" && !data.message.read && - (data.ticket.userId === userId || !data.ticket.userId) + (data.ticket.userId === user?.id || !data.ticket.userId) ) { setNotifications(prevState => { const ticketIndex = prevState.findIndex(t => t.id === data.ticket.id); @@ -122,7 +123,7 @@ const NotificationsPopOver = () => { const shouldNotNotificate = (data.message.ticketId === ticketIdRef.current && document.visibilityState === "visible") || - (data.ticket.userId && data.ticket.userId !== userId) || + (data.ticket.userId && data.ticket.userId !== user?.id) || data.ticket.isGroup; if (shouldNotNotificate) return; @@ -134,7 +135,7 @@ const NotificationsPopOver = () => { return () => { socket.disconnect(); }; - }, [history, userId]); + }, [history, user]); const handleNotifications = (data, history) => { const { message, contact, ticket } = data; diff --git a/frontend/src/components/QueueSelector/index.js b/frontend/src/components/QueueSelector/index.js index 545131c..f420105 100644 --- a/frontend/src/components/QueueSelector/index.js +++ b/frontend/src/components/QueueSelector/index.js @@ -41,7 +41,7 @@ const QueueSelector = ({ selectedQueueIds, onChange }) => { return (
- + Filas } MenuProps={{ anchorOrigin: { vertical: "bottom", @@ -61,12 +59,12 @@ const QueueSelector = ({ selectedQueueIds, onChange }) => { }} renderValue={selected => (
- {selected.length > 0 && - selected.map(value => { - const queue = queues.find(q => q.id === value); + {selected?.length > 0 && + selected.map(id => { + const queue = queues.find(q => q.id === id); return queue ? ( { ); }; -export default QueueSelector; +export default QueueSelect; diff --git a/frontend/src/components/TicketsList/index.js b/frontend/src/components/TicketsList/index.js index fd96017..6be0232 100644 --- a/frontend/src/components/TicketsList/index.js +++ b/frontend/src/components/TicketsList/index.js @@ -7,7 +7,10 @@ import Paper from "@material-ui/core/Paper"; import TicketListItem from "../TicketListItem"; import TicketsListSkeleton from "../TicketsListSkeleton"; +import TicketsListQueueSelect from "../TicketsListQueueSelect"; + import useTickets from "../../hooks/useTickets"; +import { useLocalStorage } from "../../hooks/useLocalStorage"; import { i18n } from "../../translate/i18n"; import { ListSubheader } from "@material-ui/core"; import { AuthContext } from "../../context/Auth/AuthContext"; @@ -35,6 +38,9 @@ const useStyles = makeStyles(theme => ({ zIndex: 2, backgroundColor: "white", borderBottom: "1px solid rgba(0, 0, 0, 0.12)", + display: "flex", + alignItems: "center", + justifyContent: "space-between", }, ticketsCount: { @@ -154,17 +160,36 @@ const TicketsList = ({ status, searchParam, showAll }) => { const [pageNumber, setPageNumber] = useState(1); const [ticketsList, dispatch] = useReducer(reducer, []); const { user } = useContext(AuthContext); + const [pendingSelectedQueueIds, setPendingSelectedQueueIds] = useLocalStorage( + "pendingSelectedQueues", + [] + ); + const [openSelectedQueueIds, setOpenSelectedQueueIds] = useLocalStorage( + "openSelectedQueues", + [] + ); useEffect(() => { dispatch({ type: "RESET" }); setPageNumber(1); - }, [status, searchParam, dispatch, showAll]); + }, [ + status, + searchParam, + dispatch, + showAll, + openSelectedQueueIds, + pendingSelectedQueueIds, + ]); const { tickets, hasMore, loading } = useTickets({ pageNumber, searchParam, status, showAll, + queueIds: + status === "open" + ? JSON.stringify(openSelectedQueueIds) + : JSON.stringify(pendingSelectedQueueIds), }); useEffect(() => { @@ -251,6 +276,15 @@ const TicketsList = ({ status, searchParam, showAll }) => { } }; + const handleSelectedQueues = values => { + if (status === "open") { + setOpenSelectedQueueIds(values); + } + if (status === "pending") { + setPendingSelectedQueueIds(values); + } + }; + return (
{ {status === "open" && ( - {i18n.t("ticketsList.assignedHeader")} - {ticketsList.length} +
+ {i18n.t("ticketsList.assignedHeader")} + + {ticketsList.length} + +
+
)} {status === "pending" && ( - {i18n.t("ticketsList.pendingHeader")} - {ticketsList.length} +
+ {i18n.t("ticketsList.pendingHeader")} + + {ticketsList.length} + +
+
)} {ticketsList.length === 0 && !loading ? ( diff --git a/frontend/src/components/TicketsListQueueSelect/index.js b/frontend/src/components/TicketsListQueueSelect/index.js new file mode 100644 index 0000000..b6ed048 --- /dev/null +++ b/frontend/src/components/TicketsListQueueSelect/index.js @@ -0,0 +1,59 @@ +import React from "react"; + +import MenuItem from "@material-ui/core/MenuItem"; +import FormControl from "@material-ui/core/FormControl"; +import Select from "@material-ui/core/Select"; +import { Checkbox, ListItemText } from "@material-ui/core"; + +const TicketsListQueueSelect = ({ + userQueues, + selectedQueueIds = [], + onChange, +}) => { + const handleChange = e => { + onChange(e.target.value); + }; + + return ( +
+ + + +
+ ); +}; + +export default TicketsListQueueSelect; diff --git a/frontend/src/components/UserModal/index.js b/frontend/src/components/UserModal/index.js index a01012c..b1539c8 100644 --- a/frontend/src/components/UserModal/index.js +++ b/frontend/src/components/UserModal/index.js @@ -22,7 +22,7 @@ import { i18n } from "../../translate/i18n"; import api from "../../services/api"; import toastError from "../../errors/toastError"; -import QueueSelector from "../QueueSelector"; +import QueueSelect from "../QueueSelect"; const useStyles = makeStyles(theme => ({ root: { @@ -198,7 +198,7 @@ const UserModal = ({ open, onClose, userId }) => {
- setSelectedQueueIds(values)} /> diff --git a/frontend/src/components/WhatsAppModal/index.js b/frontend/src/components/WhatsAppModal/index.js index 73bdf02..853b8bd 100644 --- a/frontend/src/components/WhatsAppModal/index.js +++ b/frontend/src/components/WhatsAppModal/index.js @@ -21,7 +21,7 @@ import { import api from "../../services/api"; import { i18n } from "../../translate/i18n"; import toastError from "../../errors/toastError"; -import QueueSelector from "../QueueSelector"; +import QueueSelect from "../QueueSelect"; const useStyles = makeStyles(theme => ({ root: { @@ -176,7 +176,7 @@ const WhatsAppModal = ({ open, onClose, whatsAppId }) => { margin="dense" />
- setSelectedQueueIds(values)} /> diff --git a/frontend/src/context/Auth/useAuth.js b/frontend/src/context/Auth/useAuth.js index 182e3ea..cba200c 100644 --- a/frontend/src/context/Auth/useAuth.js +++ b/frontend/src/context/Auth/useAuth.js @@ -6,7 +6,6 @@ import { toast } from "react-toastify"; import { i18n } from "../../translate/i18n"; import api from "../../services/api"; import toastError from "../../errors/toastError"; -// import { useLocalStorage } from "../../hooks/useLocalStorage"; const useAuth = () => { const history = useHistory(); @@ -62,12 +61,11 @@ const useAuth = () => { api.defaults.headers.Authorization = `Bearer ${data.token}`; setIsAuth(true); setUser(data.user); - setLoading(false); } catch (err) { toastError(err); - setLoading(false); } } + setLoading(false); })(); }, []); diff --git a/frontend/src/hooks/useTickets/index.js b/frontend/src/hooks/useTickets/index.js index e6eeed3..97b6e7f 100644 --- a/frontend/src/hooks/useTickets/index.js +++ b/frontend/src/hooks/useTickets/index.js @@ -9,6 +9,7 @@ const useTickets = ({ status, date, showAll, + queueIds, withUnreadMessages, }) => { const [loading, setLoading] = useState(true); @@ -27,6 +28,7 @@ const useTickets = ({ status, date, showAll, + queueIds, withUnreadMessages, }, }); @@ -41,7 +43,15 @@ const useTickets = ({ fetchTickets(); }, 500); return () => clearTimeout(delayDebounceFn); - }, [searchParam, pageNumber, status, date, showAll, withUnreadMessages]); + }, [ + searchParam, + pageNumber, + status, + date, + showAll, + queueIds, + withUnreadMessages, + ]); return { tickets, loading, hasMore }; }; From c61c99357230b57f5f4e1ba43a8a135ea5347129 Mon Sep 17 00:00:00 2001 From: canove Date: Wed, 13 Jan 2021 16:02:05 -0300 Subject: [PATCH 22/29] feat: strict "show all" option to admin only --- frontend/src/accessRules.js | 2 +- .../src/components/TicketsManager/index.js | 34 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/frontend/src/accessRules.js b/frontend/src/accessRules.js index 938dc32..e282def 100644 --- a/frontend/src/accessRules.js +++ b/frontend/src/accessRules.js @@ -4,7 +4,7 @@ const rules = { }, admin: { - static: ["drawer-admin-items:view"], + static: ["drawer-admin-items:view", "tickets-manager:showall"], }, }; diff --git a/frontend/src/components/TicketsManager/index.js b/frontend/src/components/TicketsManager/index.js index 75558af..1a24053 100644 --- a/frontend/src/components/TicketsManager/index.js +++ b/frontend/src/components/TicketsManager/index.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { makeStyles } from "@material-ui/core/styles"; import Paper from "@material-ui/core/Paper"; @@ -18,6 +18,8 @@ import TicketsList from "../TicketsList"; import TabPanel from "../TabPanel"; import { i18n } from "../../translate/i18n"; +import { AuthContext } from "../../context/Auth/AuthContext"; +import Can from "../Can"; const useStyles = makeStyles(theme => ({ ticketsWrapper: { @@ -88,6 +90,7 @@ const TicketsManager = () => { const [tab, setTab] = useState("open"); const [newTicketModalOpen, setNewTicketModalOpen] = useState(false); const [showAllTickets, setShowAllTickets] = useState(false); + const { user } = useContext(AuthContext); const handleSearchContact = e => { if (e.target.value === "") { @@ -149,19 +152,26 @@ const TicketsManager = () => { />
- setShowAllTickets(prevState => !prevState)} - name="showAllTickets" - color="primary" + ( + setShowAllTickets(prevState => !prevState)} + name="showAllTickets" + color="primary" + /> + } /> - } + )} /> + Date: Wed, 13 Jan 2021 19:43:50 -0300 Subject: [PATCH 23/29] feat: finished queue filter on tickets list --- backend/src/controllers/TicketController.ts | 4 +- .../TicketServices/ListTicketsService.ts | 9 +- frontend/src/components/Ticket/index.js | 2 +- frontend/src/components/TicketsList/index.js | 77 ++------- .../src/components/TicketsManager/index.js | 160 +++++++++++------- .../index.js | 6 +- 6 files changed, 124 insertions(+), 134 deletions(-) rename frontend/src/components/{TicketsListQueueSelect => TicketsQueueSelect}/index.js (91%) diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index 43d5678..93b03c1 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -63,7 +63,7 @@ export const store = async (req: Request, res: Response): Promise => { const io = getIO(); io.to(ticket.status).emit("ticket", { - action: "create", + action: "update", ticket }); @@ -100,7 +100,7 @@ export const update = async ( } io.to(ticket.status).to("notification").to(ticketId).emit("ticket", { - action: "updateStatus", + action: "update", ticket }); diff --git a/backend/src/services/TicketServices/ListTicketsService.ts b/backend/src/services/TicketServices/ListTicketsService.ts index cc1938a..b616933 100644 --- a/backend/src/services/TicketServices/ListTicketsService.ts +++ b/backend/src/services/TicketServices/ListTicketsService.ts @@ -34,12 +34,6 @@ const ListTicketsService = async ({ userId, withUnreadMessages }: Request): Promise => { - // const user = await ShowUserService(userId); - - // const userQueueIds = user.queues.map(queue => queue.id); - - // console.log(userQueueIds); - let whereCondition: Filterable["where"] = { [Op.or]: [{ userId }, { status: "pending" }], queueId: { [Op.or]: [queueIds, null] } @@ -60,7 +54,7 @@ const ListTicketsService = async ({ ]; if (showAll === "true") { - whereCondition = {}; + whereCondition = { queueId: { [Op.or]: [queueIds, null] } }; } if (status) { @@ -92,6 +86,7 @@ const ListTicketsService = async ({ ]; whereCondition = { + ...whereCondition, [Op.or]: [ { "$contact.name$": where( diff --git a/frontend/src/components/Ticket/index.js b/frontend/src/components/Ticket/index.js index bf694ee..c2cdd37 100644 --- a/frontend/src/components/Ticket/index.js +++ b/frontend/src/components/Ticket/index.js @@ -90,7 +90,7 @@ const Ticket = () => { socket.on("connect", () => socket.emit("joinChatBox", ticketId)); socket.on("ticket", data => { - if (data.action === "updateStatus") { + if (data.action === "update") { setTicket(data.ticket); } diff --git a/frontend/src/components/TicketsList/index.js b/frontend/src/components/TicketsList/index.js index 6be0232..e8eb0e6 100644 --- a/frontend/src/components/TicketsList/index.js +++ b/frontend/src/components/TicketsList/index.js @@ -7,10 +7,8 @@ import Paper from "@material-ui/core/Paper"; import TicketListItem from "../TicketListItem"; import TicketsListSkeleton from "../TicketsListSkeleton"; -import TicketsListQueueSelect from "../TicketsListQueueSelect"; import useTickets from "../../hooks/useTickets"; -import { useLocalStorage } from "../../hooks/useLocalStorage"; import { i18n } from "../../translate/i18n"; import { ListSubheader } from "@material-ui/core"; import { AuthContext } from "../../context/Auth/AuthContext"; @@ -117,14 +115,14 @@ const reducer = (state, action) => { return [...state]; } - if (action.type === "UPDATE_TICKET_MESSAGES_COUNT") { - const { ticket, searchParam } = action.payload; + if (action.type === "UPDATE_TICKET_UNREAD_MESSAGES") { + const ticket = action.payload; const ticketIndex = state.findIndex(t => t.id === ticket.id); if (ticketIndex !== -1) { state[ticketIndex] = ticket; state.unshift(state.splice(ticketIndex, 1)[0]); - } else if (!searchParam) { + } else { state.unshift(ticket); } @@ -155,53 +153,40 @@ const reducer = (state, action) => { } }; -const TicketsList = ({ status, searchParam, showAll }) => { +const TicketsList = ({ status, searchParam, showAll, selectedQueueIds }) => { const classes = useStyles(); const [pageNumber, setPageNumber] = useState(1); const [ticketsList, dispatch] = useReducer(reducer, []); const { user } = useContext(AuthContext); - const [pendingSelectedQueueIds, setPendingSelectedQueueIds] = useLocalStorage( - "pendingSelectedQueues", - [] - ); - const [openSelectedQueueIds, setOpenSelectedQueueIds] = useLocalStorage( - "openSelectedQueues", - [] - ); useEffect(() => { dispatch({ type: "RESET" }); setPageNumber(1); - }, [ - status, - searchParam, - dispatch, - showAll, - openSelectedQueueIds, - pendingSelectedQueueIds, - ]); + }, [status, searchParam, dispatch, showAll, selectedQueueIds]); const { tickets, hasMore, loading } = useTickets({ pageNumber, searchParam, status, showAll, - queueIds: - status === "open" - ? JSON.stringify(openSelectedQueueIds) - : JSON.stringify(pendingSelectedQueueIds), + queueIds: JSON.stringify(selectedQueueIds), }); useEffect(() => { + if (!status && !searchParam) return; dispatch({ type: "LOAD_TICKETS", payload: tickets, }); - }, [tickets]); + }, [tickets, status, searchParam]); useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + const shouldUpdateTicket = ticket => + (!ticket.userId || ticket.userId === user?.id || showAll) && + selectedQueueIds.indexOf(ticket.queueId) > -1; + socket.on("connect", () => { if (status) { socket.emit("joinTickets", status); @@ -218,10 +203,7 @@ const TicketsList = ({ status, searchParam, showAll }) => { }); } - if ( - (data.action === "updateStatus" || data.action === "create") && - (!data.ticket.userId || data.ticket.userId === user?.id || showAll) - ) { + if (data.action === "update" && shouldUpdateTicket(data.ticket)) { dispatch({ type: "UPDATE_TICKET", payload: data.ticket, @@ -234,16 +216,10 @@ const TicketsList = ({ status, searchParam, showAll }) => { }); socket.on("appMessage", data => { - if ( - data.action === "create" && - (!data.ticket.userId || data.ticket.userId === user?.id || showAll) - ) { + if (data.action === "create" && shouldUpdateTicket(data.ticket)) { dispatch({ - type: "UPDATE_TICKET_MESSAGES_COUNT", - payload: { - ticket: data.ticket, - searchParam, - }, + type: "UPDATE_TICKET_UNREAD_MESSAGES", + payload: data.ticket, }); } }); @@ -260,7 +236,7 @@ const TicketsList = ({ status, searchParam, showAll }) => { return () => { socket.disconnect(); }; - }, [status, showAll, user, searchParam]); + }, [status, showAll, user, selectedQueueIds]); const loadMore = () => { setPageNumber(prevState => prevState + 1); @@ -276,15 +252,6 @@ const TicketsList = ({ status, searchParam, showAll }) => { } }; - const handleSelectedQueues = values => { - if (status === "open") { - setOpenSelectedQueueIds(values); - } - if (status === "pending") { - setPendingSelectedQueueIds(values); - } - }; - return (
{ {ticketsList.length}
- )} {status === "pending" && ( @@ -318,11 +280,6 @@ const TicketsList = ({ status, searchParam, showAll }) => { {ticketsList.length}
- )} {ticketsList.length === 0 && !loading ? ( diff --git a/frontend/src/components/TicketsManager/index.js b/frontend/src/components/TicketsManager/index.js index 1a24053..3471742 100644 --- a/frontend/src/components/TicketsManager/index.js +++ b/frontend/src/components/TicketsManager/index.js @@ -1,4 +1,4 @@ -import React, { useContext, useState } from "react"; +import React, { useContext, useEffect, useRef, useState } from "react"; import { makeStyles } from "@material-ui/core/styles"; import Paper from "@material-ui/core/Paper"; @@ -8,8 +8,7 @@ import Tabs from "@material-ui/core/Tabs"; import Tab from "@material-ui/core/Tab"; import MoveToInboxIcon from "@material-ui/icons/MoveToInbox"; import CheckBoxIcon from "@material-ui/icons/CheckBox"; -import IconButton from "@material-ui/core/IconButton"; -import AddIcon from "@material-ui/icons/Add"; + import FormControlLabel from "@material-ui/core/FormControlLabel"; import Switch from "@material-ui/core/Switch"; @@ -20,6 +19,9 @@ import TabPanel from "../TabPanel"; import { i18n } from "../../translate/i18n"; import { AuthContext } from "../../context/Auth/AuthContext"; import Can from "../Can"; +import TicketsQueueSelect from "../TicketsQueueSelect"; +import { Button } from "@material-ui/core"; +import { useLocalStorage } from "../../hooks/useLocalStorage"; const useStyles = makeStyles(theme => ({ ticketsWrapper: { @@ -48,17 +50,12 @@ const useStyles = makeStyles(theme => ({ width: 120, }, - ticketsListActions: { - flex: "none", - marginLeft: "auto", - }, - - searchBox: { - position: "relative", + ticketOptionsBox: { display: "flex", + justifyContent: "space-between", alignItems: "center", background: "#fafafa", - padding: "10px 13px", + padding: theme.spacing(1), }, serachInputWrapper: { @@ -67,6 +64,7 @@ const useStyles = makeStyles(theme => ({ display: "flex", borderRadius: 40, padding: 4, + marginRight: theme.spacing(1), }, searchIcon: { @@ -91,15 +89,34 @@ const TicketsManager = () => { const [newTicketModalOpen, setNewTicketModalOpen] = useState(false); const [showAllTickets, setShowAllTickets] = useState(false); const { user } = useContext(AuthContext); + const searchInputRef = useRef(); + const [selectedQueueIds, setSelectedQueueIds] = useLocalStorage( + "selectedQueueIds", + [] + ); - const handleSearchContact = e => { - if (e.target.value === "") { - setSearchParam(e.target.value.toLowerCase()); + useEffect(() => { + if (tab === "search") { + searchInputRef.current.focus(); + } + }, [tab]); + + let searchTimeout; + + const handleSearch = e => { + const searchedTerm = e.target.value.toLowerCase(); + + clearTimeout(searchTimeout); + + if (searchedTerm === "") { + setSearchParam(searchedTerm); setTab("open"); return; } - setSearchParam(e.target.value.toLowerCase()); - setTab("search"); + + searchTimeout = setTimeout(() => { + setSearchParam(searchedTerm); + }, 500); }; const handleChangeTab = (e, newValue) => { @@ -141,57 +158,78 @@ const TicketsManager = () => { /> - -
- - -
-
- ( - setShowAllTickets(prevState => !prevState)} - name="showAllTickets" - color="primary" - /> - } - /> - )} - /> - - setNewTicketModalOpen(true)} - style={{ marginLeft: 20 }} - > - - -
+ + {tab === "search" ? ( +
+ + +
+ ) : ( + <> + + ( + + setShowAllTickets(prevState => !prevState) + } + name="showAllTickets" + color="primary" + /> + } + /> + )} + /> + + )} + setSelectedQueueIds(values)} + />
- - + + - + - +
); diff --git a/frontend/src/components/TicketsListQueueSelect/index.js b/frontend/src/components/TicketsQueueSelect/index.js similarity index 91% rename from frontend/src/components/TicketsListQueueSelect/index.js rename to frontend/src/components/TicketsQueueSelect/index.js index b6ed048..a774eb2 100644 --- a/frontend/src/components/TicketsListQueueSelect/index.js +++ b/frontend/src/components/TicketsQueueSelect/index.js @@ -5,7 +5,7 @@ import FormControl from "@material-ui/core/FormControl"; import Select from "@material-ui/core/Select"; import { Checkbox, ListItemText } from "@material-ui/core"; -const TicketsListQueueSelect = ({ +const TicketsQueueSelect = ({ userQueues, selectedQueueIds = [], onChange, @@ -15,7 +15,7 @@ const TicketsListQueueSelect = ({ }; return ( -
+