From 9f040ce953d07393a951e0738b81ce94672cf240 Mon Sep 17 00:00:00 2001 From: adriano Date: Mon, 25 Mar 2024 16:00:24 -0300 Subject: [PATCH 01/20] feat: Improve display of queue and WhatsApp number-based interaction report --- backend/src/controllers/ReportController.ts | 5 +- backend/src/models/User.ts | 2 +- .../ReportByNumberQueueService.ts | 177 +++++++++++------- frontend/src/pages/Report/index.js | 138 ++------------ 4 files changed, 132 insertions(+), 190 deletions(-) diff --git a/backend/src/controllers/ReportController.ts b/backend/src/controllers/ReportController.ts index 6cd6a60..01664d4 100644 --- a/backend/src/controllers/ReportController.ts +++ b/backend/src/controllers/ReportController.ts @@ -331,10 +331,7 @@ export const reportServiceByQueue = async ( const { startDate, endDate, queueId } = req.query as IndexQuery; - console.log( - `startDate: ${startDate} | endDate: ${endDate} | queueId: ${queueId}` - ); - + const reportService = await ReportByNumberQueueService({ startDate, endDate, diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index af93cd5..a2fde6a 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -30,7 +30,7 @@ class User extends Model { name: string; @Column - email: string; + email: string; @Column(DataType.VIRTUAL) password: string; diff --git a/backend/src/services/ReportServices/ReportByNumberQueueService.ts b/backend/src/services/ReportServices/ReportByNumberQueueService.ts index 5baf96a..32cd1f4 100644 --- a/backend/src/services/ReportServices/ReportByNumberQueueService.ts +++ b/backend/src/services/ReportServices/ReportByNumberQueueService.ts @@ -79,41 +79,43 @@ const ReportByNumberQueueService = async ({ { type: QueryTypes.SELECT } ); - // CHAT AVG WAINTING TIME + // CHAT WAINTING TIME const avgChatWaitingTime: any = await sequelize.query( - `SELECT SEC_TO_TIME( - AVG( - TIMESTAMPDIFF( - SECOND, - ( - SELECT createdAt - FROM Messages - WHERE ticketId = m.ticketId - AND fromMe = 0 - ORDER BY createdAt ASC - LIMIT 1 - ), - ( - SELECT createdAt - FROM Messages - WHERE ticketId = m.ticketId - AND fromAgent = 1 - ORDER BY createdAt ASC - LIMIT 1 - ) - ) - ) - ) AS AVG_AWAITING_TIME + ` + SELECT TIME_FORMAT( + SEC_TO_TIME( + TIMESTAMPDIFF( + SECOND, + ( + SELECT createdAt + FROM Messages + WHERE ticketId = m.ticketId + AND fromMe = 0 + ORDER BY createdAt ASC + LIMIT 1 + ), + ( + SELECT createdAt + FROM Messages + WHERE ticketId = m.ticketId + AND fromAgent = 1 + ORDER BY createdAt ASC + LIMIT 1 + ) + ) + ), '%H:%i:%s') AS WAITING_TIME FROM Tickets t JOIN Messages m ON t.id = m.ticketId JOIN Whatsapps w ON t.whatsappId = w.id JOIN Queues q ON q.id = t.queueId WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' - AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) - AND m.fromMe = 0 - -- AND q.id = 2 - AND w.number = ${number} - AND (t.status = 'open' OR t.status = 'closed');`, + AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) + AND m.fromMe = 0 + -- AND q.id = 2 + AND w.number = ${number} + AND (t.status = 'open' OR t.status = 'closed') + ORDER BY + WAITING_TIME;`, { type: QueryTypes.SELECT } ); @@ -129,8 +131,8 @@ const ReportByNumberQueueService = async ({ AND w.number = ${number};`, { type: QueryTypes.SELECT } - ); - + ); + reportServiceData.push({ id, name, @@ -138,9 +140,7 @@ const ReportByNumberQueueService = async ({ startedByAgent: startedByAgent[0]?.ticket_count, startedByClient: startedByClient[0]?.ticket_count, closedChat: closedChat[0]?.ticket_count, - avgChatWaitingTime: avgChatWaitingTime[0]?.AVG_AWAITING_TIME - ? avgChatWaitingTime[0]?.AVG_AWAITING_TIME - : 0, + avgChatWaitingTime: avg(avgChatWaitingTime), pendingChat: pendingChat[0]?.ticket_count }); } @@ -205,31 +205,30 @@ const ReportByNumberQueueService = async ({ { type: QueryTypes.SELECT } ); - // CHAT AVG WAINTING TIME + // CHAT WAINTING TIME const avgChatWaitingTime: any = await sequelize.query( - `SELECT SEC_TO_TIME( - AVG( - TIMESTAMPDIFF( - SECOND, - ( - SELECT createdAt - FROM Messages - WHERE ticketId = m.ticketId - AND fromMe = 0 - ORDER BY createdAt ASC - LIMIT 1 - ), - ( - SELECT createdAt - FROM Messages - WHERE ticketId = m.ticketId - AND fromAgent = 1 - ORDER BY createdAt ASC - LIMIT 1 - ) - ) - ) - ) AS AVG_AWAITING_TIME + `SELECT TIME_FORMAT( + SEC_TO_TIME( + TIMESTAMPDIFF( + SECOND, + ( + SELECT createdAt + FROM Messages + WHERE ticketId = m.ticketId + AND fromMe = 0 + ORDER BY createdAt ASC + LIMIT 1 + ), + ( + SELECT createdAt + FROM Messages + WHERE ticketId = m.ticketId + AND fromAgent = 1 + ORDER BY createdAt ASC + LIMIT 1 + ) + ) + ), '%H:%i:%s') AS WAITING_TIME FROM Tickets t JOIN Messages m ON t.id = m.ticketId JOIN Whatsapps w ON t.whatsappId = w.id @@ -238,7 +237,9 @@ const ReportByNumberQueueService = async ({ AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) AND m.fromMe = 0 AND q.id = ${q.id} - AND (t.status = 'open' OR t.status = 'closed');`, + AND (t.status = 'open' OR t.status = 'closed') + ORDER BY + WAITING_TIME;`, { type: QueryTypes.SELECT } ); @@ -254,7 +255,7 @@ const ReportByNumberQueueService = async ({ AND q.id = ${q.id};`, { type: QueryTypes.SELECT } - ); + ); reportServiceData.push({ id, @@ -265,9 +266,7 @@ const ReportByNumberQueueService = async ({ startedByAgent: startedByAgent[0]?.ticket_count, startedByClient: startedByClient[0]?.ticket_count, closedChat: closedChat[0]?.ticket_count, - avgChatWaitingTime: avgChatWaitingTime[0]?.AVG_AWAITING_TIME - ? avgChatWaitingTime[0]?.AVG_AWAITING_TIME - : 0, + avgChatWaitingTime: avg(avgChatWaitingTime), pendingChat: pendingChat[0]?.ticket_count }); } @@ -278,3 +277,55 @@ const ReportByNumberQueueService = async ({ }; export default ReportByNumberQueueService; + +function avg(avgChatWaitingTime: any) { + let waitingAVG: any = avgChatWaitingTime + .filter((t: any) => t?.WAITING_TIME) + .map((t: any) => t.WAITING_TIME) + + if (waitingAVG.length > 0) { + let midIndex = Math.floor((0 + waitingAVG.length) / 2) + + if (waitingAVG.length % 2 == 1) { + waitingAVG = waitingAVG[midIndex] + } else { + waitingAVG = calculateAverageTime( + waitingAVG[midIndex - 1], + waitingAVG[midIndex] + ) + } + } else { + waitingAVG = 0 + } + return waitingAVG +} + +function calculateAverageTime(time1: string, time2: string) { + // Function to parse time string to seconds + function timeStringToSeconds(timeString: string) { + const [hours, minutes, seconds] = timeString.split(":").map(Number); + return hours * 3600 + minutes * 60 + seconds; + } + + // Function to convert seconds to time string + function secondsToTimeString(seconds: number) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + return `${hours.toString().padStart(2, "0")}:${minutes + .toString() + .padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`; + } + + // Convert time strings to seconds + const time1Seconds = timeStringToSeconds(time1); + const time2Seconds = timeStringToSeconds(time2); + + // Calculate average seconds + const averageSeconds = Math.round((time1Seconds + time2Seconds) / 2); + + // Convert average seconds back to time string + const averageTime = secondsToTimeString(averageSeconds); + + return averageTime; +} diff --git a/frontend/src/pages/Report/index.js b/frontend/src/pages/Report/index.js index 9295a8b..055f95e 100644 --- a/frontend/src/pages/Report/index.js +++ b/frontend/src/pages/Report/index.js @@ -395,15 +395,13 @@ const Report = () => { else if (reportOption === '3') { const dataQuery = await api.get("/reports/services/numbers", { params: { startDate, endDate }, }) - console.log('DATA QUERY.data numbers: ', dataQuery.data) - dispatchQ({ type: "RESET" }) dispatchQ({ type: "LOAD_QUERY", payload: dataQuery?.data?.reportService }) } else if (reportOption === '4') { const dataQuery = await api.get("/reports/services/queues", { params: { startDate, endDate }, }) - console.log('DATA QUERY.data queues: ', dataQuery.data) + console.log(' dataQuery?.data?.reportService: ', dataQuery?.data?.reportService) dispatchQ({ type: "RESET" }) dispatchQ({ type: "LOAD_QUERY", payload: dataQuery?.data?.reportService }) @@ -958,48 +956,11 @@ const Report = () => { imagem de perfil do whatsapp }, { title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, }, - - { - title: 'Conversas iniciadas', field: 'startedByAgent', - - // cellStyle: (e, rowData) => { - - // if (rowData['statusOnline'] && rowData['statusOnline'].status) { - - // if (rowData['statusOnline'].status === 'offline') { - - // return { color: "red" } - // } - // else if (rowData['statusOnline'].status === 'online') { - // return { color: "green" } - // } - // else if (rowData['statusOnline'].status === 'logout...') { - // return { color: "orange" } - // } - // else if (rowData['statusOnline'].status === 'waiting...') { - // return { color: "orange" } - // } - // } - - - // }, - - }, - + { title: 'Conversas iniciadas', field: 'startedByAgent', }, { title: 'Conversas recebidas', field: 'startedByClient' }, { title: `Conversas finalizadas`, field: 'closedChat' }, { title: `Tempo médio de espera`, field: 'avgChatWaitingTime' }, @@ -1009,29 +970,6 @@ const Report = () => { } data={dataRows} - // actions={[ - // (rowData) => { - - // if (rowData.statusOnline && - // rowData.statusOnline['status'] && - // rowData.statusOnline['status'] === 'online') { - - - // return { - // icon: LogoutIcon, - // tooltip: 'deslogar', - // disable: false, - // onClick: (event, rowData) => { - // handleLogouOnlineUser(rowData.id) - // } - // } - - - // } - // } - // ]} - - options={ { search: true, @@ -1062,49 +1000,23 @@ const Report = () => { imagem de perfil do whatsapp }, - { title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, }, - { title: 'Fila', field: 'queueName', cellStyle: { whiteSpace: 'nowrap' }, }, { - title: 'Conversas iniciadas', field: 'startedByAgent', - - // cellStyle: (e, rowData) => { - - // if (rowData['statusOnline'] && rowData['statusOnline'].status) { - - // if (rowData['statusOnline'].status === 'offline') { - - // return { color: "red" } - // } - // else if (rowData['statusOnline'].status === 'online') { - // return { color: "green" } - // } - // else if (rowData['statusOnline'].status === 'logout...') { - // return { color: "orange" } - // } - // else if (rowData['statusOnline'].status === 'waiting...') { - // return { color: "orange" } - // } - // } - - - // }, + title: 'Fila', field: 'queueName', + cellStyle: (evt, rowData) => { + return { + whiteSpace: 'nowrap', + backgroundColor: rowData?.queueColor || 'inherit', + color: 'white' + } + } }, - + { title: 'Conversas iniciadas', field: 'startedByAgent', }, { title: 'Conversas recebidas', field: 'startedByClient' }, { title: `Conversas finalizadas`, field: 'closedChat' }, { title: `Tempo médio de espera`, field: 'avgChatWaitingTime' }, @@ -1114,29 +1026,6 @@ const Report = () => { } data={dataRows} - // actions={[ - // (rowData) => { - - // if (rowData.statusOnline && - // rowData.statusOnline['status'] && - // rowData.statusOnline['status'] === 'online') { - - - // return { - // icon: LogoutIcon, - // tooltip: 'deslogar', - // disable: false, - // onClick: (event, rowData) => { - // handleLogouOnlineUser(rowData.id) - // } - // } - - - // } - // } - // ]} - - options={ { search: true, @@ -1159,6 +1048,11 @@ const Report = () => { fontSize: 14, } + // rowStyle: rowData => ({ + // backgroundColor: rowData.queueColor || 'inherit', + // fontSize: 14 + // }) + }} /> From dc5a6945d294c48663e0f8a8067733e008e2b97d Mon Sep 17 00:00:00 2001 From: adriano Date: Wed, 27 Mar 2024 12:02:11 -0300 Subject: [PATCH 02/20] feat: Modify backend and frontend to display waiting time for customer service in report and update report controller queries accordingly --- backend/src/controllers/ReportController.ts | 32 +++++++++- backend/src/controllers/TicketController.ts | 8 ++- ...mn-reference-statusChatEndId-to-tickets.ts | 17 ++++++ backend/src/models/StatusChatEnd.ts | 12 ++-- backend/src/models/Ticket.ts | 5 ++ backend/src/routes/reportRoutes.ts | 44 ++++++++++---- .../ReportByNumberQueueService.ts | 6 +- .../CountStatusChatEndService.ts | 27 +++++++++ .../TicketServices/ShowTicketReport.ts | 60 +++++++++++++++++-- .../TicketServices/UpdateTicketService.ts | 7 ++- frontend/src/pages/Report/index.js | 2 + 11 files changed, 190 insertions(+), 30 deletions(-) create mode 100644 backend/src/database/migrations/20240325193138-change-column-reference-statusChatEndId-to-tickets.ts create mode 100644 backend/src/services/StatusChatEndService/CountStatusChatEndService.ts diff --git a/backend/src/controllers/ReportController.ts b/backend/src/controllers/ReportController.ts index 01664d4..a2956f0 100644 --- a/backend/src/controllers/ReportController.ts +++ b/backend/src/controllers/ReportController.ts @@ -20,6 +20,7 @@ import ShowQueuesByUser from "../services/UserServices/ShowQueuesByUser"; import { getIO } from "../libs/socket"; import { Json } from "sequelize/types/lib/utils"; import ReportByNumberQueueService from "../services/ReportServices/ReportByNumberQueueService"; +import CountStatusChatEndService from "../services/StatusChatEndService/CountStatusChatEndService"; type IndexQuery = { userId: string; @@ -312,7 +313,7 @@ export const reportService = async ( const reportService = await ReportByNumberQueueService({ startDate, endDate - }); + }); return res.status(200).json({ reportService }); }; @@ -331,12 +332,37 @@ export const reportServiceByQueue = async ( const { startDate, endDate, queueId } = req.query as IndexQuery; - const reportService = await ReportByNumberQueueService({ startDate, endDate, queue: true - }); + }); return res.status(200).json({ reportService }); }; + +export const reportTicksCountByStatusChatEnds = async ( + req: Request, + res: Response +): Promise => { + if ( + req.user.profile !== "master" && + req.user.profile !== "admin" && + req.user.profile !== "supervisor" + ) { + throw new AppError("ERR_NO_PERMISSION", 403); + } + + const { startDate, endDate } = req.query as IndexQuery; + + const dateToday = splitDateTime( + new Date(format(new Date(), "yyyy-MM-dd HH:mm:ss", { locale: ptBR })) + ); + + const reportStatusChatEnd = await CountStatusChatEndService( + startDate || dateToday.fullDate, + endDate || dateToday.fullDate + ); + + return res.status(200).json({ reportStatusChatEnd }); +}; diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index 2f62ba3..e8f327c 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -76,6 +76,7 @@ import CreateContactService from "../services/ContactServices/CreateContactServi import { botSendMessage } from "../services/WbotServices/wbotMessageListener"; import WhatsappQueue from "../models/WhatsappQueue"; import { get } from "../helpers/RedisClient" +import CountStatusChatEndService from "../services/StatusChatEndService/CountStatusChatEndService" export const index = async (req: Request, res: Response): Promise => { const { @@ -109,8 +110,8 @@ export const index = async (req: Request, res: Response): Promise => { withUnreadMessages, unlimited, searchParamContent - }); - + }); + return res.status(200).json({ tickets, count, hasMore }); }; @@ -348,7 +349,8 @@ export const update = async ( ticketData: { status: status, userId: userId, - statusChatEnd: statusChatEndName.name + statusChatEnd: statusChatEndName.name, + statusChatEndId: statusChatEndName.id }, ticketId }); diff --git a/backend/src/database/migrations/20240325193138-change-column-reference-statusChatEndId-to-tickets.ts b/backend/src/database/migrations/20240325193138-change-column-reference-statusChatEndId-to-tickets.ts new file mode 100644 index 0000000..9043cea --- /dev/null +++ b/backend/src/database/migrations/20240325193138-change-column-reference-statusChatEndId-to-tickets.ts @@ -0,0 +1,17 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Tickets", "statusChatEndId", { + type: DataTypes.INTEGER, + references: { model: "StatusChatEnds", key: "id" }, + onUpdate: "CASCADE", + onDelete: "SET NULL", + allowNull: true + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Tickets", "statusChatEndId"); + } +}; diff --git a/backend/src/models/StatusChatEnd.ts b/backend/src/models/StatusChatEnd.ts index dde5775..fc4505f 100644 --- a/backend/src/models/StatusChatEnd.ts +++ b/backend/src/models/StatusChatEnd.ts @@ -10,25 +10,29 @@ import { } from "sequelize-typescript"; import SchedulingNotify from "./SchedulingNotify"; +import Ticket from "./Ticket" @Table class StatusChatEnd extends Model { @PrimaryKey @AutoIncrement @Column - id: number; - + id: number; + @Column name: string; - + @CreatedAt createdAt: Date; - + @UpdatedAt updatedAt: Date; @HasMany(() => SchedulingNotify) SchedulingNotifies: SchedulingNotify[]; + + @HasMany(() => Ticket) + tickets: Ticket[]; } export default StatusChatEnd; diff --git a/backend/src/models/Ticket.ts b/backend/src/models/Ticket.ts index d3b95c9..42175c8 100644 --- a/backend/src/models/Ticket.ts +++ b/backend/src/models/Ticket.ts @@ -21,6 +21,7 @@ import User from "./User"; import Whatsapp from "./Whatsapp"; import SchedulingNotify from "./SchedulingNotify"; +import StatusChatEnd from "./StatusChatEnd" @Table class Ticket extends Model { @@ -42,6 +43,10 @@ class Ticket extends Model { @Column isGroup: boolean; + @ForeignKey(() => StatusChatEnd) + @Column + statusChatEndId: number; + @Column statusChatEnd: string; diff --git a/backend/src/routes/reportRoutes.ts b/backend/src/routes/reportRoutes.ts index d6228ef..ecaf41a 100644 --- a/backend/src/routes/reportRoutes.ts +++ b/backend/src/routes/reportRoutes.ts @@ -1,26 +1,48 @@ //relatorio -import express from "express"; +import express from "express"; -import isAuth from "../middleware/isAuth"; - -import * as ReportController from "../controllers/ReportController"; +import isAuth from "../middleware/isAuth"; -const reportRoutes = express.Router(); +import * as ReportController from "../controllers/ReportController"; -reportRoutes.get("/reports", isAuth, ReportController.reportUserByDateStartDateEnd); +const reportRoutes = express.Router(); -reportRoutes.post("/reports/onqueue", ReportController.reportOnQueue); +reportRoutes.get( + "/reports", + isAuth, + ReportController.reportUserByDateStartDateEnd +); -reportRoutes.get("/reports/user/services", isAuth, ReportController.reportUserService); +reportRoutes.post("/reports/onqueue", ReportController.reportOnQueue); + +reportRoutes.get( + "/reports/user/services", + isAuth, + ReportController.reportUserService +); reportRoutes.get( "/reports/services/numbers", isAuth, ReportController.reportService -); +); -reportRoutes.get("/reports/services/queues", isAuth, ReportController.reportServiceByQueue); +reportRoutes.get( + "/reports/services/queues", + isAuth, + ReportController.reportServiceByQueue +); -reportRoutes.get("/reports/messages", isAuth, ReportController.reportMessagesUserByDateStartDateEnd); +reportRoutes.get( + "/reports/messages", + isAuth, + ReportController.reportMessagesUserByDateStartDateEnd +); + +reportRoutes.get( + "/reports/count/statusChatEnd", + isAuth, + ReportController.reportTicksCountByStatusChatEnds +); export default reportRoutes; diff --git a/backend/src/services/ReportServices/ReportByNumberQueueService.ts b/backend/src/services/ReportServices/ReportByNumberQueueService.ts index 32cd1f4..02c07dd 100644 --- a/backend/src/services/ReportServices/ReportByNumberQueueService.ts +++ b/backend/src/services/ReportServices/ReportByNumberQueueService.ts @@ -113,7 +113,8 @@ const ReportByNumberQueueService = async ({ AND m.fromMe = 0 -- AND q.id = 2 AND w.number = ${number} - AND (t.status = 'open' OR t.status = 'closed') + AND t.status IN ('open', 'closed') + HAVING WAITING_TIME IS NOT NULL ORDER BY WAITING_TIME;`, { type: QueryTypes.SELECT } @@ -237,7 +238,8 @@ const ReportByNumberQueueService = async ({ AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) AND m.fromMe = 0 AND q.id = ${q.id} - AND (t.status = 'open' OR t.status = 'closed') + AND t.status IN ('open', 'closed') + HAVING WAITING_TIME IS NOT NULL ORDER BY WAITING_TIME;`, { type: QueryTypes.SELECT } diff --git a/backend/src/services/StatusChatEndService/CountStatusChatEndService.ts b/backend/src/services/StatusChatEndService/CountStatusChatEndService.ts new file mode 100644 index 0000000..bffbb5d --- /dev/null +++ b/backend/src/services/StatusChatEndService/CountStatusChatEndService.ts @@ -0,0 +1,27 @@ +import StatusChatEnd from "../../models/StatusChatEnd"; +import AppError from "../../errors/AppError"; + +import { Sequelize } from "sequelize"; +import { splitDateTime } from "../../helpers/SplitDateTime" +import ptBR from "date-fns/locale/pt-BR"; +import { format } from "date-fns" +const dbConfig = require("../../config/database"); +const sequelize = new Sequelize(dbConfig); +const { QueryTypes } = require("sequelize"); + +const CountStatusChatEndService = async ( + startDate: string, + endDate: string +) => { + + const countStatusChatEnd: any = await sequelize.query( + `select t.id, s.name, count(t.id) as count from Tickets t join StatusChatEnds s on +t.statusChatEndId = s.id and DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' +group by s.id;`, + { type: QueryTypes.SELECT } + ); + + return countStatusChatEnd; +}; + +export default CountStatusChatEndService; diff --git a/backend/src/services/TicketServices/ShowTicketReport.ts b/backend/src/services/TicketServices/ShowTicketReport.ts index 8e75349..f26919a 100644 --- a/backend/src/services/TicketServices/ShowTicketReport.ts +++ b/backend/src/services/TicketServices/ShowTicketReport.ts @@ -7,7 +7,7 @@ import Queue from "../../models/Queue"; import Message from "../../models/Message"; import { userInfo } from "os"; -import { Op, QueryTypes, where } from "sequelize"; +import { Op, QueryTypes, json, where } from "sequelize"; import { Sequelize } from "sequelize"; import moment from "moment"; @@ -17,7 +17,7 @@ const sequelize = new Sequelize(dbConfig); import { startOfDay, endOfDay, parseISO, getDate } from "date-fns"; import { string } from "yup/lib/locale"; import Whatsapp from "../../models/Whatsapp"; -import Query from "mysql2/typings/mysql/lib/protocol/sequences/Query" +import Query from "mysql2/typings/mysql/lib/protocol/sequences/Query"; interface Request { userId: string | number; @@ -73,9 +73,7 @@ const ShowTicketReport = async ({ { type: QueryTypes.SELECT } ); - console.log('QUERY: ', query) - - const { count, rows: tickets } = await Ticket.findAndCountAll({ + let { count, rows: tickets }: any = await Ticket.findAndCountAll({ where: { id: { [Op.in]: _ticketsId.map((t: any) => t.id) } }, @@ -153,6 +151,58 @@ const ShowTicketReport = async ({ throw new AppError("ERR_NO_TICKET_FOUND", 404); } + const ticketIds = tickets.map((t: any) => t.id); + + if (ticketIds.length > 0) { + const waiting_time: any = await sequelize.query( + `SELECT t.id as ticketId, t.status, TIME_FORMAT( + SEC_TO_TIME( + TIMESTAMPDIFF( + SECOND, + ( + SELECT createdAt + FROM Messages + WHERE ticketId = m.ticketId + AND fromMe = 0 + ORDER BY createdAt ASC + LIMIT 1 + ), + ( + SELECT createdAt + FROM Messages + WHERE ticketId = m.ticketId + AND fromAgent = 1 + ORDER BY createdAt ASC + LIMIT 1 + ) + ) + ), '%H:%i:%s') AS WAITING_TIME + FROM Tickets t + JOIN Messages m ON t.id = m.ticketId + JOIN Whatsapps w ON t.whatsappId = w.id + JOIN Queues q ON q.id = t.queueId + WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + AND t.id IN (${ticketIds.join()}) + AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) + AND m.fromMe = 0 + AND t.status IN ('open', 'closed') + HAVING WAITING_TIME IS NOT NULL + ORDER BY + WAITING_TIME;`, + { type: QueryTypes.SELECT } + ); + + for (let w of waiting_time) { + const { ticketId, status, WAITING_TIME } = w; + + const index = tickets.findIndex((t: any) => +t?.id == +ticketId); + + if (index != -1) { + tickets[index].dataValues.waiting_time = WAITING_TIME; + } + } + } + return { tickets, count, hasMore }; }; diff --git a/backend/src/services/TicketServices/UpdateTicketService.ts b/backend/src/services/TicketServices/UpdateTicketService.ts index 1c3b10a..943015b 100644 --- a/backend/src/services/TicketServices/UpdateTicketService.ts +++ b/backend/src/services/TicketServices/UpdateTicketService.ts @@ -10,7 +10,7 @@ import { createOrUpdateTicketCache } from "../../helpers/TicketCache"; import AppError from "../../errors/AppError"; import sendWhatsAppMessageSocket from "../../helpers/SendWhatsappMessageSocket"; import BotIsOnQueue from "../../helpers/BotIsOnQueue"; -import { deleteObject } from "../../helpers/RedisClient" +import { deleteObject } from "../../helpers/RedisClient"; var flatten = require("flat"); interface TicketData { @@ -18,6 +18,7 @@ interface TicketData { userId?: number; queueId?: number; statusChatEnd?: string; + statusChatEndId?: number; unreadMessages?: number; whatsappId?: string | number; } @@ -46,6 +47,7 @@ const UpdateTicketService = async ({ queueId, statusChatEnd, unreadMessages, + statusChatEndId, whatsappId } = ticketData; @@ -66,13 +68,14 @@ const UpdateTicketService = async ({ if (oldStatus === "closed") { await CheckContactOpenTickets(ticket.contact.id, ticket.whatsappId); } - + await ticket.update({ status, queueId, userId, unreadMessages, statusChatEnd, + statusChatEndId, whatsappId }); diff --git a/frontend/src/pages/Report/index.js b/frontend/src/pages/Report/index.js index 055f95e..d3b97fa 100644 --- a/frontend/src/pages/Report/index.js +++ b/frontend/src/pages/Report/index.js @@ -235,6 +235,7 @@ let columnsData = [ { title: `${i18n.t("reports.listColumns.column1_7")}`, field: 'createdAt' }, { title: `${i18n.t("reports.listColumns.column1_8")}`, field: 'updatedAt' }, { title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' }, + { title: `Espera`, field: 'waiting_time' }, { title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true }, ] @@ -249,6 +250,7 @@ let columnsDataSuper = [ { title: `${i18n.t("reports.listColumns.column1_7")}`, field: 'createdAt' }, { title: `${i18n.t("reports.listColumns.column1_8")}`, field: 'updatedAt' }, { title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' }, + { title: `Espera`, field: 'waiting_time' }, { title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true }, ] From 1a7077feaf5dd06b7f2ade1d8ee33d6c61b1cdce Mon Sep 17 00:00:00 2001 From: willian-pessoa Date: Wed, 27 Mar 2024 17:52:20 -0300 Subject: [PATCH 03/20] feat: grafico de pizza --- frontend/src/pages/Dashboard/PieChart.js | 127 +++++++++++++++++++++++ frontend/src/pages/Dashboard/index.js | 32 +++--- 2 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 frontend/src/pages/Dashboard/PieChart.js diff --git a/frontend/src/pages/Dashboard/PieChart.js b/frontend/src/pages/Dashboard/PieChart.js new file mode 100644 index 0000000..c250ac3 --- /dev/null +++ b/frontend/src/pages/Dashboard/PieChart.js @@ -0,0 +1,127 @@ +import { Box } from '@material-ui/core'; +import React from 'react'; +import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; +import { PieChart as RechartsPieChart, Pie, Sector, Cell, ResponsiveContainer } from 'recharts'; + +import Title from './Title'; + +const dataExample = [ + { + "id": 3366, + "name": "FINALIZADO", + "count": 5 + }, + { + "id": 3369, + "name": "LEMBRETE", + "count": 1 + }, + { + "id": 3367, + "name": "EXEMPLO", + "count": 3 + }, + { + "id": 3364, + "name": "EXEMPLO 2", + "count": 3 + }, + { + "id": 3364, + "name": "EXEMPLO 3", + "count": 6 + }, +] + +const COLORS = [ + '#0088FE', // Azul escuro + '#00C49F', // Verde menta + '#FFBB28', // Laranja escuro + '#FF8042', // Vermelho escuro + '#9D38BD', // Roxo escuro + '#FFD166', // Laranja claro + '#331F00', // Marrom escuro + '#C0FFC0', // Verde Claro + '#C4E538', // Verde-amarelo vibrante + '#A2A2A2', // Cinza claro +];; + +const RADIAN = Math.PI / 180; + +const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, count }) => { + const radius = innerRadius + (outerRadius - innerRadius) * 0.75; + const x = cx + radius * Math.cos(-midAngle * RADIAN); + const y = cy + radius * Math.sin(-midAngle * RADIAN); + + return ( + cx ? 'start' : 'end'} dominantBaseline="central"> + {count} + + ); +}; + +/** + * @param data array de objetos no formato + * { + "id": number | string, + "name": string, + "count": number + * } + */ +const PieChart = ({ data = dataExample }) => { + return ( + + + Tickets Status + + + {data.map((entry, index) => { + return ( + + + {entry.name} + + ) + })} + + + + + + {data.map((entry, index) => ( + + ))} + + + + + + ); + +} + +export default PieChart \ No newline at end of file diff --git a/frontend/src/pages/Dashboard/index.js b/frontend/src/pages/Dashboard/index.js index 83d26da..ccbb674 100644 --- a/frontend/src/pages/Dashboard/index.js +++ b/frontend/src/pages/Dashboard/index.js @@ -15,6 +15,7 @@ import Info from "@material-ui/icons/Info" import { AuthContext } from "../../context/Auth/AuthContext" // import { i18n } from "../../translate/i18n"; import Chart from "./Chart" +import PieChart from "./PieChart" import openSocket from "socket.io-client" import api from "../../services/api" @@ -31,7 +32,7 @@ const useStyles = makeStyles((theme) => ({ display: "flex", overflow: "auto", flexDirection: "column", - height: 240, + height: 280, }, customFixedHeightPaper: { padding: theme.spacing(2), @@ -108,7 +109,7 @@ const useStyles = makeStyles((theme) => ({ var _fifo -const sumOnlineTimeNow = (oldOnlineTimeSum) => { +const sumOnlineTimeNow = (oldOnlineTimeSum) => { let onlineTime = new Date() @@ -138,7 +139,7 @@ const sumOnlineTimeNow = (oldOnlineTimeSum) => { const isoDate = new Date(onlineTime) - const newOnlinetime = isoDate.toJSON().slice(0, 19).replace('T', ' ') + const newOnlinetime = isoDate.toJSON().slice(0, 19).replace('T', ' ') return newOnlinetime } @@ -207,9 +208,9 @@ const reducer = (state, action) => { if ("onlineTime" in onlineUser) { - if ("sumOnlineTime" in state[index]) { + if ("sumOnlineTime" in state[index]) { - state[index].sumOnlineTime.sum = onlineUser.onlineTime.split(" ")[1] + state[index].sumOnlineTime.sum = onlineUser.onlineTime.split(" ")[1] } else if (!("sumOnlineTime" in state[index])) { @@ -283,7 +284,7 @@ const Dashboard = () => { try { let date = new Date().toLocaleDateString("pt-BR").split("/") let dateToday = `${date[2]}-${date[1]}-${date[0]}` - + const { data } = await api.get("/reports/user/services", { params: { userId: null, startDate: dateToday, endDate: dateToday }, }) @@ -319,7 +320,7 @@ const Dashboard = () => { if (usersOnlineInfo[i].statusOnline && usersOnlineInfo[i].statusOnline.status === 'online') { let onlineTimeCurrent = sumOnlineTimeNow({ onlineTime: usersOnlineInfo[i].statusOnline.onlineTime, updatedAt: usersOnlineInfo[i].statusOnline.updatedAt }) - + dispatch({ type: "UPDATE_STATUS_ONLINE", payload: { userId: usersOnlineInfo[i].id, status: usersOnlineInfo[i].statusOnline.status, onlineTime: onlineTimeCurrent } }) } @@ -356,7 +357,7 @@ const Dashboard = () => { }) socket.on("onlineStatus", (data) => { - if (data.action === "logout" || data.action === "update") { + if (data.action === "logout" || data.action === "update") { dispatch({ type: "UPDATE_STATUS_ONLINE", payload: data.userOnlineTime }) } else if (data.action === "delete") { @@ -497,10 +498,17 @@ const Dashboard = () => { - - - - + + + + + + + + + + + From c27770ef0233ccbf4db84f4de93a55debeacc3aa Mon Sep 17 00:00:00 2001 From: adriano Date: Thu, 28 Mar 2024 12:20:55 -0300 Subject: [PATCH 04/20] fix: Improve ticket query in backend to resolve high delay issue --- backend/src/controllers/ReportController.ts | 4 +- .../TicketServices/ShowTicketReport.ts | 110 ++++++++++-------- frontend/src/pages/Report/index.js | 2 +- 3 files changed, 63 insertions(+), 53 deletions(-) diff --git a/backend/src/controllers/ReportController.ts b/backend/src/controllers/ReportController.ts index a2956f0..d7d70b3 100644 --- a/backend/src/controllers/ReportController.ts +++ b/backend/src/controllers/ReportController.ts @@ -262,9 +262,7 @@ export const reportMessagesUserByDateStartDateEnd = async ( data_query_messages[i].fromMe = "Cliente"; } - data_query_messages[i].id = i + 1; - - console.log("data_query_messages: ", data_query_messages[i]); + data_query_messages[i].id = i + 1; } return res.status(200).json(data_query_messages); diff --git a/backend/src/services/TicketServices/ShowTicketReport.ts b/backend/src/services/TicketServices/ShowTicketReport.ts index f26919a..1c4b1ea 100644 --- a/backend/src/services/TicketServices/ShowTicketReport.ts +++ b/backend/src/services/TicketServices/ShowTicketReport.ts @@ -18,6 +18,7 @@ import { startOfDay, endOfDay, parseISO, getDate } from "date-fns"; import { string } from "yup/lib/locale"; import Whatsapp from "../../models/Whatsapp"; import Query from "mysql2/typings/mysql/lib/protocol/sequences/Query"; +import { te } from "date-fns/locale"; interface Request { userId: string | number; @@ -43,43 +44,56 @@ const ShowTicketReport = async ({ createdOrUpdated = "created", queueId }: Request): Promise => { - let where_clause: any = {}; - let query = ""; + // let where_clause: any = {}; + // let query = ""; - if (userId !== "0") { - where_clause.userid = userId; - query = `AND t.userId = ${userId}`; - } + // if (userId !== "0") { + // where_clause.userid = userId; + // query = `AND t.userId = ${userId}`; + // } + + // if (queueId) { + // where_clause.queueId = queueId; + // query = `AND t.queueId = ${queueId}`; + // } + + const createdAtOrUpdatedAt = + createdOrUpdated == "created" ? "createdAt" : "updatedAt"; + + let where_clause = {}; if (queueId) { - where_clause.queueId = queueId; - query = `AND t.queueId = ${queueId}`; + where_clause = { + queueId: queueId, + [createdAtOrUpdatedAt]: { + [Op.gte]: startDate + " 00:00:00.000000", + [Op.lte]: endDate + " 23:59:59.999999" + } + }; + } else if (userId == "0") { + where_clause = { + [createdAtOrUpdatedAt]: { + [Op.gte]: startDate + " 00:00:00.000000", + [Op.lte]: endDate + " 23:59:59.999999" + } + }; + } else if (userId != "0") { + where_clause = { + userid: userId, + [createdAtOrUpdatedAt]: { + [Op.gte]: startDate + " 00:00:00.000000", + [Op.lte]: endDate + " 23:59:59.999999" + } + }; } const limit = 40; const offset = limit * (+pageNumber - 1); - const createdAtOrUpdatedAt = - createdOrUpdated == "created" ? "createdAt" : "updatedAt"; - - const _ticketsId = await sequelize.query( - `SELECT t.id - FROM Tickets t - INNER JOIN ( - SELECT DISTINCT ticketId - FROM Messages - WHERE ${createdAtOrUpdatedAt} >= '${startDate} 00:00:00' AND ${createdAtOrUpdatedAt} <= '${endDate} 23:59:59' - ) AS m ON m.ticketId = t.id ${query};`, - { type: QueryTypes.SELECT } - ); - let { count, rows: tickets }: any = await Ticket.findAndCountAll({ - where: { - id: { [Op.in]: _ticketsId.map((t: any) => t.id) } - }, + where: where_clause, limit, offset, - attributes: [ "id", "status", @@ -151,43 +165,41 @@ const ShowTicketReport = async ({ throw new AppError("ERR_NO_TICKET_FOUND", 404); } - const ticketIds = tickets.map((t: any) => t.id); - - if (ticketIds.length > 0) { + if (tickets.length > 0) { const waiting_time: any = await sequelize.query( `SELECT t.id as ticketId, t.status, TIME_FORMAT( - SEC_TO_TIME( + SEC_TO_TIME( TIMESTAMPDIFF( SECOND, ( - SELECT createdAt - FROM Messages - WHERE ticketId = m.ticketId - AND fromMe = 0 - ORDER BY createdAt ASC + SELECT createdAt + FROM Messages + WHERE ticketId = m.ticketId + AND fromMe = 0 + ORDER BY createdAt ASC LIMIT 1 - ), + ), ( - SELECT createdAt - FROM Messages - WHERE ticketId = m.ticketId - AND fromAgent = 1 - ORDER BY createdAt ASC + SELECT createdAt + FROM Messages + WHERE ticketId = m.ticketId + AND fromAgent = 1 + ORDER BY createdAt ASC LIMIT 1 ) - ) - ), '%H:%i:%s') AS WAITING_TIME - FROM Tickets t + ) + ), '%H:%i:%s') AS WAITING_TIME + FROM Tickets t JOIN Messages m ON t.id = m.ticketId JOIN Whatsapps w ON t.whatsappId = w.id JOIN Queues q ON q.id = t.queueId - WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' - AND t.id IN (${ticketIds.join()}) - AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) - AND m.fromMe = 0 + WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' + AND t.id IN (${tickets.map((t: any) => t.id).join()}) + AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) + AND m.fromMe = 0 AND t.status IN ('open', 'closed') HAVING WAITING_TIME IS NOT NULL - ORDER BY + ORDER BY WAITING_TIME;`, { type: QueryTypes.SELECT } ); diff --git a/frontend/src/pages/Report/index.js b/frontend/src/pages/Report/index.js index d3b97fa..1724682 100644 --- a/frontend/src/pages/Report/index.js +++ b/frontend/src/pages/Report/index.js @@ -363,7 +363,7 @@ const Report = () => { if (reportOption === '1') { const { data } = await api.get("/reports/", { params: { userId, startDate, endDate, pageNumber: pageNumberTickets, createdOrUpdated: selectedValue, queueId }, userQueues: userA.queues }) - + let ticketsQueue = data.tickets let userQueues = userA.queues let filterQueuesTickets = [] From 76929c41ec4b31efb31ab1fec62ab604a87b5504 Mon Sep 17 00:00:00 2001 From: adriano Date: Thu, 28 Mar 2024 14:12:30 -0300 Subject: [PATCH 05/20] feat: PieChart data ploted from backend --- frontend/src/pages/Dashboard/PieChart.js | 2 +- frontend/src/pages/Dashboard/index.js | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/Dashboard/PieChart.js b/frontend/src/pages/Dashboard/PieChart.js index c250ac3..87aa568 100644 --- a/frontend/src/pages/Dashboard/PieChart.js +++ b/frontend/src/pages/Dashboard/PieChart.js @@ -72,7 +72,7 @@ const PieChart = ({ data = dataExample }) => { return ( - Tickets Status + Tickets status de encerramento { const [usersOnlineInfo, dispatch] = useReducer(reducer, []) const [ticketStatusChange, setStatus] = useState() const [ticketsStatus, setTicktsStatus] = useState({ open: 0, openAll: 0, pending: 0, closed: 0 }) - + const [ticketStatusChatEnd, setTicketStatusChatEnd] = useState([]) const { user } = useContext(AuthContext) useEffect(() => { @@ -287,12 +287,17 @@ const Dashboard = () => { const { data } = await api.get("/reports/user/services", { params: { userId: null, startDate: dateToday, endDate: dateToday }, - }) - - // console.log('data.data: ', data.usersProfile) + }) dispatch({ type: "RESET" }) dispatch({ type: "LOAD_QUERY", payload: data.usersProfile }) + + const { data: ticketStatusChatEndData } = await api.get("/reports/count/statusChatEnd", { + params: { startDate: '2024-03-21', endDate: '2024-03-28' }, + }) + + setTicketStatusChatEnd(ticketStatusChatEndData.reportStatusChatEnd) + } catch (err) { } @@ -506,7 +511,7 @@ const Dashboard = () => { - + From a96d6f26c72a0781c5ca47336cea9dfcab597890 Mon Sep 17 00:00:00 2001 From: adriano Date: Thu, 28 Mar 2024 14:28:34 -0300 Subject: [PATCH 06/20] chore: update Piechart tickets name --- frontend/src/pages/Dashboard/PieChart.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/Dashboard/PieChart.js b/frontend/src/pages/Dashboard/PieChart.js index 87aa568..7a69fd7 100644 --- a/frontend/src/pages/Dashboard/PieChart.js +++ b/frontend/src/pages/Dashboard/PieChart.js @@ -72,7 +72,7 @@ const PieChart = ({ data = dataExample }) => { return ( - Tickets status de encerramento + Tickets encerramento Date: Mon, 1 Apr 2024 09:00:15 -0300 Subject: [PATCH 07/20] git commit -m "feat: Pull information from closed tickets in backend to feed pie chart in frontend and add option to change service hours on Saturday" --- backend/src/controllers/SettingController.ts | 15 +++- ...d-setting-ticket-saturday-business-time.ts | 25 ++++++ backend/src/helpers/TicketConfig.ts | 84 +++++++++++++++++-- .../WbotServices/wbotMessageListener.ts | 8 ++ frontend/src/components/ConfigModal/index.js | 83 +++++++++++++++++- frontend/src/pages/Dashboard/index.js | 2 +- 6 files changed, 205 insertions(+), 12 deletions(-) create mode 100644 backend/src/database/seeds/20240328181254-add-setting-ticket-saturday-business-time.ts diff --git a/backend/src/controllers/SettingController.ts b/backend/src/controllers/SettingController.ts index d131ceb..e1d0e46 100644 --- a/backend/src/controllers/SettingController.ts +++ b/backend/src/controllers/SettingController.ts @@ -20,14 +20,14 @@ export const index = async (req: Request, res: Response): Promise => { // const config = await SettingTicket.findAll(); - return res.status(200).json({ settings, }); + return res.status(200).json({ settings }); }; export const ticketSettings = async ( req: Request, res: Response ): Promise => { - const { number } = req.params; + const { number } = req.params; const config = await SettingTicket.findAll({ where: { number } }); @@ -40,6 +40,7 @@ export const updateTicketSettings = async ( ): Promise => { const { number, + saturdayBusinessTime, outBusinessHours, ticketExpiration, weekend, @@ -47,7 +48,7 @@ export const updateTicketSettings = async ( sunday, holiday } = req.body; - + if (!number) throw new AppError("No number selected", 400); if (outBusinessHours && Object.keys(outBusinessHours).length > 0) { @@ -58,6 +59,14 @@ export const updateTicketSettings = async ( }); } + if (saturdayBusinessTime && Object.keys(saturdayBusinessTime).length > 0) { + await updateSettingTicket({ + ...saturdayBusinessTime, + key: "saturdayBusinessTime", + number + }); + } + if (ticketExpiration && Object.keys(ticketExpiration).length > 0) { await updateSettingTicket({ ...ticketExpiration, diff --git a/backend/src/database/seeds/20240328181254-add-setting-ticket-saturday-business-time.ts b/backend/src/database/seeds/20240328181254-add-setting-ticket-saturday-business-time.ts new file mode 100644 index 0000000..60c411d --- /dev/null +++ b/backend/src/database/seeds/20240328181254-add-setting-ticket-saturday-business-time.ts @@ -0,0 +1,25 @@ +import { QueryInterface } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.bulkInsert( + "SettingTickets", + [ + { + message: "", + startTime: new Date(), + endTime: new Date(), + value: "disabled", + key: "saturdayBusinessTime", + createdAt: new Date(), + updatedAt: new Date() + } + ], + {} + ); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.bulkDelete("SettingTickets", {}); + } +}; diff --git a/backend/src/helpers/TicketConfig.ts b/backend/src/helpers/TicketConfig.ts index 0f7416e..7a98f40 100644 --- a/backend/src/helpers/TicketConfig.ts +++ b/backend/src/helpers/TicketConfig.ts @@ -39,7 +39,7 @@ const isHoliday = async (number: string | number) => { locale: ptBR }) ) - ); + ); if (currentDate.fullDate == startTime.fullDate) { obj.set = true; @@ -173,8 +173,80 @@ async function isOutBusinessTime(number: string | number) { return obj; } -export { - isWeekend, - isHoliday, - isOutBusinessTime -}; +async function isOutBusinessTimeSaturday(number: string | number) { + let obj = { set: false, msg: "" }; + + const outBusinessHoursSaturday = await SettingTicket.findOne({ + where: { key: "saturdayBusinessTime", number } + }); + + let isWithinRange = false; + + if ( + true && + outBusinessHoursSaturday + // outBusinessHoursSaturday && + // outBusinessHoursSaturday.value == "enabled" && + // outBusinessHoursSaturday?.message?.trim()?.length > 0 + ) { + const ticketDateTimeUpdate = splitDateTime( + new Date( + _format(new Date(), "yyyy-MM-dd HH:mm:ss", { + locale: ptBR + }) + ) + ); + + const startTime = splitDateTime( + new Date( + _format( + new Date(outBusinessHoursSaturday.startTime), + "yyyy-MM-dd HH:mm:ss", + { + locale: ptBR + } + ) + ) + ); + + const endTime = splitDateTime( + new Date( + _format( + new Date(outBusinessHoursSaturday.endTime), + "yyyy-MM-dd HH:mm:ss", + { + locale: ptBR + } + ) + ) + ); + + const format = "HH:mm:ss"; + const parsedStartTime = parse( + ticketDateTimeUpdate.fullTime, + format, + new Date() + ); + const parsedEndTime = parse(startTime.fullTime, format, new Date()); + const parsedTimeToCheck = parse(endTime.fullTime, format, new Date()); + const timeInterval = { start: parsedStartTime, end: parsedEndTime }; + + // If the time range spans across different days, handle the date part + if (parsedEndTime < parsedStartTime) { + const nextDay = new Date(parsedStartTime); + nextDay.setDate(nextDay.getDate() + 1); + timeInterval.end = nextDay; + } + + isWithinRange = isWithinInterval(parsedTimeToCheck, timeInterval); + + if (!isWithinRange) { + obj.set = true; + obj.msg = outBusinessHoursSaturday.message; + } + } + + return obj; +} + +export { isWeekend, isHoliday, isOutBusinessTime, isOutBusinessTimeSaturday }; diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 2739c73..ffa650e 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -10,6 +10,7 @@ import path from "path"; import { isHoliday, isOutBusinessTime, + isOutBusinessTimeSaturday, isWeekend } from "../../helpers/TicketConfig"; @@ -1221,6 +1222,13 @@ const outOfService = async (number: string) => { objs.push({ type: "holiday", msg: holiday.msg }); } + // MESSAGE TO SATURDAY BUSINESS TIME + const businessTimeSaturday = await isOutBusinessTimeSaturday(number); + + if (businessTimeSaturday && businessTimeSaturday.set) { + objs.push({ type: "saturdayBusinessTime", msg: businessTimeSaturday.msg }); + } + // MESSAGES TO SATURDAY OR SUNDAY const weekend: any = await isWeekend(number); diff --git a/frontend/src/components/ConfigModal/index.js b/frontend/src/components/ConfigModal/index.js index 40099ea..329c75b 100644 --- a/frontend/src/components/ConfigModal/index.js +++ b/frontend/src/components/ConfigModal/index.js @@ -77,8 +77,16 @@ const ConfigModal = ({ open, onClose, change }) => { const initialState = { startTimeBus: new Date(), endTimeBus: new Date(), + + startTimeBusSaturday: new Date(), + endTimeBusSaturday: new Date(), + messageBus: '', + messageBusSaturday: '', + businessTimeEnable: false, + businessTimeEnableSaturday: false, + ticketTimeExpiration: new Date(), ticketExpirationMsg: '', ticketExpirationEnable: false, @@ -115,13 +123,16 @@ const ConfigModal = ({ open, onClose, change }) => { if (!selectedNumber) return const { data } = await api.get(`/settings/ticket/${selectedNumber}`) - + if (data?.config && data.config.length === 0) { setConfig(initialState) return } const outBusinessHours = data.config.find((c) => c.key === "outBusinessHours") + + const saturdayBusinessTime = data.config.find((c) => c.key === "saturdayBusinessTime") + const ticketExpiration = data.config.find((c) => c.key === "ticketExpiration") const saturday = data.config.find((c) => c.key === "saturday") const sunday = data.config.find((c) => c.key === "sunday") @@ -134,6 +145,11 @@ const ConfigModal = ({ open, onClose, change }) => { messageBus: outBusinessHours.message, businessTimeEnable: outBusinessHours.value === 'enabled' ? true : false, + startTimeBusSaturday: saturdayBusinessTime.startTime, + endTimeBusSaturday: saturdayBusinessTime.endTime, + messageBusSaturday: saturdayBusinessTime.message, + businessTimeEnableSaturday: saturdayBusinessTime.value === 'enabled' ? true : false, + ticketTimeExpiration: ticketExpiration.startTime, ticketExpirationMsg: ticketExpiration.message, ticketExpirationEnable: ticketExpiration.value === 'enabled' ? true : false, @@ -165,6 +181,14 @@ const ConfigModal = ({ open, onClose, change }) => { message: values.messageBus, value: values.businessTimeEnable ? 'enabled' : 'disabled' }, + + saturdayBusinessTime: { + startTime: values.startTimeBusSaturday, + endTime: values.endTimeBusSaturday, + message: values.messageBusSaturday, + value: values.businessTimeEnableSaturday ? 'enabled' : 'disabled' + }, + ticketExpiration: { startTime: values.ticketTimeExpiration, message: values.ticketExpirationMsg, @@ -205,7 +229,7 @@ const ConfigModal = ({ open, onClose, change }) => { onClose() // setConfig(initialState) } - + return (
{
+
+ {/* SABADO INICIO */} +
+ + {' '} + + + + } + label={'Ativar/Desativar'} /> +
+ +
+ +
+ {/* SABADO FIM */}
diff --git a/frontend/src/pages/Dashboard/index.js b/frontend/src/pages/Dashboard/index.js index e56d268..730aee8 100644 --- a/frontend/src/pages/Dashboard/index.js +++ b/frontend/src/pages/Dashboard/index.js @@ -293,7 +293,7 @@ const Dashboard = () => { dispatch({ type: "LOAD_QUERY", payload: data.usersProfile }) const { data: ticketStatusChatEndData } = await api.get("/reports/count/statusChatEnd", { - params: { startDate: '2024-03-21', endDate: '2024-03-28' }, + params: { startDate: startDate, endDate: endDate }, }) setTicketStatusChatEnd(ticketStatusChatEndData.reportStatusChatEnd) From e6ea004edfc15679d5f42f257a60c9e3a5407be1 Mon Sep 17 00:00:00 2001 From: adriano Date: Mon, 1 Apr 2024 09:07:49 -0300 Subject: [PATCH 08/20] fix: Piechart bug startDate endDate --- frontend/src/pages/Dashboard/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/pages/Dashboard/index.js b/frontend/src/pages/Dashboard/index.js index 730aee8..9f2416b 100644 --- a/frontend/src/pages/Dashboard/index.js +++ b/frontend/src/pages/Dashboard/index.js @@ -292,9 +292,7 @@ const Dashboard = () => { dispatch({ type: "RESET" }) dispatch({ type: "LOAD_QUERY", payload: data.usersProfile }) - const { data: ticketStatusChatEndData } = await api.get("/reports/count/statusChatEnd", { - params: { startDate: startDate, endDate: endDate }, - }) + const { data: ticketStatusChatEndData } = await api.get("/reports/count/statusChatEnd") setTicketStatusChatEnd(ticketStatusChatEndData.reportStatusChatEnd) From ce7385602f0259d9f42e4fa92f575fe6956b4fd3 Mon Sep 17 00:00:00 2001 From: adriano Date: Mon, 1 Apr 2024 09:09:59 -0300 Subject: [PATCH 09/20] Feet: Get time from Frontend in Piechart --- frontend/src/pages/Dashboard/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/Dashboard/index.js b/frontend/src/pages/Dashboard/index.js index 9f2416b..ca6fe4e 100644 --- a/frontend/src/pages/Dashboard/index.js +++ b/frontend/src/pages/Dashboard/index.js @@ -292,7 +292,9 @@ const Dashboard = () => { dispatch({ type: "RESET" }) dispatch({ type: "LOAD_QUERY", payload: data.usersProfile }) - const { data: ticketStatusChatEndData } = await api.get("/reports/count/statusChatEnd") + const { data: ticketStatusChatEndData } = await api.get("/reports/count/statusChatEnd", { + params: { startDate: dateToday, endDate: dateToday }, + }) setTicketStatusChatEnd(ticketStatusChatEndData.reportStatusChatEnd) From 90e9e311c3c039fb826ec3c124018f33532cfa23 Mon Sep 17 00:00:00 2001 From: adriano Date: Mon, 1 Apr 2024 09:17:42 -0300 Subject: [PATCH 10/20] fix: Correct text of start date in frontend --- frontend/src/components/ConfigModal/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/ConfigModal/index.js b/frontend/src/components/ConfigModal/index.js index 329c75b..cf41fa3 100644 --- a/frontend/src/components/ConfigModal/index.js +++ b/frontend/src/components/ConfigModal/index.js @@ -355,7 +355,7 @@ const ConfigModal = ({ open, onClose, change }) => { Date: Mon, 1 Apr 2024 17:06:11 -0300 Subject: [PATCH 11/20] fix: Bug when send remote message --- backend/src/controllers/TicketController.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index e8f327c..7b8aefb 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -75,10 +75,10 @@ import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl"; import CreateContactService from "../services/ContactServices/CreateContactService"; import { botSendMessage } from "../services/WbotServices/wbotMessageListener"; import WhatsappQueue from "../models/WhatsappQueue"; -import { get } from "../helpers/RedisClient" -import CountStatusChatEndService from "../services/StatusChatEndService/CountStatusChatEndService" +import { get } from "../helpers/RedisClient"; +import CountStatusChatEndService from "../services/StatusChatEndService/CountStatusChatEndService"; -export const index = async (req: Request, res: Response): Promise => { +export const index = async (req: Request, res: Response): Promise => { const { pageNumber, status, @@ -110,8 +110,8 @@ export const index = async (req: Request, res: Response): Promise => { withUnreadMessages, unlimited, searchParamContent - }); - + }); + return res.status(200).json({ tickets, count, hasMore }); }; @@ -149,7 +149,7 @@ export const remoteTicketCreation = async ( const queue: any = await WhatsappQueue.findOne({ where: { whatsappId }, attributes: ["queueId"] - }); + }); const { queueId } = queue; @@ -213,7 +213,7 @@ export const remoteTicketCreation = async ( undefined, queueId ); - botSendMessage(ticket, msg); + botSendMessage(ticket, `\u200e${msg}`); } const io = getIO(); From 88a71e5758b98a4635ca6706456b0fd6ec1408a9 Mon Sep 17 00:00:00 2001 From: adriano Date: Mon, 1 Apr 2024 17:51:45 -0300 Subject: [PATCH 12/20] fix: Saturday message not matching with database enabled/disabled --- backend/src/controllers/TicketController.ts | 9 ++-- backend/src/helpers/TicketConfig.ts | 49 +++++++++++++-------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index 7b8aefb..1ad1bda 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -141,15 +141,15 @@ export const remoteTicketCreation = async ( const whatsapp = await Whatsapp.findOne({ where: { number: contact_from, status: "CONNECTED" } - }); - + }); + if (whatsapp) { const { id: whatsappId, number, status } = whatsapp; const queue: any = await WhatsappQueue.findOne({ where: { whatsappId }, attributes: ["queueId"] - }); + }); const { queueId } = queue; @@ -213,7 +213,7 @@ export const remoteTicketCreation = async ( undefined, queueId ); - botSendMessage(ticket, `\u200e${msg}`); + botSendMessage(ticket, `${msg}`); } const io = getIO(); @@ -408,7 +408,6 @@ export const update = async ( for (const w of whatsappsByqueue) { let whats = await ListWhatsAppsNumber(w.id); - console.log("-------> WHATS: ", JSON.stringify(whats, null, 6)); const ticket = await Ticket.findOne({ where: { [Op.and]: [ diff --git a/backend/src/helpers/TicketConfig.ts b/backend/src/helpers/TicketConfig.ts index 7a98f40..fe29a61 100644 --- a/backend/src/helpers/TicketConfig.ts +++ b/backend/src/helpers/TicketConfig.ts @@ -62,21 +62,8 @@ const isWeekend = async (number: string | number) => { weekend.value == "enabled" && weekend.message?.trim()?.length > 0 ) { - // Specify your desired timezone - const brazilTimeZone = "America/Sao_Paulo"; - - const currentDateUtc = new Date(); - - // Convert UTC date to Brazil time zone - const currentDate = utcToZonedTime(currentDateUtc, brazilTimeZone); - - // Format the date using the desired format - const formattedDate = _format(currentDate, "yyyy-MM-dd HH:mm:ssXXX"); - - const parsedDate = parseISO(formattedDate); - // Convert parsed date to Brazil time zone - const localDate = utcToZonedTime(parsedDate, brazilTimeZone); + const localDate = localDateConvert(); // Check if it's Saturday or Sunday if (isSaturday(localDate)) { @@ -176,6 +163,14 @@ async function isOutBusinessTime(number: string | number) { async function isOutBusinessTimeSaturday(number: string | number) { let obj = { set: false, msg: "" }; + // Convert parsed date to Brazil time zone + const localDate = localDateConvert(); + + // Check if it's Saturday or Sunday + if (!isSaturday(localDate)) { + return obj; + } + const outBusinessHoursSaturday = await SettingTicket.findOne({ where: { key: "saturdayBusinessTime", number } }); @@ -183,11 +178,9 @@ async function isOutBusinessTimeSaturday(number: string | number) { let isWithinRange = false; if ( - true && - outBusinessHoursSaturday - // outBusinessHoursSaturday && - // outBusinessHoursSaturday.value == "enabled" && - // outBusinessHoursSaturday?.message?.trim()?.length > 0 + outBusinessHoursSaturday && + outBusinessHoursSaturday.value == "enabled" && + outBusinessHoursSaturday?.message?.trim()?.length > 0 ) { const ticketDateTimeUpdate = splitDateTime( new Date( @@ -249,4 +242,22 @@ async function isOutBusinessTimeSaturday(number: string | number) { return obj; } +function localDateConvert() { + const brazilTimeZone = "America/Sao_Paulo"; + + const currentDateUtc = new Date(); + + // Convert UTC date to Brazil time zone + const currentDate = utcToZonedTime(currentDateUtc, brazilTimeZone); + + // Format the date using the desired format + const formattedDate = _format(currentDate, "yyyy-MM-dd HH:mm:ssXXX"); + + const parsedDate = parseISO(formattedDate); + + // Convert parsed date to Brazil time zone + const localDate = utcToZonedTime(parsedDate, brazilTimeZone); + return localDate; +} + export { isWeekend, isHoliday, isOutBusinessTime, isOutBusinessTimeSaturday }; From 7df322d1ab115f6a252c11fe52dc14c9f476a51e Mon Sep 17 00:00:00 2001 From: adriano Date: Tue, 2 Apr 2024 11:59:44 -0300 Subject: [PATCH 13/20] feat: Update controller to use queueId assigned to a WhatsApp number for message sending in remote ticket creation --- TEST_SERVER1/whats/app.js | 6 + backend/src/controllers/QueueController.ts | 55 ++++++ backend/src/controllers/TicketController.ts | 162 ++++++++---------- backend/src/middleware/isAuth.ts | 5 +- backend/src/routes/queueRoutes.ts | 2 + .../ListWhatsAppsForQueueService.ts | 31 +++- 6 files changed, 163 insertions(+), 98 deletions(-) diff --git a/TEST_SERVER1/whats/app.js b/TEST_SERVER1/whats/app.js index 62f316e..f8c030e 100644 --- a/TEST_SERVER1/whats/app.js +++ b/TEST_SERVER1/whats/app.js @@ -183,11 +183,17 @@ socketIo.on('connect_error', async function (err) { }) // +const wwebVersion = '2.2402.5'; + //NOVA OPÇÃO MD client = new Client({ authStrategy: new LocalAuth({ clientId: 'omnihit_sesssion' }), puppeteer: { args: ['--no-sandbox', '--disable-setuid-sandbox'], executablePath: process.env.CHROME_BIN || '/usr/bin/google-chrome-stable' }, + webVersionCache: { + type: 'remote', + remotePath: `https://raw.githubusercontent.com/wppconnect-team/wa-version/main/html/${wwebVersion}.html`, + }, }) client.initialize() diff --git a/backend/src/controllers/QueueController.ts b/backend/src/controllers/QueueController.ts index 493b951..eeb3703 100644 --- a/backend/src/controllers/QueueController.ts +++ b/backend/src/controllers/QueueController.ts @@ -8,6 +8,9 @@ import UpdateQueueService from "../services/QueueService/UpdateQueueService"; import Queue from "../models/Queue"; import AppError from "../errors/AppError"; import { del, get, set } from "../helpers/RedisClient"; +import { Op } from "sequelize"; +import ListWhatsAppsService from "../services/WhatsappService/ListWhatsAppsService"; +import Whatsapp from "../models/Whatsapp"; export const index = async (req: Request, res: Response): Promise => { const queues = await ListQueuesService(); @@ -15,6 +18,58 @@ export const index = async (req: Request, res: Response): Promise => { return res.status(200).json(queues); }; +export const listQueues = async ( + req: Request, + res: Response +): Promise => { + const whatsapps = await Whatsapp.findAll({ + where: { + name: { [Op.ne]: "botqueue" }, + number: { [Op.ne]: "" }, + phoneNumberId: false + }, + attributes: ["number"], + include: [ + { + model: Queue, + as: "queues", + attributes: ["id", "name"] + } + ] + }); + + const whats = whatsapps + ?.filter((w: any) => w?.queues?.length > 0) + ?.map((w: any) => { + const { number, queues } = w; + return { + number, + queues: queues?.map((q: any) => { + const { id, name } = q; + return { id, name }; + }) + }; + }); + + let _queues: any = []; + + for (const w of whats) { + const { queues } = w; + + for (const q of queues) { + const { id: queueId, name } = q; + + const auxQueue = _queues.findIndex((q: any) => q.queueId == queueId); + + if (auxQueue == -1) { + _queues.push({ queueId, name }); + } + } + } + + return res.status(200).json(_queues); +}; + export const store = async (req: Request, res: Response): Promise => { const { name, color, greetingMessage } = req.body; diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index 1ad1bda..549e460 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -119,10 +119,10 @@ export const remoteTicketCreation = async ( req: Request, res: Response ): Promise => { - const { contact_from, contact_to, msg, contact_name }: any = req.body; + const { queueId, contact_to, msg, contact_name }: any = req.body; - const validate = ["contact_from", "contact_to", "msg"]; - const validateOnlyNumber = ["contact_from", "contact_to"]; + const validate = ["queueId", "contact_to", "msg"]; + const validateOnlyNumber = ["queueId", "contact_to"]; for (let prop of validate) { if (!req.body[prop]) @@ -139,109 +139,97 @@ export const remoteTicketCreation = async ( } } - const whatsapp = await Whatsapp.findOne({ - where: { number: contact_from, status: "CONNECTED" } - }); - - if (whatsapp) { - const { id: whatsappId, number, status } = whatsapp; + const whatsapps = await ListWhatsAppsForQueueService(queueId, "CONNECTED"); - const queue: any = await WhatsappQueue.findOne({ - where: { whatsappId }, - attributes: ["queueId"] + if (!whatsapps || whatsapps?.length == 0) { + return res.status(500).json({ + msg: `queueId ${queueId} does not have a WhatsApp number associated with it or the number's session is disconnected.` }); + } - const { queueId } = queue; + const { id: whatsappId } = whatsapps[0]; - // const validNumber = await CheckIsValidContact(contact_to, true); - const validNumber = contact_to; + // const validNumber = await CheckIsValidContact(contact_to, true); + const validNumber = contact_to; - if (validNumber) { - let contact = await Contact.findOne({ where: { number: validNumber } }); + if (validNumber) { + let contact = await Contact.findOne({ where: { number: validNumber } }); - if (!contact) { - // const profilePicUrl = await GetProfilePicUrl(validNumber); + if (!contact) { + // const profilePicUrl = await GetProfilePicUrl(validNumber); - contact = await CreateContactService({ - name: contact_name ? contact_name : contact_to, - number: validNumber - // profilePicUrl - }); - - const io = getIO(); - io.emit("contact", { - action: "create", - contact - }); - } - - const { id: contactId } = contact; - - const botInfo = await BotIsOnQueue("botqueue"); - - let ticket = await Ticket.findOne({ - where: { - [Op.or]: [ - { contactId, status: "queueChoice" }, - { contactId, status: "open", userId: botInfo.userIdBot } - ] - } + contact = await CreateContactService({ + name: contact_name ? contact_name : contact_to, + number: validNumber + // profilePicUrl }); - if (getSettingValue("whatsaAppCloudApi")?.value == "enabled") { - if (ticket) { - await UpdateTicketService({ - ticketData: { status: "closed" }, - ticketId: ticket.id - }); - ticket = null; - } - } else { - if (ticket) { - await UpdateTicketService({ - ticketData: { status: "closed" }, - ticketId: ticket.id - }); - } - } - - if (!ticket) { - ticket = await FindOrCreateTicketService( - contact, - whatsappId, - 0, - undefined, - queueId - ); - botSendMessage(ticket, `${msg}`); - } - const io = getIO(); - io.to(ticket.status).emit("ticket", { - action: "update", - ticket + io.emit("contact", { + action: "create", + contact }); - - console.log( - `REMOTE TICKET CREATION FROM ENDPOINT | STATUS: 200 | MSG: success` - ); - return res.status(200).json({ msg: "success" }); } + const { id: contactId } = contact; + + const botInfo = await BotIsOnQueue("botqueue"); + + let ticket = await Ticket.findOne({ + where: { + [Op.or]: [ + { contactId, status: "queueChoice" }, + { contactId, status: "open", userId: botInfo.userIdBot } + ] + } + }); + + if (getSettingValue("whatsaAppCloudApi")?.value == "enabled") { + if (ticket) { + await UpdateTicketService({ + ticketData: { status: "closed" }, + ticketId: ticket.id + }); + ticket = null; + } + } else { + if (ticket) { + await UpdateTicketService({ + ticketData: { status: "closed" }, + ticketId: ticket.id + }); + } + } + + if (!ticket) { + ticket = await FindOrCreateTicketService( + contact, + whatsappId, + 0, + undefined, + queueId + ); + botSendMessage(ticket, `${msg}`); + } + + const io = getIO(); + io.to(ticket.status).emit("ticket", { + action: "update", + ticket + }); + console.log( - `REMOTE TICKET CREATION FROM ENDPOINT | STATUS: 500 | MSG: The number ${contact_to} does not exist on WhatsApp` + `REMOTE TICKET CREATION FROM ENDPOINT | STATUS: 200 | MSG: success` ); - return res - .status(500) - .json({ msg: `The number ${contact_to} does not exist on WhatsApp` }); + return res.status(200).json({ msg: "success" }); } console.log( - `REMOTE TICKET CREATION FROM ENDPOINT | STATUS: 500 | MSG: Whatsapp number ${contact_from} disconnected or it doesn't exist in omnihit` + `REMOTE TICKET CREATION FROM ENDPOINT | STATUS: 500 | MSG: The number ${contact_to} does not exist on WhatsApp` ); - return res.status(500).json({ - msg: `Whatsapp number ${contact_from} disconnected or it doesn't exist in omnihit` - }); + return res + .status(500) + .json({ msg: `The number ${contact_to} does not exist on WhatsApp` }); }; export const store = async (req: Request, res: Response): Promise => { diff --git a/backend/src/middleware/isAuth.ts b/backend/src/middleware/isAuth.ts index b19552e..91061fc 100644 --- a/backend/src/middleware/isAuth.ts +++ b/backend/src/middleware/isAuth.ts @@ -19,10 +19,11 @@ const isAuth = (req: Request, res: Response, next: NextFunction): void => { throw new AppError("ERR_SESSION_EXPIRED", 401); } - const [, token] = authHeader.split(" "); + const [, token] = authHeader.split(" "); if ( - req.originalUrl == "/tickets/remote/create" && + (req.originalUrl == "/queue/remote/list" || + req.originalUrl == "/tickets/remote/create") && token === process.env.TOKEN_REMOTE_TICKET_CREATION ) { return next(); diff --git a/backend/src/routes/queueRoutes.ts b/backend/src/routes/queueRoutes.ts index 6de13d9..96bb509 100644 --- a/backend/src/routes/queueRoutes.ts +++ b/backend/src/routes/queueRoutes.ts @@ -11,6 +11,8 @@ queueRoutes.post("/queue", isAuth, QueueController.store); queueRoutes.post("/queue/customization", QueueController.customization); +queueRoutes.get("/queue/remote/list", isAuth, QueueController.listQueues); + queueRoutes.get("/queue/:queueId", isAuth, QueueController.show); queueRoutes.put("/queue/:queueId", isAuth, QueueController.update); diff --git a/backend/src/services/WhatsappService/ListWhatsAppsForQueueService.ts b/backend/src/services/WhatsappService/ListWhatsAppsForQueueService.ts index 1f16647..836ef9e 100644 --- a/backend/src/services/WhatsappService/ListWhatsAppsForQueueService.ts +++ b/backend/src/services/WhatsappService/ListWhatsAppsForQueueService.ts @@ -6,17 +6,30 @@ const { QueryTypes } = require("sequelize"); const sequelize = new Sequelize(dbConfig); -const ListWhatsAppsForQueueService = async (queueId: number | string): Promise => { - const distinctWhatsapps = await sequelize.query( - `SELECT w.id, w.number, w.status, wq.whatsappId, wq.queueId -FROM Whatsapps w -JOIN WhatsappQueues wq ON w.id = wq.whatsappId AND wq.queueId = ${queueId} -GROUP BY w.number;`, - { type: QueryTypes.SELECT } - ); +const ListWhatsAppsForQueueService = async ( + queueId: number | string, + status?: string +): Promise => { + let distinctWhatsapps: any; + + if (status) { + distinctWhatsapps = await sequelize.query( + `SELECT w.id, w.number, w.status, wq.whatsappId, wq.queueId FROM Whatsapps w + JOIN WhatsappQueues wq ON w.id = wq.whatsappId AND wq.queueId = ${queueId} AND w.status = '${status}' + AND phoneNumberId = false + GROUP BY w.number;`, + { type: QueryTypes.SELECT } + ); + } else { + distinctWhatsapps = await sequelize.query( + `SELECT w.id, w.number, w.status, wq.whatsappId, wq.queueId FROM Whatsapps w + JOIN WhatsappQueues wq ON w.id = wq.whatsappId AND wq.queueId = ${queueId} + GROUP BY w.number;`, + { type: QueryTypes.SELECT } + ); + } return distinctWhatsapps; }; export default ListWhatsAppsForQueueService; - \ No newline at end of file From a33ce21f4430940774f82ce130ae0826cb2cb8ed Mon Sep 17 00:00:00 2001 From: adriano Date: Tue, 2 Apr 2024 15:33:34 -0300 Subject: [PATCH 14/20] feat: Update to support queueId or contact_from as parameter to create ticket remote --- backend/src/controllers/TicketController.ts | 55 +++++++++++++++++---- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index 549e460..0a60271 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -22,7 +22,7 @@ import format from "date-fns/format"; import ListTicketsServiceCache from "../services/TicketServices/ListTicketServiceCache"; import { searchTicketCache, loadTicketsCache } from "../helpers/TicketCache"; -import { Op } from "sequelize"; +import { Op, where } from "sequelize"; type IndexQuery = { searchParam: string; @@ -119,10 +119,18 @@ export const remoteTicketCreation = async ( req: Request, res: Response ): Promise => { - const { queueId, contact_to, msg, contact_name }: any = req.body; + let { queueId, contact_from, contact_to, msg, contact_name }: any = req.body; - const validate = ["queueId", "contact_to", "msg"]; - const validateOnlyNumber = ["queueId", "contact_to"]; + let whatsappId: any; + + if (!queueId && !contact_from) { + return res + .status(400) + .json({ error: `Property 'queueId' or 'contact_from' is required.` }); + } + + const validate = ["contact_to", "msg"]; + const validateOnlyNumber = ["queueId", "contact_to", "contact_from"]; for (let prop of validate) { if (!req.body[prop]) @@ -139,15 +147,42 @@ export const remoteTicketCreation = async ( } } - const whatsapps = await ListWhatsAppsForQueueService(queueId, "CONNECTED"); + if (queueId) { + const whatsapps = await ListWhatsAppsForQueueService(queueId, "CONNECTED"); - if (!whatsapps || whatsapps?.length == 0) { - return res.status(500).json({ - msg: `queueId ${queueId} does not have a WhatsApp number associated with it or the number's session is disconnected.` + if (!whatsapps || whatsapps?.length == 0) { + return res.status(500).json({ + msg: `queueId ${queueId} does not have a WhatsApp number associated with it or the number's session is disconnected.` + }); + } + + const { id } = whatsapps[0]; + + whatsappId = id; + } else if (contact_from) { + const whatsapp = await Whatsapp.findOne({ + where: { number: contact_from, status: "CONNECTED" } }); - } - const { id: whatsappId } = whatsapps[0]; + if (!whatsapp) { + return res.status(404).json({ + msg: `Whatsapp number ${contact_from} not found or disconnected!` + }); + } + + const { id } = whatsapp; + + const { queues } = await ShowWhatsAppService(id); + + if (!queues || queues.length == 0) { + return res.status(500).json({ + msg: `The WhatsApp number ${contact_from} is not associated with any queue! ` + }); + } + + queueId = queues[0].id; + whatsappId = id; + } // const validNumber = await CheckIsValidContact(contact_to, true); const validNumber = contact_to; From d827e72b7c1abda741bec3f9f99a5523d454dcd2 Mon Sep 17 00:00:00 2001 From: willian-pessoa Date: Tue, 2 Apr 2024 17:11:54 -0300 Subject: [PATCH 15/20] refactor: grafico pizza, melhorias responsividade --- frontend/src/pages/Dashboard/PieChart.js | 115 ++++++++++++----------- 1 file changed, 61 insertions(+), 54 deletions(-) diff --git a/frontend/src/pages/Dashboard/PieChart.js b/frontend/src/pages/Dashboard/PieChart.js index c250ac3..64b64b8 100644 --- a/frontend/src/pages/Dashboard/PieChart.js +++ b/frontend/src/pages/Dashboard/PieChart.js @@ -1,37 +1,24 @@ import { Box } from '@material-ui/core'; import React from 'react'; import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; -import { PieChart as RechartsPieChart, Pie, Sector, Cell, ResponsiveContainer } from 'recharts'; +import { PieChart as RechartsPieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'; import Title from './Title'; -const dataExample = [ - { - "id": 3366, - "name": "FINALIZADO", - "count": 5 - }, - { - "id": 3369, - "name": "LEMBRETE", - "count": 1 - }, - { - "id": 3367, - "name": "EXEMPLO", - "count": 3 - }, - { - "id": 3364, - "name": "EXEMPLO 2", - "count": 3 - }, - { - "id": 3364, - "name": "EXEMPLO 3", - "count": 6 - }, -] +const generateDataExample = (amount) => { + const arr = [] + for (let i = 1; i <= amount; i++) { + arr.push({ + "id": i, + "name": `Exemplo ${i}`, + "count": Math.floor(Math.random() * 10 + 2) + }) + } + + return arr +} + +const dataExample = generateDataExample(20) const COLORS = [ '#0088FE', // Azul escuro @@ -44,12 +31,22 @@ const COLORS = [ '#C0FFC0', // Verde Claro '#C4E538', // Verde-amarelo vibrante '#A2A2A2', // Cinza claro -];; + '#FFF700', // Amarelo Canário + '#FF69B4', // Rosa Flamingo + '#87CEEB', // Azul Celeste + '#228B22', // Verde Esmeralda + '#9B59B6', // Roxo Ametista + '#FF9933', // Laranja Tangerina + '#FF7F50', // Coral Vivo + '#00CED1', // Verde Água + '#000080', // Azul Marinho + '#FFDB58', // Amarelo Mostarda +]; const RADIAN = Math.PI / 180; const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, count }) => { - const radius = innerRadius + (outerRadius - innerRadius) * 0.75; + const radius = innerRadius + (outerRadius - innerRadius) * 0.80; const x = cx + radius * Math.cos(-midAngle * RADIAN); const y = cy + radius * Math.sin(-midAngle * RADIAN); @@ -70,9 +67,36 @@ const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, cou */ const PieChart = ({ data = dataExample }) => { return ( - - - Tickets Status + + + + Tickets Status + + + + + {data.map((entry, index) => ( + + ))} + + + + { top: 0, right: 0, display: "flex", flexDirection: "column", - gap: "4px" + gap: "4px", + maxWidth: "60%", + minWidth: "50%", + zIndex: 0, }}> {data.map((entry, index) => { return ( @@ -99,26 +126,6 @@ const PieChart = ({ data = dataExample }) => { ) })} - - - - - {data.map((entry, index) => ( - - ))} - - - - ); From a5657d0a2f60a2d1cd18d40be31261b5e0d24ff9 Mon Sep 17 00:00:00 2001 From: adriano Date: Wed, 3 Apr 2024 14:10:53 -0300 Subject: [PATCH 16/20] feat: Enable reception of multiple vCards sent via WhatsApp --- .../WbotServices/wbotMessageListener.ts | 104 +++++-- .../ContactCreateTicketModal/index.js | 2 +- frontend/src/components/MessageInput/index.js | 12 +- frontend/src/components/MessagesList/index.js | 289 ++++++++++-------- frontend/src/components/VcardPreview/index.js | 63 ++-- frontend/src/pages/Report/index.js | 3 +- 6 files changed, 264 insertions(+), 209 deletions(-) diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index ffa650e..1cf3016 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -46,7 +46,7 @@ import FindOrCreateTicketService from "../TicketServices/FindOrCreateTicketServi import ShowWhatsAppService from "../WhatsappService/ShowWhatsAppService"; import { debounce } from "../../helpers/Debounce"; import UpdateTicketService from "../TicketServices/UpdateTicketService"; -import { date } from "faker"; +import { date, name } from "faker"; import ShowQueueService from "../QueueService/ShowQueueService"; import ShowTicketMessage from "../TicketServices/ShowTicketMessage"; @@ -101,6 +101,7 @@ import ShowTicketService from "../TicketServices/ShowTicketService"; import ShowQueuesByUser from "../UserServices/ShowQueuesByUser"; import ListWhatsappQueuesByUserQueue from "../UserServices/ListWhatsappQueuesByUserQueue"; import CreateContactService from "../ContactServices/CreateContactService"; +import { number } from "yup"; var lst: any[] = getWhatsappIds(); @@ -316,6 +317,14 @@ const verifyMessage = async ( await ticket.update({ lastMessage: msg.body }); + if (!msg?.fromMe && msg?.vCards && msg?.vCards?.length > 0) { + if (msg.vCards.length == 1) { + messageData = { ...messageData, body: msg.vCards[0] }; + } else { + messageData = { ...messageData, body: JSON.stringify(msg.vCards) }; + } + } + await CreateMessageService({ messageData }); }; @@ -504,7 +513,7 @@ const isValidMsg = (msg: any): boolean => { msg.type === "image" || msg.type === "document" || msg.type === "vcard" || - // msg.type === "multi_vcard" || + msg.type === "multi_vcard" || msg.type === "sticker" ) return true; @@ -550,7 +559,7 @@ const transferTicket = async ( } if (queue) await botTransferTicket(queue, ticket, sendGreetingMessage); - io.emit('notifyPeding', {data: {ticket, queue}}); + io.emit("notifyPeding", { data: { ticket, queue } }); }; const botTransferTicket = async ( @@ -586,7 +595,7 @@ const botTransferTicketToUser = async ( }; const botSendMessage = (ticket: Ticket, msg: string) => { - const { phoneNumberId } = ticket; + const { phoneNumberId } = ticket; const debouncedSentMessage = debounce( async () => { @@ -644,9 +653,9 @@ const handleMessage = async ( let msgContact: any = wbot.msgContact; // let groupContact: Contact | undefined; - if (msg.fromMe) { + 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 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" @@ -787,32 +796,8 @@ const handleMessage = async ( await verifyQueue(wbot, msg, ticket, contact); } - if (msg.type === "vcard") { - try { - const array = msg.body.split("\n"); - const obj = []; - let contact = ""; - for (let index = 0; index < array.length; index++) { - const v = array[index]; - const values = v.split(":"); - for (let ind = 0; ind < values.length; ind++) { - if (values[ind].indexOf("+") !== -1) { - obj.push({ number: values[ind] }); - } - if (values[ind].indexOf("FN") !== -1) { - contact = values[ind + 1]; - } - } - } - for await (const ob of obj) { - const cont = await CreateContactService({ - name: contact, - number: ob.number.replace(/\D/g, "") - }); - } - } catch (error) { - console.log(error); - } + if (msg.type === "vcard" || msg.type === "multi_vcard") { + await vcard(msg); } const botInfo = await BotIsOnQueue("botqueue"); @@ -977,7 +962,7 @@ const handleMessage = async ( if (ticket?.queueId) { ticketHasQueue = true; } - + if (ticketHasQueue && ticket.status != "open") { let whatsapp: any = await whatsappInfo(ticket?.whatsappId); @@ -1258,6 +1243,59 @@ export { mediaTypeWhatsappOfficial, botSendMessage }; +async function vcard(msg: any) { + let array: any[] = []; + let contact: any; + let obj: any[] = []; + + try { + const multi_vcard = msg?.vCards?.length === 0 ? false : true; + + if (multi_vcard) { + array = msg?.vCards; + contact = []; + } else { + array = msg.body.split("\n"); + contact = ""; + } + + for (let index = 0; index < array.length; index++) { + const v = array[index]; + const values = v.split(":"); + for (let ind = 0; ind < values.length; ind++) { + if (values[ind].indexOf("+") !== -1) { + obj.push({ number: values[ind] }); + } + if (values[ind].indexOf("FN") !== -1) { + if (multi_vcard) + contact.push({ name: values[ind + 1].split("\n")[0] }); + else contact = values[ind + 1]; + } + } + } + + for (const i in obj) { + let data: any = {}; + + if (multi_vcard) { + data = { + name: contact[i].name, + number: obj[i].number.replace(/\D/g, "") + }; + } else { + data = { + name: contact, + number: obj[i].number.replace(/\D/g, "") + }; + } + + const cont = await CreateContactService(data); + } + } catch (error) { + console.log(error); + } +} + async function backUra(whatsappId: any, contactId: any, data: any) { let uraOptionSelected = await findObject(whatsappId, contactId, "ura"); diff --git a/frontend/src/components/ContactCreateTicketModal/index.js b/frontend/src/components/ContactCreateTicketModal/index.js index 3681c6c..3ba9281 100644 --- a/frontend/src/components/ContactCreateTicketModal/index.js +++ b/frontend/src/components/ContactCreateTicketModal/index.js @@ -157,7 +157,7 @@ const ContactCreateTicketModal = ({ modalOpen, onClose, contactId }) => { const { data } = await api.get("/whatsapp/official/matchQueue", { params: { userId: user.id, queueId: selectedQueue }, }) - console.log('WHATSAPP DATA: ', data) + // console.log('WHATSAPP DATA: ', data) setWhatsQueue(data) diff --git a/frontend/src/components/MessageInput/index.js b/frontend/src/components/MessageInput/index.js index 9773517..e710ab2 100644 --- a/frontend/src/components/MessageInput/index.js +++ b/frontend/src/components/MessageInput/index.js @@ -311,9 +311,7 @@ const MessageInput = ({ ticketStatus }) => { } const handleSendMessage = async (templateParams = null) => { - - console.log('templateParams: ', templateParams, ' | inputMessage: ', inputMessage) - + if (inputMessage.trim() === "") return setLoading(true) @@ -323,9 +321,7 @@ const MessageInput = ({ ticketStatus }) => { if (templateParams) { for (let key in templateParams) { - if (templateParams.hasOwnProperty(key)) { - // let value = templateParams[key] - // console.log('key: ', key, ' | ', 'VALUE: ', value) + if (templateParams.hasOwnProperty(key)) { if (key === '_reactName') { templateParams = null @@ -348,9 +344,7 @@ const MessageInput = ({ ticketStatus }) => { } - try { - - console.log('kkkkkkkkkkkkkkkkkkk message: ', message) + try { const { data } = await api.post(`/messages/${ticketId}`, message) setParams(null) diff --git a/frontend/src/components/MessagesList/index.js b/frontend/src/components/MessagesList/index.js index 9d25471..5a99c79 100644 --- a/frontend/src/components/MessagesList/index.js +++ b/frontend/src/components/MessagesList/index.js @@ -1,18 +1,18 @@ -import React, { useContext, useState, useEffect, useReducer, useRef } from "react"; +import React, { useContext, useState, useEffect, useReducer, useRef } from "react" -import { isSameDay, parseISO, format } from "date-fns"; -import openSocket from "socket.io-client"; -import clsx from "clsx"; -import { AuthContext } from "../../context/Auth/AuthContext"; +import { isSameDay, parseISO, format } from "date-fns" +import openSocket from "socket.io-client" +import clsx from "clsx" +import { AuthContext } from "../../context/Auth/AuthContext" -import { green } from "@material-ui/core/colors"; +import { green } from "@material-ui/core/colors" import { Button, CircularProgress, Divider, IconButton, makeStyles, -} from "@material-ui/core"; +} from "@material-ui/core" import { AccessTime, Block, @@ -20,20 +20,20 @@ import { DoneAll, ExpandMore, GetApp, -} from "@material-ui/icons"; +} from "@material-ui/icons" -import MarkdownWrapper from "../MarkdownWrapper"; -import VcardPreview from "../VcardPreview"; -import LocationPreview from "../LocationPreview"; -import Audio from "../Audio"; +import MarkdownWrapper from "../MarkdownWrapper" +import VcardPreview from "../VcardPreview" +import LocationPreview from "../LocationPreview" +import Audio from "../Audio" -import ModalImageCors from "../ModalImageCors"; -import MessageOptionsMenu from "../MessageOptionsMenu"; -import whatsBackground from "../../assets/wa-background.png"; +import ModalImageCors from "../ModalImageCors" +import MessageOptionsMenu from "../MessageOptionsMenu" +import whatsBackground from "../../assets/wa-background.png" -import api from "../../services/api"; -import toastError from "../../errors/toastError"; +import api from "../../services/api" +import toastError from "../../errors/toastError" const useStyles = makeStyles((theme) => ({ messagesListWrapper: { @@ -262,78 +262,78 @@ const useStyles = makeStyles((theme) => ({ backgroundColor: "inherit", padding: 10, }, -})); +})) const reducer = (state, action) => { if (action.type === "LOAD_MESSAGES") { - const messages = action.payload; - const newMessages = []; + const messages = action.payload + const newMessages = [] messages.forEach((message) => { - const messageIndex = state.findIndex((m) => m.id === message.id); + const messageIndex = state.findIndex((m) => m.id === message.id) if (messageIndex !== -1) { - state[messageIndex] = message; + state[messageIndex] = message } else { - newMessages.push(message); + newMessages.push(message) } - }); + }) - return [...newMessages, ...state]; + return [...newMessages, ...state] } if (action.type === "ADD_MESSAGE") { - const newMessage = action.payload; - const messageIndex = state.findIndex((m) => m.id === newMessage.id); + const newMessage = action.payload + const messageIndex = state.findIndex((m) => m.id === newMessage.id) if (messageIndex !== -1) { - state[messageIndex] = newMessage; + state[messageIndex] = newMessage } else { - state.push(newMessage); + state.push(newMessage) } - return [...state]; + return [...state] } if (action.type === "UPDATE_MESSAGE") { - const messageToUpdate = action.payload; - const messageIndex = state.findIndex((m) => m.id === messageToUpdate.id); + const messageToUpdate = action.payload + const messageIndex = state.findIndex((m) => m.id === messageToUpdate.id) if (messageIndex !== -1) { - state[messageIndex] = messageToUpdate; + state[messageIndex] = messageToUpdate } - return [...state]; + return [...state] } if (action.type === "RESET") { - return []; + return [] } -}; +} const MessagesList = ({ ticketId, isGroup }) => { - const classes = useStyles(); + const classes = useStyles() - const [messagesList, dispatch] = useReducer(reducer, []); - const [pageNumber, setPageNumber] = useState(1); - const [hasMore, setHasMore] = useState(false); - const [loading, setLoading] = useState(false); - const lastMessageRef = useRef(); + const [messagesList, dispatch] = useReducer(reducer, []) + const [pageNumber, setPageNumber] = useState(1) + const [hasMore, setHasMore] = useState(false) + const [loading, setLoading] = useState(false) + const lastMessageRef = useRef() - const [selectedMessage, setSelectedMessage] = useState({}); - const [anchorEl, setAnchorEl] = useState(null); - const messageOptionsMenuOpen = Boolean(anchorEl); - const currentTicketId = useRef(ticketId); + const [selectedMessage, setSelectedMessage] = useState({}) + const [anchorEl, setAnchorEl] = useState(null) + const messageOptionsMenuOpen = Boolean(anchorEl) + const currentTicketId = useRef(ticketId) const [sendSeen, setSendSeen] = useState(false) - const { user } = useContext(AuthContext); + const { user } = useContext(AuthContext) useEffect(() => { - dispatch({ type: "RESET" }); - setPageNumber(1); + dispatch({ type: "RESET" }) + setPageNumber(1) - currentTicketId.current = ticketId; - }, [ticketId]); + currentTicketId.current = ticketId + }, [ticketId]) useEffect(() => { @@ -359,7 +359,7 @@ const MessagesList = ({ ticketId, isGroup }) => { try { const { data } = await api.get("/messages/" + ticketId, { params: { pageNumber }, - }); + }) setSendSeen(false) @@ -382,116 +382,116 @@ const MessagesList = ({ ticketId, isGroup }) => { } } catch (err) { - setLoading(false); - toastError(err); + setLoading(false) + toastError(err) } - }; - sendSeenMessage(); - }, 500); + } + sendSeenMessage() + }, 500) return () => { - clearTimeout(delayDebounceFn); - }; + clearTimeout(delayDebounceFn) + } - }, [sendSeen, pageNumber, ticketId, user.id]); + }, [sendSeen, pageNumber, ticketId, user.id]) useEffect(() => { - setLoading(true); + setLoading(true) const delayDebounceFn = setTimeout(() => { const fetchMessages = async () => { try { const { data } = await api.get("/messages/" + ticketId, { params: { pageNumber }, - }); + }) if (currentTicketId.current === ticketId) { - dispatch({ type: "LOAD_MESSAGES", payload: data.messages }); - setHasMore(data.hasMore); - setLoading(false); + dispatch({ type: "LOAD_MESSAGES", payload: data.messages }) + setHasMore(data.hasMore) + setLoading(false) } if (pageNumber === 1 && data.messages.length > 1) { - scrollToBottom(); + scrollToBottom() } } catch (err) { - setLoading(false); - toastError(err); + setLoading(false) + toastError(err) } - }; - fetchMessages(); - }, 500); + } + fetchMessages() + }, 500) return () => { - clearTimeout(delayDebounceFn); - }; - }, [pageNumber, ticketId]); + clearTimeout(delayDebounceFn) + } + }, [pageNumber, ticketId]) useEffect(() => { - const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + const socket = openSocket(process.env.REACT_APP_BACKEND_URL) - socket.on("connect", () => socket.emit("joinChatBox", ticketId)); + socket.on("connect", () => socket.emit("joinChatBox", ticketId)) socket.on("appMessage", (data) => { if (data.action === "create") { - dispatch({ type: "ADD_MESSAGE", payload: data.message }); + dispatch({ type: "ADD_MESSAGE", payload: data.message }) - scrollToBottom(); + scrollToBottom() } if (data.action === "update") { - dispatch({ type: "UPDATE_MESSAGE", payload: data.message }); + dispatch({ type: "UPDATE_MESSAGE", payload: data.message }) } - }); + }) return () => { - socket.disconnect(); - }; - }, [ticketId]); + socket.disconnect() + } + }, [ticketId]) const loadMore = () => { - setPageNumber((prevPageNumber) => prevPageNumber + 1); - }; + setPageNumber((prevPageNumber) => prevPageNumber + 1) + } const scrollToBottom = () => { if (lastMessageRef.current) { setSendSeen(true) - lastMessageRef.current.scrollIntoView({}); + lastMessageRef.current.scrollIntoView({}) } - }; + } const handleScroll = (e) => { - if (!hasMore) return; - const { scrollTop } = e.currentTarget; + if (!hasMore) return + const { scrollTop } = e.currentTarget if (scrollTop === 0) { - document.getElementById("messagesList").scrollTop = 1; + document.getElementById("messagesList").scrollTop = 1 } if (loading) { - return; + return } if (scrollTop < 50) { - loadMore(); + loadMore() } - }; + } const handleOpenMessageOptionsMenu = (e, message) => { - setAnchorEl(e.currentTarget); - setSelectedMessage(message); - }; + setAnchorEl(e.currentTarget) + setSelectedMessage(message) + } const handleCloseMessageOptionsMenu = (e) => { - setAnchorEl(null); - }; + setAnchorEl(null) + } // const checkMessageMedia = (message) => { // if (message.mediaType === "image") { @@ -548,8 +548,6 @@ const MessagesList = ({ ticketId, isGroup }) => { return } else if (message.mediaType === "vcard") { - //console.log("vcard") - //console.log(message) let array = message.body.split("\n") let obj = [] let contact = "" @@ -567,23 +565,44 @@ const MessagesList = ({ ticketId, isGroup }) => { } return } - /*else if (message.mediaType === "multi_vcard") { - console.log("multi_vcard") - console.log(message) - - if(message.body !== null && message.body !== "") { + else if (message.mediaType === "multi_vcard") { + if (message.body !== null && message.body !== "") { let newBody = JSON.parse(message.body) + + let multi_vcard = newBody.map(v => { + let array = v.split("\n") + let obj = [] + let contact = "" + for (let index = 0; index < array.length; index++) { + const v = array[index] + let values = v.split(":") + for (let ind = 0; ind < values.length; ind++) { + if (values[ind].indexOf("+") !== -1) { + obj.push({ number: values[ind] }) + } + if (values[ind].indexOf("FN") !== -1) { + contact = values[ind + 1] + } + } + } + + return { name: contact, number: obj[0]?.number } + }) return ( <> { - newBody.map(v => ( - - )) + multi_vcard.map((v, index) => ( + <> + + + {((index + 1) <= multi_vcard.length - 1) && } + + )) } ) } else return (<>) - }*/ + } else if (/^.*\.(jpe?g|png|gif)?$/i.exec(message.mediaUrl) && message.mediaType === "image") { return } else if (message.mediaType === "audio") { @@ -614,22 +633,22 @@ const MessagesList = ({ ticketId, isGroup }) => { ) } - }; + } const renderMessageAck = (message) => { if (message.ack === 0) { - return ; + return } if (message.ack === 1) { - return ; + return } if (message.ack === 2) { - return ; + return } if (message.ack === 3 || message.ack === 4) { - return ; + return } - }; + } const renderDailyTimestamps = (message, index) => { if (index === 0) { @@ -642,12 +661,12 @@ const MessagesList = ({ ticketId, isGroup }) => { {format(parseISO(messagesList[index].createdAt), "dd/MM/yyyy")} - ); + ) } if (index < messagesList.length - 1) { - let messageDay = parseISO(messagesList[index].createdAt); - let previousMessageDay = parseISO(messagesList[index - 1].createdAt); + let messageDay = parseISO(messagesList[index].createdAt) + let previousMessageDay = parseISO(messagesList[index - 1].createdAt) if (!isSameDay(messageDay, previousMessageDay)) { @@ -660,14 +679,14 @@ const MessagesList = ({ ticketId, isGroup }) => { {format(parseISO(messagesList[index].createdAt), "dd/MM/yyyy")} - ); + ) } } if (index === messagesList.length - 1) { - let messageDay = parseISO(messagesList[index].createdAt); - let previousMessageDay = parseISO(messagesList[index - 1].createdAt); + let messageDay = parseISO(messagesList[index].createdAt) + let previousMessageDay = parseISO(messagesList[index - 1].createdAt) return ( <> @@ -687,24 +706,24 @@ const MessagesList = ({ ticketId, isGroup }) => { style={{ float: "left", clear: "both" }} /> - ); + ) } - }; + } const renderMessageDivider = (message, index) => { if (index < messagesList.length && index > 0) { - let messageUser = messagesList[index].fromMe; - let previousMessageUser = messagesList[index - 1].fromMe; + let messageUser = messagesList[index].fromMe + let previousMessageUser = messagesList[index - 1].fromMe if (messageUser !== previousMessageUser) { return ( - ); + ) } } - }; + } const renderQuotedMessage = (message) => { return ( @@ -727,8 +746,8 @@ const MessagesList = ({ ticketId, isGroup }) => { {message.quotedMsg?.body} - ); - }; + ) + } // const renderMessages = () => { // if (messagesList.length > 0) { @@ -837,7 +856,7 @@ const MessagesList = ({ ticketId, isGroup }) => { )} {(message.mediaUrl || message.mediaType === "location" || message.mediaType === "vcard" - //|| message.mediaType === "multi_vcard" + || message.mediaType === "multi_vcard" ) && checkMessageMedia(message)}
{message.quotedMsg && renderQuotedMessage(message)} @@ -866,7 +885,7 @@ const MessagesList = ({ ticketId, isGroup }) => { {(message.mediaUrl || message.mediaType === "location" || message.mediaType === "vcard" - //|| message.mediaType === "multi_vcard" + // || message.mediaType === "multi_vcard" ) && checkMessageMedia(message)}
{ } else { return
Say hello to your new contact!
} - }; + } return (
@@ -919,7 +938,7 @@ const MessagesList = ({ ticketId, isGroup }) => {
)}
- ); -}; + ) +} -export default MessagesList; \ No newline at end of file +export default MessagesList \ No newline at end of file diff --git a/frontend/src/components/VcardPreview/index.js b/frontend/src/components/VcardPreview/index.js index f2a63be..8f5d0b6 100644 --- a/frontend/src/components/VcardPreview/index.js +++ b/frontend/src/components/VcardPreview/index.js @@ -1,29 +1,29 @@ -import React, { useEffect, useState, useContext } from 'react'; -import { useHistory } from "react-router-dom"; -import toastError from "../../errors/toastError"; -import api from "../../services/api"; +import React, { useEffect, useState, useContext } from 'react' +import { useHistory } from "react-router-dom" +import toastError from "../../errors/toastError" +import api from "../../services/api" -import Avatar from "@material-ui/core/Avatar"; -import Typography from "@material-ui/core/Typography"; -import Grid from "@material-ui/core/Grid"; +import Avatar from "@material-ui/core/Avatar" +import Typography from "@material-ui/core/Typography" +import Grid from "@material-ui/core/Grid" -import { AuthContext } from "../../context/Auth/AuthContext"; +import { AuthContext } from "../../context/Auth/AuthContext" -import { Button, Divider, } from "@material-ui/core"; +import { Button, Divider, } from "@material-ui/core" -const VcardPreview = ({ contact, numbers }) => { - const history = useHistory(); - const { user } = useContext(AuthContext); +const VcardPreview = ({ contact, numbers, multi_vCard }) => { + const history = useHistory() + const { user } = useContext(AuthContext) const [selectedContact, setContact] = useState({ name: "", number: 0, profilePicUrl: "" - }); + }) useEffect(() => { const delayDebounceFn = setTimeout(() => { - const fetchContacts = async () => { + const fetchContacts = async () => { try { let contactObj = { name: contact, @@ -31,19 +31,20 @@ const VcardPreview = ({ contact, numbers }) => { number: numbers !== undefined && numbers.replace(/\D/g, ""), email: "" } - - const { data } = await api.post("/contact", contactObj); + + const { data } = await api.post("/contact", contactObj) + setContact(data) } catch (err) { console.log(err) - toastError(err); + toastError(err) } - }; - fetchContacts(); - }, 500); - return () => clearTimeout(delayDebounceFn); - }, [contact, numbers]); + } + fetchContacts() + }, 500) + return () => clearTimeout(delayDebounceFn) + }, [contact, numbers]) const handleNewChat = async () => { try { @@ -51,10 +52,10 @@ const VcardPreview = ({ contact, numbers }) => { contactId: selectedContact.id, userId: user.id, status: "open", - }); - history.push(`/tickets/${ticket.id}`); + }) + history.push(`/tickets/${ticket.id}`) } catch (err) { - toastError(err); + toastError(err) } } @@ -73,19 +74,23 @@ const VcardPreview = ({ contact, numbers }) => { - + {!multi_vCard && } + + + {/* {multi_vCard && } */} +
- ); + ) -}; +} -export default VcardPreview; \ No newline at end of file +export default VcardPreview \ No newline at end of file diff --git a/frontend/src/pages/Report/index.js b/frontend/src/pages/Report/index.js index 1724682..95083fe 100644 --- a/frontend/src/pages/Report/index.js +++ b/frontend/src/pages/Report/index.js @@ -460,8 +460,7 @@ const Report = () => { } // Get from report type option - const reportTypeValue = (data) => { - console.log('DATA: ', data) + const reportTypeValue = (data) => { let type = '1' if (data === '1') type = 'default' if (data === '2') type = 'synthetic' From 5eac253549aad56ced50995fd33fefcb143e1bb6 Mon Sep 17 00:00:00 2001 From: adriano Date: Wed, 3 Apr 2024 18:38:56 -0300 Subject: [PATCH 17/20] feat: Update to allow creation of remote tickets from cost center --- backend/src/controllers/QueueController.ts | 4 +-- backend/src/controllers/TicketController.ts | 31 ++++++++++++++++--- .../20240403202639-add-cc-to-Queues.ts | 14 +++++++++ backend/src/models/Queue.ts | 3 ++ .../QueueService/CreateQueueService.ts | 1 + .../QueueService/UpdateQueueService.ts | 3 +- .../ListWhatsAppsForQueueService.ts | 4 +-- frontend/src/components/QueueModal/index.js | 15 +++++++++ 8 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 backend/src/database/migrations/20240403202639-add-cc-to-Queues.ts diff --git a/backend/src/controllers/QueueController.ts b/backend/src/controllers/QueueController.ts index eeb3703..8f5aa81 100644 --- a/backend/src/controllers/QueueController.ts +++ b/backend/src/controllers/QueueController.ts @@ -71,9 +71,9 @@ export const listQueues = async ( }; export const store = async (req: Request, res: Response): Promise => { - const { name, color, greetingMessage } = req.body; + const { name, color, greetingMessage, cc } = req.body; - const queue = await CreateQueueService({ name, color, greetingMessage }); + const queue = await CreateQueueService({ name, color, greetingMessage, cc }); const io = getIO(); io.emit("queue", { diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index 0a60271..fd6c193 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -77,6 +77,7 @@ import { botSendMessage } from "../services/WbotServices/wbotMessageListener"; import WhatsappQueue from "../models/WhatsappQueue"; import { get } from "../helpers/RedisClient"; import CountStatusChatEndService from "../services/StatusChatEndService/CountStatusChatEndService"; +import Queue from "../models/Queue"; export const index = async (req: Request, res: Response): Promise => { const { @@ -119,14 +120,15 @@ export const remoteTicketCreation = async ( req: Request, res: Response ): Promise => { - let { queueId, contact_from, contact_to, msg, contact_name }: any = req.body; + let { queueId, contact_from, cc, contact_to, msg, contact_name }: any = + req.body; let whatsappId: any; - if (!queueId && !contact_from) { - return res - .status(400) - .json({ error: `Property 'queueId' or 'contact_from' is required.` }); + if (!queueId && !contact_from && !cc) { + return res.status(400).json({ + error: `Property 'queueId' or 'contact_from' or 'cc' is required.` + }); } const validate = ["contact_to", "msg"]; @@ -182,6 +184,25 @@ export const remoteTicketCreation = async ( queueId = queues[0].id; whatsappId = id; + } else if (cc) { + const queue = await Queue.findOne({ where: { cc } }); + if (!queue) { + return res.status(404).json({ + msg: `Queue with cc ${cc} not found! ` + }); + } + + queueId = queue.id; + + const whatsapps = await ListWhatsAppsForQueueService(queueId, "CONNECTED"); + + if ( whatsapps.length === 0) { + return res.status(500).json({ + msg: `No WhatsApp found for this cc ${cc} or the WhatsApp number is disconnected! ` + }); + } + + whatsappId = whatsapps[0].id; } // const validNumber = await CheckIsValidContact(contact_to, true); diff --git a/backend/src/database/migrations/20240403202639-add-cc-to-Queues.ts b/backend/src/database/migrations/20240403202639-add-cc-to-Queues.ts new file mode 100644 index 0000000..60cf4ff --- /dev/null +++ b/backend/src/database/migrations/20240403202639-add-cc-to-Queues.ts @@ -0,0 +1,14 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Queues", "cc", { + type: DataTypes.STRING, + allowNull: true + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Queues", "cc"); + } +}; diff --git a/backend/src/models/Queue.ts b/backend/src/models/Queue.ts index c5c06d9..45ae22f 100644 --- a/backend/src/models/Queue.ts +++ b/backend/src/models/Queue.ts @@ -36,6 +36,9 @@ class Queue extends Model { @Column greetingMessage: string; + @Column + cc: string; + @CreatedAt createdAt: Date; diff --git a/backend/src/services/QueueService/CreateQueueService.ts b/backend/src/services/QueueService/CreateQueueService.ts index 528d1b1..d13b5cc 100644 --- a/backend/src/services/QueueService/CreateQueueService.ts +++ b/backend/src/services/QueueService/CreateQueueService.ts @@ -7,6 +7,7 @@ interface QueueData { name: string; color: string; greetingMessage?: string; + cc?: string; } const CreateQueueService = async (queueData: QueueData): Promise => { diff --git a/backend/src/services/QueueService/UpdateQueueService.ts b/backend/src/services/QueueService/UpdateQueueService.ts index 59a6077..6f78880 100644 --- a/backend/src/services/QueueService/UpdateQueueService.ts +++ b/backend/src/services/QueueService/UpdateQueueService.ts @@ -3,12 +3,13 @@ import * as Yup from "yup"; import AppError from "../../errors/AppError"; import Queue from "../../models/Queue"; import ShowQueueService from "./ShowQueueService"; -import { set } from "../../helpers/RedisClient" +import { set } from "../../helpers/RedisClient"; interface QueueData { name?: string; color?: string; greetingMessage?: string; + cc?: string; } const UpdateQueueService = async ( diff --git a/backend/src/services/WhatsappService/ListWhatsAppsForQueueService.ts b/backend/src/services/WhatsappService/ListWhatsAppsForQueueService.ts index 836ef9e..63d6c91 100644 --- a/backend/src/services/WhatsappService/ListWhatsAppsForQueueService.ts +++ b/backend/src/services/WhatsappService/ListWhatsAppsForQueueService.ts @@ -10,9 +10,9 @@ const ListWhatsAppsForQueueService = async ( queueId: number | string, status?: string ): Promise => { - let distinctWhatsapps: any; + let distinctWhatsapps: any[] = []; - if (status) { + if (status) { distinctWhatsapps = await sequelize.query( `SELECT w.id, w.number, w.status, wq.whatsappId, wq.queueId FROM Whatsapps w JOIN WhatsappQueues wq ON w.id = wq.whatsappId AND wq.queueId = ${queueId} AND w.status = '${status}' diff --git a/frontend/src/components/QueueModal/index.js b/frontend/src/components/QueueModal/index.js index fa5caa9..3baaa77 100644 --- a/frontend/src/components/QueueModal/index.js +++ b/frontend/src/components/QueueModal/index.js @@ -70,6 +70,7 @@ const QueueModal = ({ open, onClose, queueId }) => { name: "", color: "", greetingMessage: "", + cc:"" }; const [colorPickerModalOpen, setColorPickerModalOpen] = useState(false); @@ -94,6 +95,7 @@ const QueueModal = ({ open, onClose, queueId }) => { name: "", color: "", greetingMessage: "", + cc: "" }); }; }, [queueId, open]); @@ -213,6 +215,19 @@ const QueueModal = ({ open, onClose, queueId }) => { margin="dense" /> +
+ +
- - - - - ); + + +
+ + {statusChatEndName === 'LEMBRETE' && + + + + Lembrete + + + + + + + + + + + + + + + + + + + + + + + } + + + {statusChatEndName === 'AGENDAMENTO À CONFIRMAR' && + + + + + Agendamento + + + + + + + + + + + + + + + + + + + {currencyHourBefore && startDate && typeof (startDate) === 'string' && startDate.trim().length > 0 && currenciesTimeBefore.length > 0 && + 0 ? false : true} + select + label="Enviar mensagem para cliente" + value={currencyHourBefore} + size="small" + onChange={handleChangeHourBefore} + > + {currenciesTimeBefore.map((option) => ( + + {option.label} + + ))} + + } + + + + + + + + + + + + + } + + + + +
+ + + + +
+ +
+ +
+ + + ) } export default Modal \ No newline at end of file diff --git a/frontend/src/components/PositionModal/index.js b/frontend/src/components/PositionModal/index.js new file mode 100644 index 0000000..516f4ac --- /dev/null +++ b/frontend/src/components/PositionModal/index.js @@ -0,0 +1,255 @@ +import React, { useState, useEffect, useRef, useContext } from "react" + +import * as Yup from "yup" +import { Formik, Form, Field } from "formik" +import { toast } from "react-toastify" +import openSocket from 'socket.io-client' + + +import { + makeStyles, + Button, + TextField, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + CircularProgress, +} from "@material-ui/core" +import { green } from "@material-ui/core/colors" +import { i18n } from "../../translate/i18n" +import QueueSelect from '../QueueSelect' +import { AuthContext } from '../../context/Auth/AuthContext' + +import api from "../../services/api" +import toastError from "../../errors/toastError" + +const useStyles = makeStyles((theme) => ({ + root: { + flexWrap: "wrap", + }, + textField: { + marginRight: theme.spacing(1), + width: "100%", + }, + + btnWrapper: { + position: "relative", + }, + + buttonProgress: { + color: green[500], + position: "absolute", + top: "50%", + left: "50%", + marginTop: -12, + marginLeft: -12, + }, + textQuickAnswerContainer: { + width: "100%", + }, +})) + +const PositionSchema = Yup.object().shape({ + name: Yup.string() + .required("Required"), +}) + +const PositionModal = ({ + open, + onClose, + positionId, + initialValues, + onSave, +}) => { + const classes = useStyles() + const isMounted = useRef(true) + + const initialState = { + name: "", + } + + const [position, setPosition] = useState(initialState) + const [selectedQueueIds, setSelectedQueueIds] = useState([]) + const { user, setting, getSettingValue } = useContext(AuthContext) + const [settings, setSettings] = useState(setting) + + // console.log('USER: ', JSON.stringify(user, null, 6)) + + useEffect(() => { + return () => { + isMounted.current = false + } + }, []) + + useEffect(() => { + setSettings(setting) + }, [setting]) + + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL) + + socket.on('settings', (data) => { + if (data.action === 'update') { + setSettings((prevState) => { + const aux = [...prevState] + const settingIndex = aux.findIndex((s) => s.key === data.setting.key) + aux[settingIndex].value = data.setting.value + return aux + }) + } + }) + + return () => { + socket.disconnect() + } + }, []) + + useEffect(() => { + const fetchPosition = async () => { + if (initialValues) { + setPosition((prevState) => { + return { ...prevState, ...initialValues } + }) + } + + if (!positionId) return + + try { + const { data } = await api.get(`/positions/${positionId}`) + if (isMounted.current) { + setPosition(data) + + if (data?.queues) { + console.log('data.queues: ', data.queues) + const quickQueueIds = data.queues?.map((queue) => queue.id) + setSelectedQueueIds(quickQueueIds) + } + } + } catch (err) { + toastError(err) + } + } + + fetchPosition() + }, [positionId, open, initialValues]) + + const handleClose = () => { + onClose() + setPosition(initialState) + } + + const handleSavePosition = async (values) => { + try { + + if (positionId) { + await api.put(`/positions/${positionId}`, values) + handleClose() + toast.success("Cargo editado com sucesso") + } else { + const { data } = await api.post("/positions", values) + if (onSave) { + onSave(data) + } + handleClose() + toast.success("Cargo salvo com sucesso") + } + + } catch (err) { + toastError(err) + } + } + + return ( +
+ + + {positionId + ? `Editar Cargo` + : `Adicionar Cargo`} + + { + setTimeout(() => { + handleSavePosition(values) + actions.setSubmitting(false) + }, 400) + }} + > + {({ values, errors, touched, isSubmitting }) => ( +
+ +
+ +
+
+ { + ((settings && getSettingValue('quickAnswerByQueue') === 'enabled')) && ( + { + return setSelectedQueueIds(selectedIds) + }} + _queues={user.queues} + /> + ) + } +
+
+ + + + +
+ )} +
+
+
+ ) +} + +export default PositionModal diff --git a/frontend/src/components/QueueModal/index.js b/frontend/src/components/QueueModal/index.js index 3baaa77..eb82ee8 100644 --- a/frontend/src/components/QueueModal/index.js +++ b/frontend/src/components/QueueModal/index.js @@ -1,26 +1,31 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useContext } from "react" -import * as Yup from "yup"; -import { Formik, Form, Field } from "formik"; -import { toast } from "react-toastify"; +import * as Yup from "yup" +import { Formik, Form, Field } from "formik" +import { toast } from "react-toastify" -import { makeStyles } from "@material-ui/core/styles"; -import { green } from "@material-ui/core/colors"; -import Button from "@material-ui/core/Button"; -import TextField from "@material-ui/core/TextField"; -import Dialog from "@material-ui/core/Dialog"; -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 { makeStyles } from "@material-ui/core/styles" +import { green } from "@material-ui/core/colors" +import Button from "@material-ui/core/Button" +import TextField from "@material-ui/core/TextField" +import Dialog from "@material-ui/core/Dialog" +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 { i18n } from "../../translate/i18n" + +import api from "../../services/api" +import toastError from "../../errors/toastError" +import ColorPicker from "../ColorPicker" +import { IconButton, InputAdornment } from "@material-ui/core" +import { Colorize } from "@material-ui/icons" + +import { AuthContext } from '../../context/Auth/AuthContext' +import openSocket from 'socket.io-client' -import { i18n } from "../../translate/i18n"; -import api from "../../services/api"; -import toastError from "../../errors/toastError"; -import ColorPicker from "../ColorPicker"; -import { IconButton, InputAdornment } from "@material-ui/core"; -import { Colorize } from "@material-ui/icons"; const useStyles = makeStyles(theme => ({ root: { @@ -52,7 +57,7 @@ const useStyles = makeStyles(theme => ({ width: 20, height: 20, }, -})); +})) const QueueSchema = Yup.object().shape({ name: Yup.string() @@ -61,63 +66,92 @@ const QueueSchema = Yup.object().shape({ .required("Required"), color: Yup.string().min(3, "Too Short!").max(9, "Too Long!").required(), greetingMessage: Yup.string(), -}); +}) const QueueModal = ({ open, onClose, queueId }) => { - const classes = useStyles(); + const classes = useStyles() const initialState = { name: "", color: "", greetingMessage: "", - cc:"" - }; + farewellMessage: "", + cc: "" + } - const [colorPickerModalOpen, setColorPickerModalOpen] = useState(false); - const [queue, setQueue] = useState(initialState); - const greetingRef = useRef(); + const { user, setting, getSettingValue } = useContext(AuthContext) + const [settings, setSettings] = useState(setting) + + + const [colorPickerModalOpen, setColorPickerModalOpen] = useState(false) + const [queue, setQueue] = useState(initialState) + const greetingRef = useRef() + + useEffect(() => { + setSettings(setting) + }, [setting]) + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL) + + socket.on('settings', (data) => { + if (data.action === 'update') { + setSettings((prevState) => { + const aux = [...prevState] + const settingIndex = aux.findIndex((s) => s.key === data.setting.key) + aux[settingIndex].value = data.setting.value + return aux + }) + } + }) + + return () => { + socket.disconnect() + } + }, []) useEffect(() => { (async () => { - if (!queueId) return; + if (!queueId) return try { - const { data } = await api.get(`/queue/${queueId}`); + const { data } = await api.get(`/queue/${queueId}`) setQueue(prevState => { - return { ...prevState, ...data }; - }); + return { ...prevState, ...data } + }) } catch (err) { - toastError(err); + toastError(err) } - })(); + })() return () => { setQueue({ name: "", color: "", greetingMessage: "", + farewellMessage: "", cc: "" - }); - }; - }, [queueId, open]); + }) + } + }, [queueId, open]) const handleClose = () => { - onClose(); - setQueue(initialState); - }; + onClose() + setQueue(initialState) + } const handleSaveQueue = async values => { try { if (queueId) { - await api.put(`/queue/${queueId}`, values); + await api.put(`/queue/${queueId}`, values) } else { - await api.post("/queue", values); + await api.post("/queue", values) } - toast.success("Queue saved successfully"); - handleClose(); + toast.success("Queue saved successfully") + handleClose() } catch (err) { - toastError(err); + toastError(err) } - }; + } return (
@@ -133,9 +167,9 @@ const QueueModal = ({ open, onClose, queueId }) => { validationSchema={QueueSchema} onSubmit={(values, actions) => { setTimeout(() => { - handleSaveQueue(values); - actions.setSubmitting(false); - }, 400); + handleSaveQueue(values) + actions.setSubmitting(false) + }, 400) }} > {({ touched, errors, isSubmitting, values }) => ( @@ -158,8 +192,8 @@ const QueueModal = ({ open, onClose, queueId }) => { name="color" id="color" onFocus={() => { - setColorPickerModalOpen(true); - greetingRef.current.focus(); + setColorPickerModalOpen(true) + greetingRef.current.focus() }} error={touched.color && Boolean(errors.color)} helperText={touched.color && errors.color} @@ -189,10 +223,10 @@ const QueueModal = ({ open, onClose, queueId }) => { open={colorPickerModalOpen} handleClose={() => setColorPickerModalOpen(false)} onChange={color => { - values.color = color; + values.color = color setQueue(() => { - return { ...values, color }; - }); + return { ...values, color } + }) }} />
@@ -215,6 +249,32 @@ const QueueModal = ({ open, onClose, queueId }) => { margin="dense" />
+ + { + ((settings && getSettingValue('farewellMessageByQueue') === 'enabled')) && ( +
+ +
+ ) + } +
{
- ); -}; + ) +} -export default QueueModal; \ No newline at end of file +export default QueueModal \ No newline at end of file diff --git a/frontend/src/components/QueueSelect/index.js b/frontend/src/components/QueueSelect/index.js index b80164e..552b552 100644 --- a/frontend/src/components/QueueSelect/index.js +++ b/frontend/src/components/QueueSelect/index.js @@ -1,13 +1,13 @@ -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 toastError from "../../errors/toastError"; -import api from "../../services/api"; -import { i18n } from "../../translate/i18n"; +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 toastError from "../../errors/toastError" +import api from "../../services/api" +import { i18n } from "../../translate/i18n" const useStyles = makeStyles(theme => ({ chips: { @@ -17,26 +17,30 @@ const useStyles = makeStyles(theme => ({ chip: { margin: 2, }, -})); +})) -const QueueSelect = ({ selectedQueueIds, onChange }) => { - const classes = useStyles(); - const [queues, setQueues] = useState([]); +const QueueSelect = ({ selectedQueueIds, onChange, _queues = [] }) => { + const classes = useStyles() + + const [queues, setQueues] = useState(_queues) useEffect(() => { + + if (_queues.length > 0) return + (async () => { try { - const { data } = await api.get("/queue"); - setQueues(data); + const { data } = await api.get("/queue") + setQueues(data) } catch (err) { - toastError(err); + toastError(err) } - })(); - }, []); + })() + }, []) const handleChange = e => { - onChange(e.target.value); - }; + onChange(e.target.value) + } return (
@@ -62,7 +66,7 @@ const QueueSelect = ({ selectedQueueIds, onChange }) => {
{selected?.length > 0 && selected.map(id => { - const queue = queues.find(q => q.id === id); + const queue = queues.find(q => q.id === id) return queue ? ( { label={queue.name} className={classes.chip} /> - ) : null; + ) : null })}
)} @@ -84,7 +88,7 @@ const QueueSelect = ({ selectedQueueIds, onChange }) => {
- ); -}; + ) +} -export default QueueSelect; +export default QueueSelect diff --git a/frontend/src/components/QuickAnswersModal/index.js b/frontend/src/components/QuickAnswersModal/index.js index ba100c6..462fb81 100644 --- a/frontend/src/components/QuickAnswersModal/index.js +++ b/frontend/src/components/QuickAnswersModal/index.js @@ -1,8 +1,10 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useContext } from "react" + +import * as Yup from "yup" +import { Formik, Form, Field } from "formik" +import { toast } from "react-toastify" +import openSocket from 'socket.io-client' -import * as Yup from "yup"; -import { Formik, Form, Field } from "formik"; -import { toast } from "react-toastify"; import { makeStyles, @@ -13,12 +15,14 @@ import { DialogContent, DialogTitle, CircularProgress, -} from "@material-ui/core"; -import { green } from "@material-ui/core/colors"; -import { i18n } from "../../translate/i18n"; +} from "@material-ui/core" +import { green } from "@material-ui/core/colors" +import { i18n } from "../../translate/i18n" +import QueueSelect from '../QueueSelect' +import { AuthContext } from '../../context/Auth/AuthContext' -import api from "../../services/api"; -import toastError from "../../errors/toastError"; +import api from "../../services/api" +import toastError from "../../errors/toastError" const useStyles = makeStyles((theme) => ({ root: { @@ -44,7 +48,7 @@ const useStyles = makeStyles((theme) => ({ textQuickAnswerContainer: { width: "100%", }, -})); +})) const QuickAnswerSchema = Yup.object().shape({ shortcut: Yup.string() @@ -55,7 +59,7 @@ const QuickAnswerSchema = Yup.object().shape({ .min(8, "Too Short!") .max(30000, "Too Long!") .required("Required"), -}); +}) const QuickAnswersModal = ({ open, @@ -64,67 +68,105 @@ const QuickAnswersModal = ({ initialValues, onSave, }) => { - const classes = useStyles(); - const isMounted = useRef(true); + const classes = useStyles() + const isMounted = useRef(true) const initialState = { shortcut: "", message: "", - }; + } - const [quickAnswer, setQuickAnswer] = useState(initialState); + const [quickAnswer, setQuickAnswer] = useState(initialState) + const [selectedQueueIds, setSelectedQueueIds] = useState([]) + const { user, setting, getSettingValue } = useContext(AuthContext) + const [settings, setSettings] = useState(setting) + + // console.log('USER: ', JSON.stringify(user, null, 6)) useEffect(() => { return () => { - isMounted.current = false; - }; - }, []); + isMounted.current = false + } + }, []) + + useEffect(() => { + setSettings(setting) + }, [setting]) + + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL) + + socket.on('settings', (data) => { + if (data.action === 'update') { + setSettings((prevState) => { + const aux = [...prevState] + const settingIndex = aux.findIndex((s) => s.key === data.setting.key) + aux[settingIndex].value = data.setting.value + return aux + }) + } + }) + + return () => { + socket.disconnect() + } + }, []) useEffect(() => { const fetchQuickAnswer = async () => { if (initialValues) { setQuickAnswer((prevState) => { - return { ...prevState, ...initialValues }; - }); + return { ...prevState, ...initialValues } + }) } - if (!quickAnswerId) return; + if (!quickAnswerId) return try { - const { data } = await api.get(`/quickAnswers/${quickAnswerId}`); + const { data } = await api.get(`/quickAnswers/${quickAnswerId}`) if (isMounted.current) { - setQuickAnswer(data); + setQuickAnswer(data) + + if (data?.queues) { + console.log('data.queues: ', data.queues) + const quickQueueIds = data.queues?.map((queue) => queue.id) + setSelectedQueueIds(quickQueueIds) + } } } catch (err) { - toastError(err); + toastError(err) } - }; + } - fetchQuickAnswer(); - }, [quickAnswerId, open, initialValues]); + fetchQuickAnswer() + }, [quickAnswerId, open, initialValues]) const handleClose = () => { - onClose(); - setQuickAnswer(initialState); - }; + onClose() + setQuickAnswer(initialState) + } const handleSaveQuickAnswer = async (values) => { try { + + values = { ...values, queueIds: selectedQueueIds } + if (quickAnswerId) { - await api.put(`/quickAnswers/${quickAnswerId}`, values); - handleClose(); + await api.put(`/quickAnswers/${quickAnswerId}`, values) + handleClose() } else { - const { data } = await api.post("/quickAnswers", values); + const { data } = await api.post("/quickAnswers", values) if (onSave) { - onSave(data); + onSave(data) } - handleClose(); + handleClose() } - toast.success(i18n.t("quickAnswersModal.success")); + toast.success(i18n.t("quickAnswersModal.success")) } catch (err) { - toastError(err); + toastError(err) } - }; + } return (
@@ -146,9 +188,9 @@ const QuickAnswersModal = ({ validationSchema={QuickAnswerSchema} onSubmit={(values, actions) => { setTimeout(() => { - handleSaveQuickAnswer(values); - actions.setSubmitting(false); - }, 400); + handleSaveQuickAnswer(values) + actions.setSubmitting(false) + }, 400) }} > {({ values, errors, touched, isSubmitting }) => ( @@ -183,6 +225,19 @@ const QuickAnswersModal = ({ fullWidth />
+
+ { + ((settings && getSettingValue('quickAnswerByQueue') === 'enabled')) && ( + { + return setSelectedQueueIds(selectedIds) + }} + _queues={user.queues} + /> + ) + } +
- ); -}; + ) +} -export default QuickAnswersModal; +export default QuickAnswersModal diff --git a/frontend/src/components/Report/SelectField/index.js b/frontend/src/components/Report/SelectField/index.js index a234526..ef14f72 100644 --- a/frontend/src/components/Report/SelectField/index.js +++ b/frontend/src/components/Report/SelectField/index.js @@ -1,85 +1,76 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react' -import Box from '@mui/material/Box'; -import TextField from '@mui/material/TextField'; +import Box from '@mui/material/Box' +import TextField from '@mui/material/TextField' const SelectTextFields = (props) => { - - // const [currency, setCurrency] = useState(props.emptyField ? '0' : '1'); - - - - const [currency, setCurrency] = useState(props.textBoxFieldSelected ? props.textBoxFieldSelected: '0'); - - // const [currency, setCurrency] = useState(props.textBoxFieldSelected); - - if(!props.textBoxFieldSelected){ - props.currencies.push({ 'value': 0, 'label': ''}) + const [currency, setCurrency] = useState(props.textBoxFieldSelected ? props.textBoxFieldSelected : '0') + if (!props.textBoxFieldSelected) { + props.currencies.push({ 'value': 0, 'label': '' }) } + useEffect(() => { + props.func(currency) - useEffect(()=>{ - - props.func(currency); - - },[currency, props]) + }, [currency, props]) const handleChange = (event) => { - setCurrency(event.target.value); + setCurrency(event.target.value) - }; + } - return ( + return ( - - - - {props.currencies.map((option, index) => ( - - ))} + - + display: 'flex', + flexDirection: 'column', + '& .MuiTextField-root': { m: 1, }, + } + } + noValidate + autoComplete="off" + > + - + {props.currencies.map((option, index) => ( + + ))} - - ); + + + + + + + ) } -export default SelectTextFields +export default SelectTextFields diff --git a/frontend/src/components/StatusChatEndModal/index.js b/frontend/src/components/StatusChatEndModal/index.js new file mode 100644 index 0000000..1c18884 --- /dev/null +++ b/frontend/src/components/StatusChatEndModal/index.js @@ -0,0 +1,260 @@ +import React, { useState, useEffect, useRef, useContext } from "react" + +import * as Yup from "yup" +import { Formik, Form, Field } from "formik" +import { toast } from "react-toastify" +import openSocket from 'socket.io-client' + + +import { + makeStyles, + Button, + TextField, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + CircularProgress, +} from "@material-ui/core" +import { green } from "@material-ui/core/colors" +import { i18n } from "../../translate/i18n" +import QueueSelect from '../QueueSelect' +import { AuthContext } from '../../context/Auth/AuthContext' + +import api from "../../services/api" +import toastError from "../../errors/toastError" + +const useStyles = makeStyles((theme) => ({ + root: { + flexWrap: "wrap", + }, + textField: { + marginRight: theme.spacing(1), + width: "100%", + }, + + btnWrapper: { + position: "relative", + }, + + buttonProgress: { + color: green[500], + position: "absolute", + top: "50%", + left: "50%", + marginTop: -12, + marginLeft: -12, + }, + textQuickAnswerContainer: { + width: "100%", + }, +})) + +const StatusChatEndSchema = Yup.object().shape({ + name: Yup.string() + .min(2, "Too Short!") + .max(40, "Too Long!") + .required("Required"), +}) + +const StatusChatEndModal = ({ + open, + onClose, + statusChatEndId, + initialValues, + onSave, +}) => { + const classes = useStyles() + const isMounted = useRef(true) + + const initialState = { + name: "", + farewellMessage: "", + isDefault: false, + } + + const [statusChatEnd, setStatusChatEnd] = useState(initialState) + const { user, setting, getSettingValue } = useContext(AuthContext) + const [settings, setSettings] = useState(setting) + + // console.log('USER: ', JSON.stringify(user, null, 6)) + + useEffect(() => { + return () => { + isMounted.current = false + } + }, []) + + useEffect(() => { + setSettings(setting) + }, [setting]) + + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL) + + socket.on('settings', (data) => { + if (data.action === 'update') { + setSettings((prevState) => { + const aux = [...prevState] + const settingIndex = aux.findIndex((s) => s.key === data.setting.key) + aux[settingIndex].value = data.setting.value + return aux + }) + } + }) + + return () => { + socket.disconnect() + } + }, []) + + useEffect(() => { + const fetchQuickAnswer = async () => { + if (initialValues) { + setStatusChatEnd((prevState) => { + return { ...prevState, ...initialValues } + }) + } + + if (!statusChatEndId) return + + try { + const { data } = await api.get(`/statusChatEnd/${statusChatEndId}`) + if (isMounted.current) { + setStatusChatEnd(data) + } + } catch (err) { + toastError(err) + } + } + + fetchQuickAnswer() + }, [statusChatEndId, open, initialValues]) + + const handleClose = () => { + onClose() + setStatusChatEnd(initialState) + } + + const handleSaveStatusChatEnd = (values) => { + + (async () => { + try { + + if (statusChatEndId) { + await api.put(`/statusChatEnd/${statusChatEndId}`, values) + handleClose() + toast.success("Status de encerramento editado com sucesso") + + } else { + const { data } = await api.post("/statusChatEnd", values) + if (onSave) { + onSave(data) + } + handleClose() + toast.success("Status de encerramento criado com sucesso") + + } + } catch (err) { + toastError(err) + } + })() + + + } + + return ( +
+ + + {statusChatEndId + ? `Editar Status de encerramento` + : `Adicionar Status de encerramento`} + + { + setTimeout(() => { + handleSaveStatusChatEnd(values) + actions.setSubmitting(false) + }, 400) + }} + > + {({ values, errors, touched, isSubmitting }) => ( +
+ +
+ +
+
+ +
+
+ + + + +
+ )} +
+
+
+ ) +} + +export default StatusChatEndModal diff --git a/frontend/src/components/Ticket/index.js b/frontend/src/components/Ticket/index.js index a88507b..abc5cd3 100644 --- a/frontend/src/components/Ticket/index.js +++ b/frontend/src/components/Ticket/index.js @@ -1,23 +1,23 @@ -import React, { useState, useEffect } from "react"; -import { useParams, useHistory } from "react-router-dom"; +import React, { useState, useEffect } from "react" +import { useParams, useHistory } from "react-router-dom" -import { toast } from "react-toastify"; -import openSocket from "socket.io-client"; -import clsx from "clsx"; +import { toast } from "react-toastify" +import openSocket from "socket.io-client" +import clsx from "clsx" -import { Paper, makeStyles } from "@material-ui/core"; +import { Paper, makeStyles } from "@material-ui/core" -import ContactDrawer from "../ContactDrawer"; -import MessageInput from "../MessageInput/"; -import TicketHeader from "../TicketHeader"; -import TicketInfo from "../TicketInfo"; -import TicketActionButtons from "../TicketActionButtons"; -import MessagesList from "../MessagesList"; -import api from "../../services/api"; -import { ReplyMessageProvider } from "../../context/ReplyingMessage/ReplyingMessageContext"; -import toastError from "../../errors/toastError"; +import ContactDrawer from "../ContactDrawer" +import MessageInput from "../MessageInput/" +import TicketHeader from "../TicketHeader" +import TicketInfo from "../TicketInfo" +import TicketActionButtons from "../TicketActionButtons" +import MessagesList from "../MessagesList" +import api from "../../services/api" +import { ReplyMessageProvider } from "../../context/ReplyingMessage/ReplyingMessageContext" +import toastError from "../../errors/toastError" -const drawerWidth = 320; +const drawerWidth = 320 const useStyles = makeStyles((theme) => ({ root: { @@ -71,88 +71,104 @@ const useStyles = makeStyles((theme) => ({ }), marginRight: 0, }, -})); +})) const Ticket = () => { - const { ticketId } = useParams(); - const history = useHistory(); - const classes = useStyles(); + const { ticketId } = useParams() + const history = useHistory() + const classes = useStyles() - const [drawerOpen, setDrawerOpen] = useState(false); - const [loading, setLoading] = useState(true); - const [contact, setContact] = useState({}); - const [ticket, setTicket] = useState({}); + const [drawerOpen, setDrawerOpen] = useState(false) + const [loading, setLoading] = useState(true) + const [contact, setContact] = useState({}) + const [ticket, setTicket] = useState({}) const [statusChatEnd, setStatusChatEnd] = useState({}) + const [defaultStatusChatEnd, setDefaultStatusChatEnd] = useState('') useEffect(() => { - setLoading(true); + setLoading(true) const delayDebounceFn = setTimeout(() => { const fetchTicket = async () => { try { // maria julia - const { data } = await api.get("/tickets/" + ticketId); + const { data } = await api.get("/tickets/" + ticketId) // setContact(data.contact); // setTicket(data); - setContact(data.contact.contact); - setTicket(data.contact); + setContact(data.contact.contact) + setTicket(data.contact) setStatusChatEnd(data.statusChatEnd) - setLoading(false); + setLoading(false) } catch (err) { - setLoading(false); - toastError(err); + setLoading(false) + toastError(err) } - }; - fetchTicket(); - }, 500); - return () => clearTimeout(delayDebounceFn); - }, [ticketId, history]); + } + fetchTicket() + }, 500) + return () => clearTimeout(delayDebounceFn) + }, [ticketId, history]) useEffect(() => { - const socket = openSocket(process.env.REACT_APP_BACKEND_URL); - socket.on("connect", () => socket.emit("joinChatBox", ticketId)); + (async () => { + try { + + const { data } = await api.get("/statusChatEnd/true") + + setDefaultStatusChatEnd(data?.name?.trim()) + + } catch (err) { + toastError(err) + } + })() + }, []) + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL) + + socket.on("connect", () => socket.emit("joinChatBox", ticketId)) socket.on("ticket", (data) => { if (data.action === "update") { - setTicket(data.ticket); + setTicket(data.ticket) } if (data.action === "delete") { - toast.success("Ticket deleted sucessfully."); - history.push("/tickets"); + toast.success("Ticket deleted sucessfully.") + history.push("/tickets") } - }); + }) socket.on("contact", (data) => { if (data.action === "update") { setContact((prevState) => { if (prevState.id === data.contact?.id) { - return { ...prevState, ...data.contact }; + return { ...prevState, ...data.contact } } - return prevState; - }); + return prevState + }) } - }); + }) return () => { - socket.disconnect(); - }; - }, [ticketId, history]); + socket.disconnect() + } + }, [ticketId, history]) const handleDrawerOpen = () => { - setDrawerOpen(true); - }; + setDrawerOpen(true) + } const handleDrawerClose = () => { - setDrawerOpen(false); - }; + setDrawerOpen(false) + } return (
@@ -171,8 +187,8 @@ const Ticket = () => { onClick={handleDrawerOpen} />
-
- +
+
@@ -190,7 +206,7 @@ const Ticket = () => { loading={loading} />
- ); -}; + ) +} -export default Ticket; +export default Ticket diff --git a/frontend/src/components/TicketActionButtons/index.js b/frontend/src/components/TicketActionButtons/index.js index 8382add..e23e5c8 100644 --- a/frontend/src/components/TicketActionButtons/index.js +++ b/frontend/src/components/TicketActionButtons/index.js @@ -1,21 +1,21 @@ -import React, { useContext, useState } from "react"; -import { useHistory } from "react-router-dom"; +import React, { useContext, useState } from "react" +import { useHistory } from "react-router-dom" -import { makeStyles } from "@material-ui/core/styles"; -import { IconButton } from "@material-ui/core"; -import { MoreVert, Replay } from "@material-ui/icons"; +import { makeStyles } from "@material-ui/core/styles" +import { IconButton } from "@material-ui/core" +import { MoreVert, Replay } from "@material-ui/icons" -import { i18n } from "../../translate/i18n"; -import api from "../../services/api"; -import TicketOptionsMenu from "../TicketOptionsMenu"; -import ButtonWithSpinner from "../ButtonWithSpinner"; -import toastError from "../../errors/toastError"; -import { AuthContext } from "../../context/Auth/AuthContext"; +import { i18n } from "../../translate/i18n" +import api from "../../services/api" +import TicketOptionsMenu from "../TicketOptionsMenu" +import ButtonWithSpinner from "../ButtonWithSpinner" +import toastError from "../../errors/toastError" +import { AuthContext } from "../../context/Auth/AuthContext" -import Modal from "../ChatEnd/ModalChatEnd"; -import { render } from '@testing-library/react'; +import Modal from "../ChatEnd/ModalChatEnd" +import { render } from '@testing-library/react' -import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption"; +import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption" const useStyles = makeStyles(theme => ({ actionButtons: { @@ -27,15 +27,15 @@ const useStyles = makeStyles(theme => ({ margin: theme.spacing(1), }, }, -})); +})) -const TicketActionButtons = ({ ticket, statusChatEnd }) => { - const classes = useStyles(); - const history = useHistory(); +const TicketActionButtons = ({ ticket, statusChatEnd, defaultStatusChatEnd }) => { + const classes = useStyles() + const history = useHistory() + + const [anchorEl, setAnchorEl] = useState(null) + const [loading, setLoading] = useState(false) - const [anchorEl, setAnchorEl] = useState(null); - const [loading, setLoading] = useState(false); - // const [useDialogflow, setUseDialogflow] = useState(ticket.contact.useDialogflow); // const [/*useDialogflow*/, setUseDialogflow] = useState(() => { @@ -47,25 +47,25 @@ const TicketActionButtons = ({ ticket, statusChatEnd }) => { // } // }); - const ticketOptionsMenuOpen = Boolean(anchorEl); - const { user } = useContext(AuthContext); + const ticketOptionsMenuOpen = Boolean(anchorEl) + const { user } = useContext(AuthContext) - const { tabOption, setTabOption } = useContext(TabTicketContext); + const { tabOption, setTabOption } = useContext(TabTicketContext) const handleOpenTicketOptionsMenu = e => { - setAnchorEl(e.currentTarget); - }; + setAnchorEl(e.currentTarget) + } const handleCloseTicketOptionsMenu = e => { - setAnchorEl(null); - }; + setAnchorEl(null) + } const chatEndVal = (data) => { if (data) { - data = { ...data, 'ticketId': ticket.id } + data = { ...data, 'ticketId': ticket.id } handleUpdateTicketStatus(null, "closed", user?.id, data) @@ -75,20 +75,20 @@ const TicketActionButtons = ({ ticket, statusChatEnd }) => { const handleModal = (/*status, userId*/) => { - render() - }; + } const handleUpdateTicketStatus = async (e, status, userId, schedulingData = {}) => { - setLoading(true); + setLoading(true) try { if (status === 'closed') { @@ -101,7 +101,7 @@ const TicketActionButtons = ({ ticket, statusChatEnd }) => { status: status, userId: userId || null, schedulingNotifyData: JSON.stringify(schedulingData) - }); + }) } else { @@ -113,24 +113,24 @@ const TicketActionButtons = ({ ticket, statusChatEnd }) => { await api.put(`/tickets/${ticket.id}`, { status: status, userId: userId || null - }); + }) } - setLoading(false); + setLoading(false) if (status === "open") { - history.push(`/tickets/${ticket.id}`); + history.push(`/tickets/${ticket.id}`) } else { - history.push("/tickets"); + history.push("/tickets") } } catch (err) { - setLoading(false); - toastError(err); + setLoading(false) + toastError(err) } - }; + } // const handleContactToggleUseDialogflow = async () => { // setLoading(true); @@ -207,7 +207,7 @@ const TicketActionButtons = ({ ticket, statusChatEnd }) => { )} - ); -}; + ) +} -export default TicketActionButtons; +export default TicketActionButtons diff --git a/frontend/src/components/UserModal/index.js b/frontend/src/components/UserModal/index.js index cd568e8..51d660d 100644 --- a/frontend/src/components/UserModal/index.js +++ b/frontend/src/components/UserModal/index.js @@ -1,8 +1,8 @@ -import React, { useState, useEffect, useContext } from "react"; +import React, { useState, useEffect, useContext } from "react" -import * as Yup from "yup"; -import { Formik, Form, Field } from "formik"; -import { toast } from "react-toastify"; +import * as Yup from "yup" +import { Formik, Form, Field } from "formik" +import { toast } from "react-toastify" import { Button, @@ -18,20 +18,20 @@ import { TextField, InputAdornment, IconButton - } from '@material-ui/core'; +} from '@material-ui/core' -import { Visibility, VisibilityOff } from '@material-ui/icons'; +import { Visibility, VisibilityOff } from '@material-ui/icons' -import { makeStyles } from "@material-ui/core/styles"; -import { green } from "@material-ui/core/colors"; +import { makeStyles } from "@material-ui/core/styles" +import { green } from "@material-ui/core/colors" -import { i18n } from "../../translate/i18n"; +import { i18n } from "../../translate/i18n" -import api from "../../services/api"; -import toastError from "../../errors/toastError"; -import QueueSelect from "../QueueSelect"; -import { AuthContext } from "../../context/Auth/AuthContext"; -import { Can } from "../Can"; +import api from "../../services/api" +import toastError from "../../errors/toastError" +import QueueSelect from "../QueueSelect" +import { AuthContext } from "../../context/Auth/AuthContext" +import { Can } from "../Can" const useStyles = makeStyles(theme => ({ root: { @@ -61,7 +61,7 @@ const useStyles = makeStyles(theme => ({ margin: theme.spacing(1), minWidth: 120, }, -})); +})) const UserSchema = Yup.object().shape({ name: Yup.string() @@ -71,60 +71,79 @@ const UserSchema = Yup.object().shape({ password: Yup.string().min(5, "Too Short!").max(50, "Too Long!"), email: Yup.string().min(2, "Too Short!") - .max(50, "Too Long!") - .required("Required"), - - // email: Yup.string().email("Invalid email").required("Required"), -}); + .max(50, "Too Long!") + .required("Required"), -const UserModal = ({ open, onClose, userId }) => { - const classes = useStyles(); + // email: Yup.string().email("Invalid email").required("Required"), +}) + +const UserModal = ({ open, onClose, userId, }) => { + const classes = useStyles() const initialState = { name: "", email: "", password: "", positionCompany: "", - position: "", profile: "user", - }; + } - const { user: loggedInUser } = useContext(AuthContext); - - const [user, setUser] = useState(initialState); - const [selectedQueueIds, setSelectedQueueIds] = useState([]); - const [showPassword, setShowPassword] = useState(false); + const { user: loggedInUser } = useContext(AuthContext) + const [user, setUser] = useState(initialState) + const [selectedQueueIds, setSelectedQueueIds] = useState([]) + const [showPassword, setShowPassword] = useState(false) + const [positions, setPositions] = useState([]) + const [selectedPosition, setSelectedPosition] = useState('') useEffect(() => { const fetchUser = async () => { - if (!userId) return; + setSelectedPosition('') + if (!userId) return try { - const { data } = await api.get(`/users/${userId}`); + const { data } = await api.get(`/users/${userId}`) setUser(prevState => { - return { ...prevState, ...data }; - }); - const userQueueIds = data.queues?.map(queue => queue.id); - setSelectedQueueIds(userQueueIds); - } catch (err) { - toastError(err); - } - }; + return { ...prevState, ...data } + }) + const userQueueIds = data.queues?.map(queue => queue.id) + setSelectedQueueIds(userQueueIds) - fetchUser(); - }, [userId, open]); + if (data?.positionId) + setSelectedPosition(data.positionId) + else + setSelectedPosition('') + } catch (err) { + toastError(err) + } + } + + fetchUser() + }, [userId, open]) + + useEffect(() => { + const fetchUser = async () => { + try { + const { data } = await api.get(`/positions`) + setPositions(data.positions) + } catch (err) { + toastError(err) + } + } + + fetchUser() + }, [userId, open]) const handleClose = () => { - onClose(); - setUser(initialState); - }; + onClose() + setUser(initialState) + } const handleSaveUser = async values => { - const userData = { ...values, queueIds: selectedQueueIds }; + const userData = { ...values, queueIds: selectedQueueIds, positionId: selectedPosition } try { if (userId) { - const user = await api.put(`/users/${userId}`, userData); + const user = await api.put(`/users/${userId}`, userData) console.log('USER: ', user.data) @@ -149,14 +168,14 @@ const UserModal = ({ open, onClose, userId }) => { } else { - await api.post("/users", userData); + await api.post("/users", userData) } - toast.success(i18n.t("userModal.success")); + toast.success(i18n.t("userModal.success")) } catch (err) { - toastError(err); + toastError(err) } - handleClose(); - }; + handleClose() + } return (
@@ -178,9 +197,9 @@ const UserModal = ({ open, onClose, userId }) => { validationSchema={UserSchema} onSubmit={(values, actions) => { setTimeout(() => { - handleSaveUser(values); - actions.setSubmitting(false); - }, 400); + handleSaveUser(values) + actions.setSubmitting(false) + }, 400) }} > {({ touched, errors, isSubmitting }) => ( @@ -208,41 +227,41 @@ const UserModal = ({ open, onClose, userId }) => { helperText={touched.password && errors.password} type={showPassword ? 'text' : 'password'} InputProps={{ - endAdornment: ( - - setShowPassword((e) => !e)} - > - {showPassword ? : } - - - ) + endAdornment: ( + + setShowPassword((e) => !e)} + > + {showPassword ? : } + + + ) }} fullWidth />
- -
- +
+ { />
- + { /> )} /> + +
+ + {"Cargo"} + + +
+
- ); -}; + ) +} -export default UserModal; +export default UserModal diff --git a/frontend/src/context/Auth/AuthContext.js b/frontend/src/context/Auth/AuthContext.js index 5368f33..fbeda88 100644 --- a/frontend/src/context/Auth/AuthContext.js +++ b/frontend/src/context/Auth/AuthContext.js @@ -5,14 +5,14 @@ import useAuth from '../../hooks/useAuth.js' const AuthContext = createContext() const AuthProvider = ({ children }) => { - const { loading, user, isAuth, handleLogin, handleLogout, setSetting } = + const { loading, user, isAuth, handleLogin, handleLogout, setSetting, setting, getSettingValue } = useAuth() //{ return ( {children} diff --git a/frontend/src/context/Setting/SettingContext.js b/frontend/src/context/Setting/SettingContext.js new file mode 100644 index 0000000..c3a464c --- /dev/null +++ b/frontend/src/context/Setting/SettingContext.js @@ -0,0 +1,19 @@ +import React, { createContext } from 'react' + +import useAuth from '../../hooks/useAuth.js/index.js' + +const SettingContext = createContext() + +const SettingProvider = ({ children }) => { + const { loading, user, isAuth, handleLogin, handleLogout, setSetting } = useAuth() + + return ( + + {children} + + ) +} + +export { SettingContext, SettingProvider } diff --git a/frontend/src/hooks/useAuth.js/index.js b/frontend/src/hooks/useAuth.js/index.js index 80db7f4..e2da363 100644 --- a/frontend/src/hooks/useAuth.js/index.js +++ b/frontend/src/hooks/useAuth.js/index.js @@ -16,6 +16,11 @@ const useAuth = () => { const [setting, setSetting] = useState({}) + const getSettingValue = (key) => { + return setting?.find((s) => s?.key === key)?.value + } + + api.interceptors.request.use( (config) => { const token = localStorage.getItem('token') @@ -153,6 +158,7 @@ const useAuth = () => { handleLogout, setting, setSetting, + getSettingValue } } diff --git a/frontend/src/layout/MainListItems.js b/frontend/src/layout/MainListItems.js index 1e7611c..03339d2 100644 --- a/frontend/src/layout/MainListItems.js +++ b/frontend/src/layout/MainListItems.js @@ -24,6 +24,8 @@ import PeopleAltOutlinedIcon from '@material-ui/icons/PeopleAltOutlined' import ContactPhoneOutlinedIcon from '@material-ui/icons/ContactPhoneOutlined' import AccountTreeOutlinedIcon from '@material-ui/icons/AccountTreeOutlined' import QuestionAnswerOutlinedIcon from '@material-ui/icons/QuestionAnswerOutlined' +import RateReviewOutlinedIcon from '@material-ui/icons/RateReviewOutlined' +import PlaylistAddIcon from '@material-ui/icons/PlaylistAdd' import { i18n } from '../translate/i18n' import { WhatsAppsContext } from '../context/WhatsApp/WhatsAppsContext' @@ -168,6 +170,18 @@ const MainListItems = (props) => { {i18n.t("mainDrawer.listItems.administration")} + } + /> + + } + /> + { return value } - useEffect(() => { + useEffect(() => { const delayDebounceFn = setTimeout(() => { @@ -195,7 +195,7 @@ const Contacts = () => { } }) - if (insertOnQueue && insertOnQueue.data) { + if (insertOnQueue && insertOnQueue.data) { setZipFile(insertOnQueue.data.app.file) setOnQueueProcessStatus(insertOnQueue.data.app.status) @@ -243,8 +243,7 @@ const Contacts = () => { return } - const { data } = await api.get("/contacts/", { params: { searchParam, pageNumber }, }) - + const { data } = await api.get("/contacts/", { params: { searchParam, pageNumber, userId: user.id }, }) dispatch({ type: "LOAD_CONTACTS", payload: data.contacts }) setHasMore(data.hasMore) @@ -342,14 +341,14 @@ const Contacts = () => { // if (isMounted.current) setLoading(false) }, [history]) - const handleOpenCreateTicketModal = (contactId) => { + const handleOpenCreateTicketModal = (contactId) => { setSelectedContactId(contactId) - if (getSettingValue('whatsaAppCloudApi') === 'disabled' && user?.queues?.length === 1){ + if (getSettingValue('whatsaAppCloudApi') === 'disabled' && user?.queues?.length === 1) { handleSaveTicketOneQueue(contactId, user.id, user.queues[0].id) } - else{ + else { setIsCreateTicketModalOpen(true) } @@ -359,8 +358,8 @@ const Contacts = () => { const handleCloseCreateTicketModal = () => { setIsCreateTicketModalOpen(false) - } - + } + const hadleEditContact = (contactId) => { setSelectedContactId(contactId) @@ -537,7 +536,7 @@ const Contacts = () => { open={contactModalOpen} onClose={handleCloseContactModal} aria-labelledby="form-dialog-title" - contactId={selectedContactId} + contactId={selectedContactId} > { + if (action.type === "LOAD_POSITIONS") { + const positions = action.payload + const newPositions = [] + + positions.forEach((position) => { + const positionIndex = state.findIndex((q) => q.id === position.id) + if (positionIndex !== -1) { + state[positionIndex] = position + } else { + newPositions.push(position) + } + }) + + return [...state, ...newPositions] + } + + if (action.type === "UPDATE_POSITIONS") { + const position = action.payload + const positionIndex = state.findIndex((q) => q.id === position.id) + + if (positionIndex !== -1) { + state[positionIndex] = position + return [...state] + } else { + return [position, ...state] + } + } + + if (action.type === "DELETE_POSITIONS") { + const positionId = action.payload + + const positionIndex = state.findIndex((q) => q.id === positionId) + if (positionIndex !== -1) { + state.splice(positionIndex, 1) + } + return [...state] + } + + if (action.type === "RESET") { + return [] + } +} + +const useStyles = makeStyles((theme) => ({ + mainPaper: { + flex: 1, + padding: theme.spacing(1), + overflowY: "scroll", + ...theme.scrollbarStyles, + }, +})) + +const Position = () => { + const classes = useStyles() + + const [loading, setLoading] = useState(false) + const [pageNumber, setPageNumber] = useState(1) + const [searchParam, setSearchParam] = useState("") + const [positions, dispatch] = useReducer(reducer, []) + const [selectedPosition, setSelectedPosition] = useState(null) + const [positionModalOpen, setPositionModalOpen] = useState(false) + const [deletingPosition, setDeletingPosition] = useState(null) + const [confirmModalOpen, setConfirmModalOpen] = useState(false) + const [hasMore, setHasMore] = useState(false) + const { user, } = useContext(AuthContext) + + + useEffect(() => { + dispatch({ type: "RESET" }) + setPageNumber(1) + }, [searchParam]) + + useEffect(() => { + setLoading(true) + const delayDebounceFn = setTimeout(() => { + const fetchPositions = async () => { + try { + const { data } = await api.get("/positions/", { + params: { searchParam, pageNumber }, + }) + dispatch({ type: "LOAD_POSITIONS", payload: data.positions }) + setHasMore(data.hasMore) + setLoading(false) + } catch (err) { + toastError(err) + } + } + fetchPositions() + }, 500) + return () => clearTimeout(delayDebounceFn) + }, [searchParam, pageNumber]) + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL) + + socket.on("position", (data) => { + if (data.action === "update" || data.action === "create") { + dispatch({ type: "UPDATE_POSITIONS", payload: data.position }) + } + + if (data.action === "delete") { + dispatch({ + type: "DELETE_POSITIONS", + payload: +data.positionId, + }) + } + }) + + return () => { + socket.disconnect() + } + }, []) + + const handleSearch = (event) => { + setSearchParam(event.target.value.toLowerCase()) + } + + const handleOpenPositionModal = () => { + setSelectedPosition(null) + setPositionModalOpen(true) + } + + const handleClosePositionModal = () => { + setSelectedPosition(null) + setPositionModalOpen(false) + } + + const handleEditPosition = (position) => { + setSelectedPosition(position) + setPositionModalOpen(true) + } + + const handleDeletePosition = async (positionId) => { + try { + await api.delete(`/positions/${positionId}`) + toast.success("Cargo deletado com sucesso") + } catch (err) { + toastError(err) + } + setDeletingPosition(null) + setSearchParam("") + setPageNumber(1) + } + + const loadMore = () => { + setPageNumber((prevState) => prevState + 1) + } + + const handleScroll = (e) => { + if (!hasMore || loading) return + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget + if (scrollHeight - (scrollTop + 100) < clientHeight) { + loadMore() + } + } + + return ( + + handleDeletePosition(deletingPosition.id)} + > + {i18n.t("quickAnswers.confirmationModal.deleteMessage")} + + + + {"Cargo"} + + + + + ), + }} + /> + + + + + + + + + Cargo + + + {i18n.t("quickAnswers.table.actions")} + + + + + <> + {positions.map((position) => ( + + {position.name} + + handleEditPosition(position)} + > + + + + { + setConfirmModalOpen(true) + setDeletingPosition(position) + }} + > + + + + + ))} + {loading && } + + +
+
+
+ ) +} + +export default Position diff --git a/frontend/src/pages/Queues/index.js b/frontend/src/pages/Queues/index.js index 570c3f2..a2ffea1 100644 --- a/frontend/src/pages/Queues/index.js +++ b/frontend/src/pages/Queues/index.js @@ -91,7 +91,7 @@ const reducer = (state, action) => { const Queues = () => { const classes = useStyles() - const { user } = useContext(AuthContext) + const { user, setting, getSettingValue } = useContext(AuthContext) const [queues, dispatch] = useReducer(reducer, []) const [loading, setLoading] = useState(false) @@ -102,6 +102,11 @@ const Queues = () => { const [settings, setSettings] = useState([]) + + useEffect(() => { + setSettings(setting) + }, [setting]) + useEffect(() => { ; (async () => { setLoading(true) @@ -115,25 +120,7 @@ const Queues = () => { setLoading(false) } })() - }, []) - - useEffect(() => { - const fetchSession = async () => { - try { - const { data } = await api.get('/settings') - setSettings(data.settings) - } catch (err) { - toastError(err) - } - } - fetchSession() - }, []) - - const getSettingValue = (key) => { - const { value } = settings.find((s) => s.key === key) - - return value - } + }, []) useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL) diff --git a/frontend/src/pages/QuickAnswers/index.js b/frontend/src/pages/QuickAnswers/index.js index 87958ea..5a71233 100644 --- a/frontend/src/pages/QuickAnswers/index.js +++ b/frontend/src/pages/QuickAnswers/index.js @@ -1,5 +1,5 @@ -import React, { useState, useEffect, useReducer } from "react"; -import openSocket from "socket.io-client"; +import React, { useState, useContext, useEffect, useReducer } from "react" +import openSocket from "socket.io-client" import { Button, @@ -13,66 +13,67 @@ import { TableRow, InputAdornment, TextField, -} from "@material-ui/core"; -import { Edit, DeleteOutline } from "@material-ui/icons"; -import SearchIcon from "@material-ui/icons/Search"; +} from "@material-ui/core" +import { Edit, DeleteOutline } from "@material-ui/icons" +import SearchIcon from "@material-ui/icons/Search" -import MainContainer from "../../components/MainContainer"; -import MainHeader from "../../components/MainHeader"; -import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper"; -import Title from "../../components/Title"; +import MainContainer from "../../components/MainContainer" +import MainHeader from "../../components/MainHeader" +import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper" +import Title from "../../components/Title" -import api from "../../services/api"; -import { i18n } from "../../translate/i18n"; -import TableRowSkeleton from "../../components/TableRowSkeleton"; -import QuickAnswersModal from "../../components/QuickAnswersModal"; -import ConfirmationModal from "../../components/ConfirmationModal"; -import { toast } from "react-toastify"; -import toastError from "../../errors/toastError"; +import api from "../../services/api" +import { i18n } from "../../translate/i18n" +import TableRowSkeleton from "../../components/TableRowSkeleton" +import QuickAnswersModal from "../../components/QuickAnswersModal" +import ConfirmationModal from "../../components/ConfirmationModal" +import { toast } from "react-toastify" +import toastError from "../../errors/toastError" +import { AuthContext } from '../../context/Auth/AuthContext' const reducer = (state, action) => { if (action.type === "LOAD_QUICK_ANSWERS") { - const quickAnswers = action.payload; - const newQuickAnswers = []; + const quickAnswers = action.payload + const newQuickAnswers = [] quickAnswers.forEach((quickAnswer) => { - const quickAnswerIndex = state.findIndex((q) => q.id === quickAnswer.id); + const quickAnswerIndex = state.findIndex((q) => q.id === quickAnswer.id) if (quickAnswerIndex !== -1) { - state[quickAnswerIndex] = quickAnswer; + state[quickAnswerIndex] = quickAnswer } else { - newQuickAnswers.push(quickAnswer); + newQuickAnswers.push(quickAnswer) } - }); + }) - return [...state, ...newQuickAnswers]; + return [...state, ...newQuickAnswers] } if (action.type === "UPDATE_QUICK_ANSWERS") { - const quickAnswer = action.payload; - const quickAnswerIndex = state.findIndex((q) => q.id === quickAnswer.id); + const quickAnswer = action.payload + const quickAnswerIndex = state.findIndex((q) => q.id === quickAnswer.id) if (quickAnswerIndex !== -1) { - state[quickAnswerIndex] = quickAnswer; - return [...state]; + state[quickAnswerIndex] = quickAnswer + return [...state] } else { - return [quickAnswer, ...state]; + return [quickAnswer, ...state] } } if (action.type === "DELETE_QUICK_ANSWERS") { - const quickAnswerId = action.payload; + const quickAnswerId = action.payload - const quickAnswerIndex = state.findIndex((q) => q.id === quickAnswerId); + const quickAnswerIndex = state.findIndex((q) => q.id === quickAnswerId) if (quickAnswerIndex !== -1) { - state.splice(quickAnswerIndex, 1); + state.splice(quickAnswerIndex, 1) } - return [...state]; + return [...state] } if (action.type === "RESET") { - return []; + return [] } -}; +} const useStyles = makeStyles((theme) => ({ mainPaper: { @@ -81,117 +82,118 @@ const useStyles = makeStyles((theme) => ({ overflowY: "scroll", ...theme.scrollbarStyles, }, -})); +})) const QuickAnswers = () => { - const classes = useStyles(); + const classes = useStyles() + + const [loading, setLoading] = useState(false) + const [pageNumber, setPageNumber] = useState(1) + const [searchParam, setSearchParam] = useState("") + const [quickAnswers, dispatch] = useReducer(reducer, []) + const [selectedQuickAnswers, setSelectedQuickAnswers] = useState(null) + const [quickAnswersModalOpen, setQuickAnswersModalOpen] = useState(false) + const [deletingQuickAnswers, setDeletingQuickAnswers] = useState(null) + const [confirmModalOpen, setConfirmModalOpen] = useState(false) + const [hasMore, setHasMore] = useState(false) + const { user, } = useContext(AuthContext) - const [loading, setLoading] = useState(false); - const [pageNumber, setPageNumber] = useState(1); - const [searchParam, setSearchParam] = useState(""); - const [quickAnswers, dispatch] = useReducer(reducer, []); - const [selectedQuickAnswers, setSelectedQuickAnswers] = useState(null); - const [quickAnswersModalOpen, setQuickAnswersModalOpen] = useState(false); - const [deletingQuickAnswers, setDeletingQuickAnswers] = useState(null); - const [confirmModalOpen, setConfirmModalOpen] = useState(false); - const [hasMore, setHasMore] = useState(false); useEffect(() => { - dispatch({ type: "RESET" }); - setPageNumber(1); - }, [searchParam]); + dispatch({ type: "RESET" }) + setPageNumber(1) + }, [searchParam]) useEffect(() => { - setLoading(true); + setLoading(true) const delayDebounceFn = setTimeout(() => { const fetchQuickAnswers = async () => { try { const { data } = await api.get("/quickAnswers/", { - params: { searchParam, pageNumber }, - }); - dispatch({ type: "LOAD_QUICK_ANSWERS", payload: data.quickAnswers }); - setHasMore(data.hasMore); - setLoading(false); + params: { searchParam, pageNumber, userId: user.id }, + }) + dispatch({ type: "LOAD_QUICK_ANSWERS", payload: data.quickAnswers }) + setHasMore(data.hasMore) + setLoading(false) } catch (err) { - toastError(err); + toastError(err) } - }; - fetchQuickAnswers(); - }, 500); - return () => clearTimeout(delayDebounceFn); - }, [searchParam, pageNumber]); + } + fetchQuickAnswers() + }, 500) + return () => clearTimeout(delayDebounceFn) + }, [searchParam, pageNumber]) useEffect(() => { - const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + const socket = openSocket(process.env.REACT_APP_BACKEND_URL) socket.on("quickAnswer", (data) => { if (data.action === "update" || data.action === "create") { - dispatch({ type: "UPDATE_QUICK_ANSWERS", payload: data.quickAnswer }); + dispatch({ type: "UPDATE_QUICK_ANSWERS", payload: data.quickAnswer }) } if (data.action === "delete") { dispatch({ type: "DELETE_QUICK_ANSWERS", payload: +data.quickAnswerId, - }); + }) } - }); + }) return () => { - socket.disconnect(); - }; - }, []); + socket.disconnect() + } + }, []) const handleSearch = (event) => { - setSearchParam(event.target.value.toLowerCase()); - }; + setSearchParam(event.target.value.toLowerCase()) + } const handleOpenQuickAnswersModal = () => { - setSelectedQuickAnswers(null); - setQuickAnswersModalOpen(true); - }; + setSelectedQuickAnswers(null) + setQuickAnswersModalOpen(true) + } const handleCloseQuickAnswersModal = () => { - setSelectedQuickAnswers(null); - setQuickAnswersModalOpen(false); - }; + setSelectedQuickAnswers(null) + setQuickAnswersModalOpen(false) + } const handleEditQuickAnswers = (quickAnswer) => { - setSelectedQuickAnswers(quickAnswer); - setQuickAnswersModalOpen(true); - }; + setSelectedQuickAnswers(quickAnswer) + setQuickAnswersModalOpen(true) + } const handleDeleteQuickAnswers = async (quickAnswerId) => { try { - await api.delete(`/quickAnswers/${quickAnswerId}`); - toast.success(i18n.t("quickAnswers.toasts.deleted")); + await api.delete(`/quickAnswers/${quickAnswerId}`) + toast.success(i18n.t("quickAnswers.toasts.deleted")) } catch (err) { - toastError(err); + toastError(err) } - setDeletingQuickAnswers(null); - setSearchParam(""); - setPageNumber(1); - }; + setDeletingQuickAnswers(null) + setSearchParam("") + setPageNumber(1) + } const loadMore = () => { - setPageNumber((prevState) => prevState + 1); - }; + setPageNumber((prevState) => prevState + 1) + } const handleScroll = (e) => { - if (!hasMore || loading) return; - const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + if (!hasMore || loading) return + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget if (scrollHeight - (scrollTop + 100) < clientHeight) { - loadMore(); + loadMore() } - }; + } return ( { { - setConfirmModalOpen(true); - setDeletingQuickAnswers(quickAnswer); + setConfirmModalOpen(true) + setDeletingQuickAnswers(quickAnswer) }} > @@ -282,7 +284,7 @@ const QuickAnswers = () => { - ); -}; + ) +} -export default QuickAnswers; +export default QuickAnswers diff --git a/frontend/src/pages/SchedulesReminder/index.js b/frontend/src/pages/SchedulesReminder/index.js index c26d8dd..78ac312 100644 --- a/frontend/src/pages/SchedulesReminder/index.js +++ b/frontend/src/pages/SchedulesReminder/index.js @@ -1,43 +1,43 @@ -import React, { useState, useEffect, useReducer } from "react"; -import MainContainer from "../../components/MainContainer"; -import api from "../../services/api"; +import React, { useState, useEffect, useReducer } from "react" +import MainContainer from "../../components/MainContainer" +import api from "../../services/api" //import { data } from '../../components/Report/MTable/data'; import DatePicker1 from '../../components/Report/DatePicker' import DatePicker2 from '../../components/Report/DatePicker' //import { Button } from "@material-ui/core"; -import PropTypes from 'prop-types'; -import Box from '@mui/material/Box'; +import PropTypes from 'prop-types' +import Box from '@mui/material/Box' -import SearchIcon from "@material-ui/icons/Search"; -import TextField from "@material-ui/core/TextField"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import Button from "@material-ui/core/Button"; +import SearchIcon from "@material-ui/icons/Search" +import TextField from "@material-ui/core/TextField" +import InputAdornment from "@material-ui/core/InputAdornment" +import Button from "@material-ui/core/Button" -import MaterialTable from 'material-table'; +import MaterialTable from 'material-table' -import Delete from '@material-ui/icons/Delete'; -import Edit from '@material-ui/icons/Edit'; +import Delete from '@material-ui/icons/Delete' +import Edit from '@material-ui/icons/Edit' -import { render } from '@testing-library/react'; +import { render } from '@testing-library/react' // import Modal from "../../../..ChatEnd/ModalChatEnd"; -import Modal from "../../components/ModalUpdateScheduleReminder"; +import Modal from "../../components/ModalUpdateScheduleReminder" -import openSocket from "socket.io-client"; +import openSocket from "socket.io-client" -import { toast } from "react-toastify"; -import toastError from "../../errors/toastError"; -import ConfirmationModal from "../../components/ConfirmationModal"; +import { toast } from "react-toastify" +import toastError from "../../errors/toastError" +import ConfirmationModal from "../../components/ConfirmationModal" -import { i18n } from "../../translate/i18n"; +import { i18n } from "../../translate/i18n" const reducerQ = (state, action) => { @@ -82,14 +82,14 @@ const reducerQ = (state, action) => { if (action.type === "DELETE_SCHEDULING") { - const scheduleId = action.payload; + const scheduleId = action.payload - const scheduleIndex = state.findIndex((u) => u.id === scheduleId); + const scheduleIndex = state.findIndex((u) => u.id === scheduleId) if (scheduleIndex !== -1) { - state.splice(scheduleIndex, 1); + state.splice(scheduleIndex, 1) } - return [...state]; + return [...state] } @@ -105,7 +105,7 @@ const reducerQ = (state, action) => { if (action.type === "RESET") { - return []; + return [] } } @@ -139,7 +139,7 @@ const reducerQ = (state, action) => { function Item(props) { - const { sx, ...other } = props; + const { sx, ...other } = props return ( - ); + ) } Item.propTypes = { @@ -168,7 +168,7 @@ Item.propTypes = { PropTypes.func, PropTypes.object, ]), -}; +} @@ -179,23 +179,23 @@ const SchedulesReminder = () => { //-------- - const [searchParam] = useState(""); - const [loading, setLoading] = useState(null); + const [searchParam] = useState("") + const [loading, setLoading] = useState(null) //const [hasMore, setHasMore] = useState(false); - const [pageNumber, setPageNumber] = useState(1); + const [pageNumber, setPageNumber] = useState(1) // const [users, dispatch] = useReducer(reducer, []); //const [columns, setColums] = useState([]) const [startDate, setDatePicker1] = useState(new Date()) const [endDate, setDatePicker2] = useState(new Date()) const [query, dispatchQ] = useReducer(reducerQ, []) - const [contactNumber, setContactNumber] = useState(""); + const [contactNumber, setContactNumber] = useState("") const [resetChild, setReset] = useState(false) - const [selectedSchedule, setSelectedSchedule] = useState(null); - const [confirmModalOpen, setConfirmModalOpen] = useState(false); - const [dataRows, setData] = useState([]); + const [selectedSchedule, setSelectedSchedule] = useState(null) + const [confirmModalOpen, setConfirmModalOpen] = useState(false) + const [dataRows, setData] = useState([]) const [statusEndChat, setStatusEndChat] = useState(null) @@ -204,13 +204,13 @@ const SchedulesReminder = () => { useEffect(() => { - const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + const socket = openSocket(process.env.REACT_APP_BACKEND_URL) socket.on("schedulingNotify", (data) => { - setLoading(true); + setLoading(true) // if (data.action === "update" || data.action === "create") { @@ -221,27 +221,27 @@ const SchedulesReminder = () => { if (data.action === "delete") { - dispatchQ({ type: "DELETE_SCHEDULING", payload: +data.schedulingNotifyId }); + dispatchQ({ type: "DELETE_SCHEDULING", payload: +data.schedulingNotifyId }) //handleDeleteRows(data.schedulingNotifyId) } - setLoading(false); + setLoading(false) - }); + }) return () => { - socket.disconnect(); - }; - }, []); + socket.disconnect() + } + }, []) useEffect(() => { // dispatch({ type: "RESET" }); dispatchQ({ type: "RESET" }) - setPageNumber(1); - }, [searchParam]); + setPageNumber(1) + }, [searchParam]) //natalia @@ -252,71 +252,62 @@ const SchedulesReminder = () => { const fetchStatusChatEnd = async () => { try { - const statusChatEndLoad = await api.get("/statusChatEnd", { + const { data } = await api.get("/statusChatEnd", { params: { searchParam, pageNumber }, - }); - - // dispatch({ type: "LOAD_STATUS_CHAT_END", payload: statusChatEndLoad.data }); - - - - - // setStatusEndChat(statusChatEndLoad.data.filter(status => (status.id == '2' || status.id == '3'))) - - setStatusEndChat(statusChatEndLoad.data.filter(status => (`${status.id}` === '2' || `${status.id}` === '3'))) - + }) + setStatusEndChat(data.statusChatEnd.filter(status => (status.name === "LEMBRETE" || status.name === "AGENDAMENTO À CONFIRMAR"))) //setHasMore(data.hasMore); // setLoading(false); } catch (err) { - console.log(err); + console.log(err) } - }; + } - fetchStatusChatEnd(); + fetchStatusChatEnd() - }, 500); - return () => clearTimeout(delayDebounceFn); - }, [searchParam, pageNumber]); + }, 500) + return () => clearTimeout(delayDebounceFn) + }, [searchParam, pageNumber]) useEffect(() => { - setLoading(true); + setLoading(true) const delayDebounceFn = setTimeout(() => { const fetchQueries = async () => { try { - const dataQuery = await api.get("/schedules/", { params: { contactNumber, startDate, endDate }, }); + const dataQuery = await api.get("/schedules/", { params: { contactNumber, startDate, endDate }, }) dispatchQ({ type: "RESET" }) - dispatchQ({ type: "LOAD_QUERY", payload: dataQuery.data }); - setLoading(false); + dispatchQ({ type: "LOAD_QUERY", payload: dataQuery.data }) + setLoading(false) } catch (err) { - console.log(err); + console.log(err) } - }; + } - fetchQueries(); + fetchQueries() - }, 500); - return () => clearTimeout(delayDebounceFn); + }, 500) + return () => clearTimeout(delayDebounceFn) - }, [contactNumber, startDate, endDate]); + }, [contactNumber, startDate, endDate]) - useEffect(() => { - - if (!loading) { - + useEffect(() => { + if (!loading) { setData(query.map(({ scheduleReminder, ...others }) => ( - { ...others, 'scheduleReminder': `${others.statusChatEndId}` === '3' ? 'Agendamento' : 'Lembrete' } + { + ...others, 'scheduleReminder': others['statusChatEnd.name'] === 'AGENDAMENTO À CONFIRMAR' ? 'Agendamento' : 'Lembrete' + } ))) } @@ -338,8 +329,8 @@ const SchedulesReminder = () => { const handleSearch = (event) => { - setContactNumber(event.target.value.toLowerCase()); - }; + setContactNumber(event.target.value.toLowerCase()) + } const handleClear = () => { @@ -350,10 +341,10 @@ const SchedulesReminder = () => { } const handleCloseConfirmationModal = () => { - setConfirmModalOpen(false); + setConfirmModalOpen(false) - setSelectedSchedule(null); - }; + setSelectedSchedule(null) + } // const handleDeleteRows = (id) => { @@ -370,58 +361,46 @@ const SchedulesReminder = () => { const handleDeleteSchedule = async (scheduleId) => { try { - await api.delete(`/schedule/${scheduleId}`); - toast.success(("Lembrete/Agendamento deletado com sucesso!")); + await api.delete(`/schedule/${scheduleId}`) + toast.success(("Lembrete/Agendamento deletado com sucesso!")) //handleDeleteRows(scheduleId) } catch (err) { - toastError(err); + toastError(err) } - setSelectedSchedule(null); - }; + setSelectedSchedule(null) + } const handleUpdateSchedule = async (scheduleData, rowsDataNew) => { try { - await api.post("/schedule", scheduleData); - toast.success(("Lembrete/Agendamento atualizado com sucesso!")); + await api.post("/schedule", scheduleData) + toast.success(("Lembrete/Agendamento atualizado com sucesso!")) ////////////////// - const dataUpdate = [...dataRows]; - const index = rowsDataNew.tableData['id']; - dataUpdate[index] = rowsDataNew; + const dataUpdate = [...dataRows] + const index = rowsDataNew.tableData['id'] + dataUpdate[index] = rowsDataNew setData([...dataUpdate].map(({ scheduleReminder, ...others }) => ( - { ...others, 'scheduleReminder': `${others.statusChatEndId}` === '3' ? 'Agendamento' : 'Lembrete' } - ))); - - - - - ///////////////// + { ...others, 'scheduleReminder': others.statusChatEndName === 'AGENDAMENTO À CONFIRMAR' ? 'Agendamento' : 'Lembrete' } + ))) } catch (err) { - toastError(err); + toastError(err) } // - setSelectedSchedule(null); - }; + setSelectedSchedule(null) + } const chatEndVal = (data, rowsDataNew) => { if (data) { - - - - - - handleUpdateSchedule(data, rowsDataNew) - } } @@ -439,7 +418,7 @@ const SchedulesReminder = () => { rowData={rowData} />) - }; + } return ( @@ -492,7 +471,7 @@ const SchedulesReminder = () => { handleDeleteSchedule(selectedSchedule.id)} @@ -532,8 +511,8 @@ const SchedulesReminder = () => { icon: Edit, tooltip: 'Editar', onClick: (event, rowData) => { - - setSelectedSchedule(rowData); + console.log('ROW DATA EDIT: ', rowData) + setSelectedSchedule(rowData) handleModal(rowData) } @@ -543,8 +522,8 @@ const SchedulesReminder = () => { tooltip: 'Deletar', onClick: (event, rowData) => { - setSelectedSchedule(rowData); - setConfirmModalOpen(true); + setSelectedSchedule(rowData) + setConfirmModalOpen(true) } // onClick: handleDeleteRows } @@ -584,6 +563,6 @@ const SchedulesReminder = () => { ) -}; +} -export default SchedulesReminder; +export default SchedulesReminder diff --git a/frontend/src/pages/Settings/index.js b/frontend/src/pages/Settings/index.js index 149cb75..0fe21e2 100644 --- a/frontend/src/pages/Settings/index.js +++ b/frontend/src/pages/Settings/index.js @@ -81,18 +81,36 @@ const Settings = () => { } }, []) - useEffect(() => { - console.log('------> settings: ', settings) - }, [settings]) + // useEffect(() => { + // console.log('------> settings: ', settings) + // }, [settings]) const handleChangeSetting = async (e) => { const selectedValue = e.target.value - const settingKey = e.target.name + const settingKey = e.target.name try { await api.put(`/settings/${settingKey}`, { value: selectedValue, }) + + if (settingKey === 'farewellMessageByQueue' && + selectedValue === 'enabled' && + getSettingValue('farewellMessageByStatusChatEnd') === 'enabled') { + await api.put(`/settings/farewellMessageByStatusChatEnd`, { + value: 'disabled', + }) + } + + if (settingKey === 'farewellMessageByStatusChatEnd' && + selectedValue === 'enabled' && + getSettingValue('farewellMessageByQueue') === 'enabled') { + await api.put(`/settings/farewellMessageByQueue`, { + value: 'disabled', + }) + } + + toast.success(i18n.t('settings.success')) } catch (err) { toastError(err) @@ -313,6 +331,121 @@ const Settings = () => { + +
+ + + + Respostas rápidas por fila + + + + + +
+ + +
+ + + + Mensagem de encerramento por fila + + + + + +
+ + +
+ + + + Mensagem de encerramento por status de fechamento + + + + + +
+ +
+ + + + Exibir contatos por fila + + + + + +
+ )} /> diff --git a/frontend/src/pages/StatusChatEnd/index.js b/frontend/src/pages/StatusChatEnd/index.js new file mode 100644 index 0000000..c8de207 --- /dev/null +++ b/frontend/src/pages/StatusChatEnd/index.js @@ -0,0 +1,328 @@ +import React, { useState, useContext, useEffect, useReducer } from "react" +import openSocket from "socket.io-client" + +import { + Button, + IconButton, + makeStyles, + Paper, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + InputAdornment, + TextField, + CheckCircle, +} from "@material-ui/core" +import { Edit, DeleteOutline } from "@material-ui/icons" +import SearchIcon from "@material-ui/icons/Search" + +import MainContainer from "../../components/MainContainer" +import MainHeader from "../../components/MainHeader" +import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper" +import Title from "../../components/Title" + +import api from "../../services/api" +import { i18n } from "../../translate/i18n" +import TableRowSkeleton from "../../components/TableRowSkeleton" +import QuickAnswersModal from "../../components/QuickAnswersModal" +import ConfirmationModal from "../../components/ConfirmationModal" +import { toast } from "react-toastify" +import toastError from "../../errors/toastError" +import { AuthContext } from '../../context/Auth/AuthContext' +import StatusChatEndModal from "../../components/StatusChatEndModal" +import Switch from '@mui/material/Switch' + +const reducer = (state, action) => { + if (action.type === "LOAD_STATUS_CHAT_END") { + const statusChatEnds = action.payload + const newQuickAnswers = [] + + statusChatEnds.forEach((statusChatEnd) => { + const quickAnswerIndex = state.findIndex((q) => q.id === statusChatEnd.id) + if (quickAnswerIndex !== -1) { + state[quickAnswerIndex] = statusChatEnd + } else { + newQuickAnswers.push(statusChatEnd) + } + }) + + return [...state, ...newQuickAnswers] + } + + if (action.type === "UPDATE_STATUS_CHAT_END") { + const statusChatEnd = action.payload + const quickAnswerIndex = state.findIndex((q) => q.id === statusChatEnd.id) + + if (quickAnswerIndex !== -1) { + state[quickAnswerIndex] = statusChatEnd + return [...state] + } else { + return [statusChatEnd, ...state] + } + } + + if (action.type === "DELETE_STATUS_CHAT_END") { + const quickAnswerId = action.payload + + const quickAnswerIndex = state.findIndex((q) => q.id === quickAnswerId) + if (quickAnswerIndex !== -1) { + state.splice(quickAnswerIndex, 1) + } + return [...state] + } + + if (action.type === "RESET") { + return [] + } +} + +const useStyles = makeStyles((theme) => ({ + mainPaper: { + flex: 1, + padding: theme.spacing(1), + overflowY: "scroll", + ...theme.scrollbarStyles, + }, +})) + +const StatusChatEnd = () => { + const classes = useStyles() + + const [loading, setLoading] = useState(false) + const [pageNumber, setPageNumber] = useState(1) + const [searchParam, setSearchParam] = useState("") + const [statusChatEnds, dispatch] = useReducer(reducer, []) + const [selectedStatusChatEnd, setSelectedStatusChatEnd] = useState(null) + const [statusChatEndModalOpen, setStatusChatEndsModalOpen] = useState(false) + const [deletingStatusChatEnds, setDeletingStatusChatEnds] = useState(null) + const [confirmModalOpen, setConfirmModalOpen] = useState(false) + const [hasMore, setHasMore] = useState(false) + // const { user, } = useContext(AuthContext) + const [checked, setChecked] = useState(new Array(statusChatEnds.length).fill(false)) + + useEffect(() => { + dispatch({ type: "RESET" }) + setPageNumber(1) + }, [searchParam]) + + useEffect(() => { + setLoading(true) + const delayDebounceFn = setTimeout(() => { + const fetchQuickAnswers = async () => { + try { + const { data } = await api.get("/statusChatEnd", { + params: { searchParam, pageNumber }, + }) + + setChecked(data?.statusChatEnd?.map(s => s.isDefault ? true : false)) + + dispatch({ type: "LOAD_STATUS_CHAT_END", payload: data.statusChatEnd }) + setHasMore(data.hasMore) + setLoading(false) + } catch (err) { + toastError(err) + } + } + fetchQuickAnswers() + }, 500) + return () => clearTimeout(delayDebounceFn) + }, [searchParam, pageNumber]) + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL) + + socket.on("statusChatEnd", (data) => { + if (data.action === "update" || data.action === "create") { + dispatch({ type: "UPDATE_STATUS_CHAT_END", payload: data.statusChatEnd }) + } + + if (data.action === "delete") { + dispatch({ + type: "DELETE_STATUS_CHAT_END", + payload: +data.statusChatEndId, + }) + } + }) + + return () => { + socket.disconnect() + } + }, []) + + const handleSearch = (event) => { + setSearchParam(event.target.value.toLowerCase()) + } + + const handleOpenQuickAnswersModal = () => { + setSelectedStatusChatEnd(null) + setStatusChatEndsModalOpen(true) + } + + const handleCloseQuickAnswersModal = () => { + setSelectedStatusChatEnd(null) + setStatusChatEndsModalOpen(false) + } + + const handleEditStatusChatEnd = (statusChatEnd) => { + setSelectedStatusChatEnd(statusChatEnd) + setStatusChatEndsModalOpen(true) + } + + const handleDeleteStatusChatEnd = async (statusChatEndId) => { + try { + await api.delete(`/statusChatEnd/${statusChatEndId}`) + toast.success("Status de encerramento excluido com sucesso") + } catch (err) { + toastError(err) + } + setDeletingStatusChatEnds(null) + setSearchParam("") + setPageNumber(1) + } + + const loadMore = () => { + setPageNumber((prevState) => prevState + 1) + } + + const handleChange = async (event, statusChatEnd, index) => { + + const newChecked = new Array(statusChatEnds.length).fill(false) + newChecked[index] = event.target.checked + setChecked(newChecked) + + try { + const { id } = statusChatEnd + await api.put(`/statusChatEnd/${id}`, { isDefault: event.target.checked }) + toast.success("Status de encerramento padrão salvo com sucesso") + + } catch (error) { + toast.success("Erro: ", error) + + } + } + + + const handleScroll = (e) => { + if (!hasMore || loading) return + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget + if (scrollHeight - (scrollTop + 100) < clientHeight) { + loadMore() + } + } + + return ( + + handleDeleteStatusChatEnd(deletingStatusChatEnds.id)} + > + {i18n.t("quickAnswers.confirmationModal.deleteMessage")} + + + + {"Status de encerramento"} + + + + + ), + }} + /> + + + + + + + + + {"Status de encerramento"} + + + {"Mensagem de despedida"} + + + {"Padrão"} + + + {i18n.t("quickAnswers.table.actions")} + + + + + <> + {statusChatEnds.map((statusChatEnd, index) => { + + return ( + + {statusChatEnd.name} + {statusChatEnd.farewellMessage} + + + handleChange(event, statusChatEnd, index)} + inputProps={{ 'aria-label': 'controlled' }} + /> + + + handleEditStatusChatEnd(statusChatEnd)} + > + + + + { + setConfirmModalOpen(true) + setDeletingStatusChatEnds(statusChatEnd) + }} + > + + + + + ) + })} + {loading && } + + +
+
+
+ ) +} + +export default StatusChatEnd diff --git a/frontend/src/pages/Users/index.js b/frontend/src/pages/Users/index.js index 1ace0ad..c6c3af2 100644 --- a/frontend/src/pages/Users/index.js +++ b/frontend/src/pages/Users/index.js @@ -118,6 +118,7 @@ const Users = () => { const { data } = await api.get("/users/", { params: { searchParam, pageNumber }, }) + console.log('data.users: ', data.users) dispatch({ type: "LOAD_USERS", payload: data.users }) setHasMore(data.hasMore) setLoading(false) @@ -132,7 +133,7 @@ const Users = () => { - useEffect(() => { + useEffect(() => { const delayDebounceFn = setTimeout(() => { const fetchSession = async () => { try { @@ -149,13 +150,13 @@ const Users = () => { } fetchSession() }, 500) - return () => clearTimeout(delayDebounceFn) + return () => clearTimeout(delayDebounceFn) }, []) const getSettingValue = (key) => { - - return settings?.find((s) => s?.key === key)?.value + + return settings?.find((s) => s?.key === key)?.value // const { value } = settings.find((s) => s?.key === key) // return value @@ -309,52 +310,52 @@ const Users = () => { )} /> */} - - - - - - - - - - {i18n.t("users.table.name")} - - {i18n.t("users.table.email")} - - - {i18n.t("users.table.profile")} - - - Cargo - - - {i18n.t("users.table.actions")} - - - - - - <> - {users.map((user) => ( - - {user.name} - {user.email} - {user.profile} - {user.positionCompany} - - - - handleEditUser(user)} - > - - + + + + + + +
+ + + {i18n.t("users.table.name")} + + {i18n.t("users.table.email")} + + + {i18n.t("users.table.profile")} + + + Cargo + + + {i18n.t("users.table.actions")} + + + + + + <> + {users.map((user) => ( + + {user.name} + {user.email} + {user.profile} + {user?.position?.name} + + + + handleEditUser(user)} + > + + { component={QuickAnswers} isPrivate /> + + From c9750222c04b2dc5ad27f85569f0b9b4ba62c746 Mon Sep 17 00:00:00 2001 From: adriano Date: Mon, 15 Apr 2024 08:12:29 -0300 Subject: [PATCH 19/20] feat: Add new field to Tickets table to differentiate campaign tickets --- backend/src/controllers/TicketController.ts | 3 ++- ...14173559-add-column-isRemote-to-tickets.ts | 15 +++++++++++++ ...2-add-enble-filter-media-types-settings.ts | 22 ------------------- backend/src/models/Ticket.ts | 4 ++++ .../FindOrCreateTicketService.ts | 6 +++-- .../TicketServices/UpdateTicketService.ts | 9 +++++--- .../src/components/TicketListItem/index.js | 12 +++++++++- 7 files changed, 42 insertions(+), 29 deletions(-) create mode 100644 backend/src/database/migrations/20240414173559-add-column-isRemote-to-tickets.ts delete mode 100644 backend/src/database/seeds/20240405121812-add-enble-filter-media-types-settings.ts diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index 469deea..272bc10 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -264,7 +264,8 @@ export const remoteTicketCreation = async ( whatsappId, 0, undefined, - queueId + queueId, + true ); botSendMessage(ticket, `${msg}`); } diff --git a/backend/src/database/migrations/20240414173559-add-column-isRemote-to-tickets.ts b/backend/src/database/migrations/20240414173559-add-column-isRemote-to-tickets.ts new file mode 100644 index 0000000..5cbac7e --- /dev/null +++ b/backend/src/database/migrations/20240414173559-add-column-isRemote-to-tickets.ts @@ -0,0 +1,15 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Tickets", "isRemote", { + type: DataTypes.BOOLEAN, + allowNull: true, + defaultValue: false + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Tickets", "isRemote"); + } +}; diff --git a/backend/src/database/seeds/20240405121812-add-enble-filter-media-types-settings.ts b/backend/src/database/seeds/20240405121812-add-enble-filter-media-types-settings.ts deleted file mode 100644 index 2e148e7..0000000 --- a/backend/src/database/seeds/20240405121812-add-enble-filter-media-types-settings.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { QueryInterface } from "sequelize"; - -module.exports = { - up: (queryInterface: QueryInterface) => { - return queryInterface.bulkInsert( - "Settings", - [ - { - key: "filterMediasByType", - value: "disabled", - createdAt: new Date(), - updatedAt: new Date() - } - ], - {} - ); - }, - - down: (queryInterface: QueryInterface) => { - return queryInterface.bulkDelete("Settings", {}); - } -}; diff --git a/backend/src/models/Ticket.ts b/backend/src/models/Ticket.ts index 42175c8..71d12dd 100644 --- a/backend/src/models/Ticket.ts +++ b/backend/src/models/Ticket.ts @@ -43,6 +43,10 @@ class Ticket extends Model { @Column isGroup: boolean; + @Default(false) + @Column + isRemote: boolean; + @ForeignKey(() => StatusChatEnd) @Column statusChatEndId: number; diff --git a/backend/src/services/TicketServices/FindOrCreateTicketService.ts b/backend/src/services/TicketServices/FindOrCreateTicketService.ts index 29fe820..fb14349 100644 --- a/backend/src/services/TicketServices/FindOrCreateTicketService.ts +++ b/backend/src/services/TicketServices/FindOrCreateTicketService.ts @@ -14,7 +14,8 @@ const FindOrCreateTicketService = async ( whatsappId: number, unreadMessages: number, groupContact?: Contact, - queueId?: number | string + queueId?: number | string, + isRemote?: boolean ): Promise => { try { let ticket; @@ -106,7 +107,8 @@ const FindOrCreateTicketService = async ( queueId, unreadMessages, whatsappId, - phoneNumberId + phoneNumberId, + isRemote }); } diff --git a/backend/src/services/TicketServices/UpdateTicketService.ts b/backend/src/services/TicketServices/UpdateTicketService.ts index 943015b..545e015 100644 --- a/backend/src/services/TicketServices/UpdateTicketService.ts +++ b/backend/src/services/TicketServices/UpdateTicketService.ts @@ -21,6 +21,7 @@ interface TicketData { statusChatEndId?: number; unreadMessages?: number; whatsappId?: string | number; + isRemote?: boolean; } interface Request { @@ -48,7 +49,8 @@ const UpdateTicketService = async ({ statusChatEnd, unreadMessages, statusChatEndId, - whatsappId + whatsappId, + isRemote } = ticketData; const ticket = await ShowTicketService(ticketId); @@ -68,7 +70,7 @@ const UpdateTicketService = async ({ if (oldStatus === "closed") { await CheckContactOpenTickets(ticket.contact.id, ticket.whatsappId); } - + await ticket.update({ status, queueId, @@ -76,7 +78,8 @@ const UpdateTicketService = async ({ unreadMessages, statusChatEnd, statusChatEndId, - whatsappId + whatsappId, + isRemote }); await ticket.reload(); diff --git a/frontend/src/components/TicketListItem/index.js b/frontend/src/components/TicketListItem/index.js index 60c0fee..ab279cb 100644 --- a/frontend/src/components/TicketListItem/index.js +++ b/frontend/src/components/TicketListItem/index.js @@ -246,7 +246,17 @@ const TicketListItem = ({ ticket }) => { loading={loading} onClick={e => handleAcepptTicket(ticket.id)} > - {i18n.t("ticketsList.buttons.accept")} + {/* {i18n.t("ticketsList.buttons.accept")} */} + <> + {/* {i18n.t("ticketsList.buttons.accept")}
CAMPANHA */} + + {ticket?.isRemote ? ( + <>{i18n.t("ticketsList.buttons.accept")}
CAMPANHA + ) : ( + <>{i18n.t("ticketsList.buttons.accept")} + )} + + )} From 42073cc821e2bcf29d8f12479e59652d65927349 Mon Sep 17 00:00:00 2001 From: adriano Date: Mon, 15 Apr 2024 11:04:14 -0300 Subject: [PATCH 20/20] fix: Resolve duplicated quick responses bug and remove queue selection field on position page --- .../helpers/QuickAnswearByqueueFiltered.ts | 8 ++++-- .../src/components/PositionModal/index.js | 21 +++++++-------- .../src/components/QuickAnswersModal/index.js | 26 +++++++++++++------ 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/backend/src/helpers/QuickAnswearByqueueFiltered.ts b/backend/src/helpers/QuickAnswearByqueueFiltered.ts index 493f741..5ac0961 100644 --- a/backend/src/helpers/QuickAnswearByqueueFiltered.ts +++ b/backend/src/helpers/QuickAnswearByqueueFiltered.ts @@ -5,10 +5,11 @@ export default function quickAnswearByQueueFiltered( quickAnswers: QuickAnswer[] ) { let auxQuickAnswear = []; + let repet: any[] = []; const userQueues = queueIds.map((uq: any) => uq.queueId); for (const quickAnswer of quickAnswers) { - const { queues } = quickAnswer; + const { queues, id } = quickAnswer; if (queues.length == 0) { auxQuickAnswear.push(quickAnswer); @@ -17,7 +18,10 @@ export default function quickAnswearByQueueFiltered( for (const q of queues) { if (userQueues.includes(q.id)) { - auxQuickAnswear.push(quickAnswer); + if (repet.includes(id)) continue; + repet.push(id); + + auxQuickAnswear.push(quickAnswer); } } } diff --git a/frontend/src/components/PositionModal/index.js b/frontend/src/components/PositionModal/index.js index 516f4ac..29dcde5 100644 --- a/frontend/src/components/PositionModal/index.js +++ b/frontend/src/components/PositionModal/index.js @@ -70,11 +70,10 @@ const PositionModal = ({ } const [position, setPosition] = useState(initialState) - const [selectedQueueIds, setSelectedQueueIds] = useState([]) - const { user, setting, getSettingValue } = useContext(AuthContext) + // const [selectedQueueIds, setSelectedQueueIds] = useState([]) + const { setting } = useContext(AuthContext) const [settings, setSettings] = useState(setting) - - // console.log('USER: ', JSON.stringify(user, null, 6)) + useEffect(() => { return () => { @@ -121,11 +120,11 @@ const PositionModal = ({ if (isMounted.current) { setPosition(data) - if (data?.queues) { - console.log('data.queues: ', data.queues) - const quickQueueIds = data.queues?.map((queue) => queue.id) - setSelectedQueueIds(quickQueueIds) - } + // if (data?.queues) { + // console.log('data.queues: ', data.queues) + // const quickQueueIds = data.queues?.map((queue) => queue.id) + // setSelectedQueueIds(quickQueueIds) + // } } } catch (err) { toastError(err) @@ -203,7 +202,7 @@ const PositionModal = ({ fullWidth /> -
+ {/*
{ ((settings && getSettingValue('quickAnswerByQueue') === 'enabled')) && ( ) } -
+
*/}