Compare commits

..

2 Commits

Author SHA1 Message Date
gustavo-gsp cc12cafb99 merge changes, to merge the updated versions
Merge branch 'el_lojas_melhorias' of github.com:AdrianoRobson/projeto-hit into el_lojas_melhorias
2024-04-18 10:19:36 -03:00
gustavo-gsp 860d462d37 feat: add sound notifications and popOver for new tickets in waiting status, with option to enable in settings
Details:
- Implemented functionality to provide sound notifications and popOver alerts for new tickets in waiting status. Users can enable this feature in the settings.

feat: add average waiting time in the Waiting tab on the Tickets screen, with option to enable in settings

Details:
- Added functionality to display the average waiting time in the Waiting tab on the Tickets screen. Users can enable this feature in the settings.

feat: add ticket link in simple export directly from the reports table

Details:
- Implemented functionality to include the ticket link in the simple export directly from the reports table.

feat: remove report export options for supervisor profile

Details:
- Removed the report export options for the supervisor profile to restrict access.
2024-04-18 10:18:17 -03:00
33 changed files with 634 additions and 1161 deletions

View File

@ -8,6 +8,7 @@
"watch": "tsc -w", "watch": "tsc -w",
"start": "nodemon --expose-gc dist/server.js", "start": "nodemon --expose-gc dist/server.js",
"dev:server": "ts-node-dev --respawn --transpile-only --ignore node_modules src/server.ts", "dev:server": "ts-node-dev --respawn --transpile-only --ignore node_modules src/server.ts",
"dev": "nodemon --exec npm run dev:server",
"pretest": "NODE_ENV=test sequelize db:migrate && NODE_ENV=test sequelize db:seed:all", "pretest": "NODE_ENV=test sequelize db:migrate && NODE_ENV=test sequelize db:seed:all",
"test": "NODE_ENV=test jest", "test": "NODE_ENV=test jest",
"posttest": "NODE_ENV=test sequelize db:migrate:undo:all" "posttest": "NODE_ENV=test sequelize db:migrate:undo:all"

View File

@ -30,7 +30,6 @@ type IndexQuery = {
queueId: string; queueId: string;
pageNumber: string; pageNumber: string;
userQueues: []; userQueues: [];
isRemote: string;
}; };
type ReportOnQueue = { type ReportOnQueue = {
@ -263,7 +262,7 @@ export const reportMessagesUserByDateStartDateEnd = async (
data_query_messages[i].fromMe = "Cliente"; data_query_messages[i].fromMe = "Cliente";
} }
data_query_messages[i].id = i + 1; data_query_messages[i].id = i + 1;
} }
return res.status(200).json(data_query_messages); return res.status(200).json(data_query_messages);
@ -303,19 +302,15 @@ export const reportService = async (
throw new AppError("ERR_NO_PERMISSION", 403); throw new AppError("ERR_NO_PERMISSION", 403);
} }
const { startDate, endDate, queueId, isRemote } = req.query as IndexQuery; const { startDate, endDate, queueId } = req.query as IndexQuery;
console.log( console.log(
`startDate: ${startDate} | endDate: ${endDate} | queueId: ${queueId}` `startDate: ${startDate} | endDate: ${endDate} | queueId: ${queueId}`
); );
console.log("IS REMOTE: ", isRemote);
console.log("isRemote: ", isRemote && isRemote == "true" ? true : false);
const reportService = await ReportByNumberQueueService({ const reportService = await ReportByNumberQueueService({
startDate, startDate,
endDate, endDate
isRemote: isRemote && isRemote == "true" ? true : false
}); });
return res.status(200).json({ reportService }); return res.status(200).json({ reportService });
@ -333,13 +328,12 @@ export const reportServiceByQueue = async (
throw new AppError("ERR_NO_PERMISSION", 403); throw new AppError("ERR_NO_PERMISSION", 403);
} }
const { startDate, endDate, queueId, isRemote } = req.query as IndexQuery; const { startDate, endDate, queueId } = req.query as IndexQuery;
const reportService = await ReportByNumberQueueService({ const reportService = await ReportByNumberQueueService({
startDate, startDate,
endDate, endDate,
queue: true, queue: true
isRemote: isRemote && isRemote == "true" ? true : false
}); });
return res.status(200).json({ reportService }); return res.status(200).json({ reportService });

View File

@ -123,12 +123,11 @@ export const update = async (
throw new AppError("ERR_NO_PERMISSION", 403); throw new AppError("ERR_NO_PERMISSION", 403);
} }
const { settingKey: key } = req.params; const { settingKey: key } = req.params;
const { value, obj } = req.body; const { value } = req.body;
const setting = await UpdateSettingService({ const setting = await UpdateSettingService({
key, key,
value, value
obj
}); });
if (key && key == "whatsaAppCloudApi") { if (key && key == "whatsaAppCloudApi") {
@ -166,7 +165,7 @@ export const update = async (
} }
loadSettings(); loadSettings();
const io = getIO(); const io = getIO();
io.emit("settings", { io.emit("settings", {
action: "update", action: "update",

View File

@ -75,11 +75,10 @@ import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl";
import CreateContactService from "../services/ContactServices/CreateContactService"; import CreateContactService from "../services/ContactServices/CreateContactService";
import { botSendMessage } from "../services/WbotServices/wbotMessageListener"; import { botSendMessage } from "../services/WbotServices/wbotMessageListener";
import WhatsappQueue from "../models/WhatsappQueue"; import WhatsappQueue from "../models/WhatsappQueue";
import { del, get, set } from "../helpers/RedisClient"; import { get } from "../helpers/RedisClient";
import CountStatusChatEndService from "../services/StatusChatEndService/CountStatusChatEndService"; import CountStatusChatEndService from "../services/StatusChatEndService/CountStatusChatEndService";
import Queue from "../models/Queue"; import Queue from "../models/Queue";
import StatusChatEnd from "../models/StatusChatEnd"; import StatusChatEnd from "../models/StatusChatEnd";
import controllByNumber from "../helpers/controllByNumber";
export const index = async (req: Request, res: Response): Promise<Response> => { export const index = async (req: Request, res: Response): Promise<Response> => {
const { const {
@ -102,23 +101,20 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
queueIds = JSON.parse(queueIdsStringified); queueIds = JSON.parse(queueIdsStringified);
} }
const { tickets, count, hasMore, remoteTicketsControll } = const { tickets, count, hasMore } = await ListTicketsService({
await ListTicketsService({ searchParam,
searchParam, pageNumber,
pageNumber, status,
status, date,
date, showAll,
showAll, userId,
userId, queueIds,
queueIds, withUnreadMessages,
withUnreadMessages, unlimited,
unlimited, searchParamContent
searchParamContent });
});
return res return res.status(200).json({ tickets, count, hasMore });
.status(200)
.json({ tickets, count, hasMore, remoteTicketsControll });
}; };
export const remoteTicketCreation = async ( export const remoteTicketCreation = async (
@ -236,8 +232,7 @@ export const remoteTicketCreation = async (
const botInfo = await BotIsOnQueue("botqueue"); const botInfo = await BotIsOnQueue("botqueue");
// ticket from queueChoice or bot let ticket = await Ticket.findOne({
let ticket: any = await Ticket.findOne({
where: { where: {
[Op.or]: [ [Op.or]: [
{ contactId, status: "queueChoice" }, { contactId, status: "queueChoice" },
@ -263,69 +258,27 @@ export const remoteTicketCreation = async (
} }
} }
ticket = await Ticket.findOne({ if (!ticket) {
where: { ticket = await FindOrCreateTicketService(
[Op.or]: [ contact,
{ contactId, status: "pending" }, whatsappId,
{ contactId, status: "open" } 0,
] undefined,
} queueId,
}); true
if (ticket) {
console.log(
`THE CAMPAIGN TICKET WAS NOT CREATED BECAUSE THE TICKET IS PENDING OR OPEN`
); );
botSendMessage(ticket, `${msg}`);
return res.status(422).json({
msg: `The campaign ticket was not created because the number ${contact_to} already has a ticket open or pending`
});
} }
ticket = await FindOrCreateTicketService(
contact,
whatsappId,
0,
undefined,
queueId,
true
);
// botSendMessage(ticket, `${msg}`);
await ticket.update({
lastMessage: msg
});
await set(
`remote:ticketId:${ticket.id}`,
JSON.stringify({
id: ticket.id,
createdAt: ticket.createdAt,
updatedAt: ticket.updatedAt,
whatsappId: ticket.whatsappId
})
);
const io = getIO(); const io = getIO();
io.to(ticket.status).emit("ticket", { io.to(ticket.status).emit("ticket", {
action: "update", action: "update",
ticket ticket
}); });
const obj = await controllByNumber();
if (obj?.tickets) {
io.emit("remoteTickesControll", {
action: "update",
tickets: obj.ticketIds
});
}
console.log( console.log(
`REMOTE TICKET CREATION FROM ENDPOINT | STATUS: 200 | MSG: success` `REMOTE TICKET CREATION FROM ENDPOINT | STATUS: 200 | MSG: success`
); );
return res.status(200).json({ msg: "success" }); return res.status(200).json({ msg: "success" });
} }
@ -512,7 +465,7 @@ export const update = async (
} }
const schedulingNotifyCreate = await CreateSchedulingNotifyService({ const schedulingNotifyCreate = await CreateSchedulingNotifyService({
ticketId: scheduleData.ticketId, ticketId: scheduleData.ticketId,
statusChatEndId: `${statusChatEnd.id}`, statusChatEndId: `${statusChatEnd.id}`,
schedulingDate: scheduleData.schedulingDate, schedulingDate: scheduleData.schedulingDate,
schedulingTime: scheduleData.schedulingTime, schedulingTime: scheduleData.schedulingTime,
@ -695,7 +648,5 @@ export const remove = async (
ticketId: +ticketId ticketId: +ticketId
}); });
await del(`remote:ticketId:${ticketId}`);
return res.status(200).json({ message: "ticket deleted" }); return res.status(200).json({ message: "ticket deleted" });
}; };

View File

@ -1,15 +0,0 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Tickets", "remoteDone", {
type: DataTypes.BOOLEAN,
allowNull: true,
defaultValue: false
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Tickets", "remoteDone");
}
};

View File

@ -1,14 +0,0 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Settings", "obj", {
type: DataTypes.STRING,
allowNull: true
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Settings", "obj");
}
};

View File

@ -6,8 +6,8 @@ module.exports = {
"Settings", "Settings",
[ [
{ {
key: "remoteTicketSendControll", key: "blockAudioVideoMedia",
value: "true", value: "disabled",
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date()
} }

View File

@ -0,0 +1,22 @@
import { QueryInterface } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.bulkInsert(
"Settings",
[
{
key: "waitingTimeTickets",
value: "disabled",
createdAt: new Date(),
updatedAt: new Date()
}
],
{}
);
},
down: (queryInterface: QueryInterface) => {
return queryInterface.bulkDelete("Settings", {});
}
};

View File

@ -0,0 +1,22 @@
import { QueryInterface } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.bulkInsert(
"Settings",
[
{
key: "notificationTransferQueue",
value: "disabled",
createdAt: new Date(),
updatedAt: new Date()
}
],
{}
);
},
down: (queryInterface: QueryInterface) => {
return queryInterface.bulkDelete("Settings", {});
}
};

View File

@ -1,78 +0,0 @@
import { intervalToDuration } from "date-fns";
import { del, get, set } from "./RedisClient";
import { getIO } from "../libs/socket";
import controllByNumber from "./controllByNumber";
let timer: any;
const AutoRemoteTickets = async () => {
try {
let obj: any = await controllByNumber();
if (!obj?.tickets) return;
for (const ticket of obj.tickets) {
if (!ticket.includes("messageDateTime")) continue;
let match = ticket.match(/"messageDateTime":("[^"]*")/);
let messageDateTime = match ? match[1] : null;
console.log("messageDateTime: ", messageDateTime);
match = ticket.match(/"whatsappId":(\d+)/);
let whatsappId = match ? match[1] : null;
console.log("whatsappId: ", whatsappId);
const whatsapp = await get({
key: `whatsapp:${whatsappId}`
});
match = whatsapp.match(/"number":"(\d+)"/);
let number = match ? match[1] : null;
console.log("number: ", number);
match = ticket.match(/"id":(\d+)/);
let ticketId = match ? match[1] : null;
console.log("ticketId: ", ticketId);
number = JSON.parse(number);
ticketId = JSON.parse(ticketId);
let timeDiff: any = intervalToDuration({
start: new Date(JSON.parse(messageDateTime)),
end: new Date()
});
console.log("______timeDiff: ", timeDiff);
if (timeDiff.seconds > 50) {
del(`remote:ticketId:${ticketId}`);
obj = await controllByNumber();
const io = getIO();
io.emit("remoteTickesControll", {
action: "update",
tickets: obj.ticketIds
});
}
}
} catch (error) {
console.log("There was an error on auto remote tickets service: ", error);
}
};
const schedule = async () => {
try {
clearInterval(timer);
await AutoRemoteTickets();
} catch (error) {
console.log("error on schedule: ", error);
} finally {
timer = setInterval(schedule, 5000);
}
};
timer = setInterval(schedule, 5000);
export default schedule;

View File

@ -29,24 +29,14 @@ export async function getSimple(key: string) {
export async function get({ key, value, parse }: getData) { export async function get({ key, value, parse }: getData) {
if (key.includes("*")) { if (key.includes("*")) {
const keys = await redis.keys(key); const keys = await redis.keys(key);
if (keys.length > 0) { if (keys.length > 0) {
if (value) { for (const key of keys) {
for (const key of keys) { const val = await redis.get(key);
const val = await redis.get(key); if (val.includes(value)) {
if (val.includes(value)) { if (parse) return JSON.parse(val);
if (parse) return JSON.parse(val); return val;
return val;
}
} }
} else {
let res: any[] = [];
for (const key of keys) {
const val = await redis.get(key);
if (parse) res.push(JSON.parse(val));
res.push(val);
}
return res;
} }
} }
return null; return null;

View File

@ -1,41 +0,0 @@
import { get, set } from "./RedisClient";
async function controllByNumber() {
let tickets = await get({ key: "remote:ticketId*", parse: false });
if (!tickets) return { ticketIds: [], tickets: null };
let controll: any[] = [];
for (const ticket of tickets) {
let match = ticket.match(/"whatsappId":(\d+)/);
let whatsappId = match ? match[1] : null;
const whatsapp = await get({
key: `whatsapp:${whatsappId}`
});
match = whatsapp.match(/"number":"(\d+)"/);
let number = match ? match[1] : null;
match = ticket.match(/"id":(\d+)/);
let ticketId = match ? match[1] : null;
number = JSON.parse(number);
ticketId = JSON.parse(ticketId);
const index = controll.findIndex((c: any) => c.number == number);
if (index == -1) {
controll.push({ ticketId, number });
}
}
const ticketIds = controll.map((c: any) => c.ticketId);
set(`remote:controll`, JSON.stringify(ticketIds));
return { ticketIds, tickets };
}
export default controllByNumber;

View File

@ -16,9 +16,6 @@ class Setting extends Model<Setting> {
@Column @Column
value: string; value: string;
@Column
obj: string;
@CreatedAt @CreatedAt
createdAt: Date; createdAt: Date;

View File

@ -21,7 +21,7 @@ import User from "./User";
import Whatsapp from "./Whatsapp"; import Whatsapp from "./Whatsapp";
import SchedulingNotify from "./SchedulingNotify"; import SchedulingNotify from "./SchedulingNotify";
import StatusChatEnd from "./StatusChatEnd"; import StatusChatEnd from "./StatusChatEnd"
@Table @Table
class Ticket extends Model<Ticket> { class Ticket extends Model<Ticket> {
@ -47,10 +47,6 @@ class Ticket extends Model<Ticket> {
@Column @Column
isRemote: boolean; isRemote: boolean;
@Default(false)
@Column
remoteDone: boolean;
@ForeignKey(() => StatusChatEnd) @ForeignKey(() => StatusChatEnd)
@Column @Column
statusChatEndId: number; statusChatEndId: number;

View File

@ -9,7 +9,7 @@ settingRoutes.get("/settings", SettingController.index);
settingRoutes.get("/settings/ticket/:number", SettingController.ticketSettings); settingRoutes.get("/settings/ticket/:number", SettingController.ticketSettings);
// settingRoutes.get("/settings/:settingKey", isAuth, SettingsController.show); // routes.get("/settings/:settingKey", isAuth, SettingsController.show);
settingRoutes.put( settingRoutes.put(
"/settings/ticket", "/settings/ticket",

View File

@ -13,7 +13,6 @@ import { loadContactsCache } from "./helpers/ContactsCache";
import { loadSchedulesCache } from "./helpers/SchedulingNotifyCache"; import { loadSchedulesCache } from "./helpers/SchedulingNotifyCache";
import { delRestoreControllFile } from "./helpers/RestoreControll"; import { delRestoreControllFile } from "./helpers/RestoreControll";
import "./helpers/AutoCloseTickets"; import "./helpers/AutoCloseTickets";
import "./helpers/AutoRemoteTickets"
import "./helpers/SchedulingNotifySendMessage"; import "./helpers/SchedulingNotifySendMessage";
import axios from "axios"; import axios from "axios";
@ -91,27 +90,17 @@ gracefulShutdown(server);
const { phoneNumberId, id, greetingMessage } = whatsapps[i]; const { phoneNumberId, id, greetingMessage } = whatsapps[i];
if (phoneNumberId) { if (phoneNumberId) {
// await set( await set(
// `whatsapp:${whatsapps[i].dataValues.id}`, `whatsapp:${whatsapps[i].dataValues.id}`,
// JSON.stringify({ JSON.stringify({
// number: whatsapps[i].dataValues.number, number: whatsapps[i].dataValues.number,
// id, id,
// greetingMessage, greetingMessage,
// phoneNumberId phoneNumberId
// }) })
// ); );
} }
await set(
`whatsapp:${whatsapps[i].dataValues.id}`,
JSON.stringify({
number: whatsapps[i].dataValues.number,
id,
greetingMessage,
phoneNumberId
})
);
if (phoneNumberId) { if (phoneNumberId) {
continue; continue;
} }

View File

@ -1,5 +1,4 @@
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import { get, set } from "../../helpers/RedisClient";
import { updateTicketCacheByTicketId } from "../../helpers/TicketCache"; import { updateTicketCacheByTicketId } from "../../helpers/TicketCache";
import { getIO } from "../../libs/socket"; import { getIO } from "../../libs/socket";
import Message from "../../models/Message"; import Message from "../../models/Message";
@ -22,8 +21,10 @@ interface Request {
const CreateMessageService = async ({ const CreateMessageService = async ({
messageData messageData
}: Request): Promise<Message> => { }: Request): Promise<Message> => {
try { try {
await Message.upsert(messageData); await Message.upsert(messageData);
const message = await Message.findByPk(messageData.id, { const message = await Message.findByPk(messageData.id, {
@ -46,35 +47,13 @@ const CreateMessageService = async ({
throw new Error("ERR_CREATING_MESSAGE"); throw new Error("ERR_CREATING_MESSAGE");
} }
//////////////////// SETTINGS /////////////////////////// if (message.ticket.status != "queueChoice") {
let ticketRemote = await get({
key: `remote:ticketId:${message.ticket.id}`
});
if (ticketRemote && !ticketRemote.includes("messageDateTime")) {
ticketRemote = JSON.parse(ticketRemote);
ticketRemote = {
...ticketRemote,
...{ messageDateTime: message.createdAt }
};
const ticket = await Ticket.findByPk(message.ticket.id);
ticket?.update({ remoteDone: true });
console.log('MESSAGE SERVICE: XXXXXXXXXXXXXXXXXXXXXXX')
set(`remote:ticketId:${message.ticket.id}`, JSON.stringify(ticketRemote));
}
//////////////////////////////////////////////////////////
if (message.ticket.status != "queueChoice") {
await updateTicketCacheByTicketId(message.ticket.id, { await updateTicketCacheByTicketId(message.ticket.id, {
lastMessage: message.body, lastMessage: message.body,
updatedAt: new Date(message.ticket.updatedAt).toISOString(), updatedAt: new Date(message.ticket.updatedAt).toISOString(),
"contact.profilePicUrl": message.ticket.contact.profilePicUrl, "contact.profilePicUrl": message.ticket.contact.profilePicUrl,
unreadMessages: message.ticket.unreadMessages unreadMessages: message.ticket.unreadMessages
}); });
const io = getIO(); const io = getIO();
io.to(message.ticketId.toString()) io.to(message.ticketId.toString())

View File

@ -15,17 +15,14 @@ interface Request {
startDate: string | number; startDate: string | number;
endDate: string; endDate: string;
queue?: boolean; queue?: boolean;
isRemote?: boolean;
} }
const ReportByNumberQueueService = async ({ const ReportByNumberQueueService = async ({
startDate, startDate,
endDate, endDate,
queue = false, queue = false
isRemote = false
}: Request): Promise<any[]> => { }: Request): Promise<any[]> => {
let reportServiceData: any[] = []; let reportServiceData: any[] = [];
const includeIsRemote = isRemote ? "t.isRemote = true AND" : "";
const whatsapps = await Whatsapp.findAll(); const whatsapps = await Whatsapp.findAll();
@ -33,11 +30,6 @@ const ReportByNumberQueueService = async ({
for (const whatsapp of whatsapps) { for (const whatsapp of whatsapps) {
const { id, name, number } = whatsapp; const { id, name, number } = whatsapp;
let startedByClient: any;
let avgChatWaitingTime: any;
let pendingChat: any;
let closedChat: any;
if ( if (
!number || !number ||
reportServiceData.findIndex((w: any) => w?.number == number) != -1 reportServiceData.findIndex((w: any) => w?.number == number) != -1
@ -53,17 +45,16 @@ const ReportByNumberQueueService = async ({
JOIN Messages m ON t.id = m.ticketId JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId JOIN Queues q ON q.id = t.queueId
WHERE ${includeIsRemote} DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' 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.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromAgent = 1 AND m.fromAgent = 1
AND w.number = ${number};`, AND w.number = ${number};`,
{ type: QueryTypes.SELECT } { type: QueryTypes.SELECT }
); );
if (!isRemote) { // CHAT STARTED BY CLIENT
// CHAT STARTED BY CLIENT const startedByClient: any = await sequelize.query(
startedByClient = await sequelize.query( `SELECT COUNT(DISTINCT t.id) AS ticket_count
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t FROM Tickets t
JOIN Messages m ON t.id = m.ticketId JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id JOIN Whatsapps w ON t.whatsappId = w.id
@ -72,28 +63,12 @@ const ReportByNumberQueueService = async ({
AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromMe = 0 AND m.fromMe = 0
AND w.number = ${number};`, AND w.number = ${number};`,
{ type: QueryTypes.SELECT } { type: QueryTypes.SELECT }
); );
} else {
// CHAT RESPONSE BY CLIENT
startedByClient = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t
JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE ${includeIsRemote} DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND m.createdAt = (SELECT MAX(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromMe = 0
AND w.number = ${number};`,
{ type: QueryTypes.SELECT }
);
}
if (!isRemote) { // CHAT CLOSED
// CHAT CLOSED const closedChat: any = await sequelize.query(
closedChat = await sequelize.query( `SELECT COUNT(DISTINCT t.id) AS ticket_count
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t FROM Tickets t
JOIN Messages m ON t.id = m.ticketId JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id JOIN Whatsapps w ON t.whatsappId = w.id
@ -101,26 +76,12 @@ const ReportByNumberQueueService = async ({
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND t.status = 'closed' AND t.status = 'closed'
AND w.number = ${number};`, AND w.number = ${number};`,
{ type: QueryTypes.SELECT } { type: QueryTypes.SELECT }
); );
} else {
// CHAT CLOSED
closedChat = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE ${includeIsRemote} DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND t.status = 'closed'
AND w.number = ${number};`,
{ type: QueryTypes.SELECT }
);
}
if (!isRemote) { // CHAT WAINTING TIME
// CHAT WAINTING TIME const avgChatWaitingTime: any = await sequelize.query(
avgChatWaitingTime = await sequelize.query( `
`
SELECT TIME_FORMAT( SELECT TIME_FORMAT(
SEC_TO_TIME( SEC_TO_TIME(
TIMESTAMPDIFF( TIMESTAMPDIFF(
@ -143,27 +104,25 @@ const ReportByNumberQueueService = async ({
) )
) )
), '%H:%i:%s') AS WAITING_TIME ), '%H:%i:%s') AS WAITING_TIME
FROM Tickets t FROM Tickets t
JOIN Messages m ON t.id = m.ticketId JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' WHERE 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.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromMe = 0 AND m.fromMe = 0
-- AND q.id = 2 -- AND q.id = 2
AND w.number = ${number} AND w.number = ${number}
AND t.status IN ('open', 'closed') AND t.status IN ('open', 'closed')
HAVING WAITING_TIME IS NOT NULL HAVING WAITING_TIME IS NOT NULL
ORDER BY ORDER BY
WAITING_TIME;`, WAITING_TIME;`,
{ type: QueryTypes.SELECT } { type: QueryTypes.SELECT }
); );
}
if (!isRemote) { // CHAT PENDING
// CHAT PENDING const pendingChat: any = await sequelize.query(
pendingChat = await sequelize.query( `SELECT COUNT(DISTINCT t.id) AS ticket_count
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t FROM Tickets t
JOIN Messages m ON t.id = m.ticketId JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id JOIN Whatsapps w ON t.whatsappId = w.id
@ -172,41 +131,23 @@ const ReportByNumberQueueService = async ({
AND t.status = 'pending' AND t.status = 'pending'
AND w.number = ${number};`, AND w.number = ${number};`,
{ type: QueryTypes.SELECT } { type: QueryTypes.SELECT }
); );
} else {
// CHAT PENDING REMOTE
pendingChat = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE ${includeIsRemote} DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND t.status = 'pending'
AND w.number = ${number};`,
{ type: QueryTypes.SELECT }
);
}
reportServiceData.push({ reportServiceData.push({
id, id,
name, name,
number, number,
startedByAgent: startedByAgent[0]?.ticket_count, startedByAgent: startedByAgent[0]?.ticket_count,
startedByClient: startedByClient ? startedByClient[0]?.ticket_count : 0, startedByClient: startedByClient[0]?.ticket_count,
closedChat: closedChat[0]?.ticket_count, closedChat: closedChat[0]?.ticket_count,
avgChatWaitingTime: avgChatWaitingTime ? avg(avgChatWaitingTime) : 0, avgChatWaitingTime: avg(avgChatWaitingTime),
pendingChat: pendingChat[0]?.ticket_count pendingChat: pendingChat[0]?.ticket_count
}); });
} }
} else { } else {
for (const whatsapp of whatsapps) { for (const whatsapp of whatsapps) {
const { id, name, number } = whatsapp; const { id, name, number } = whatsapp;
let startedByClient: any;
let avgChatWaitingTime: any;
let pendingChat: any;
let closedChat: any;
if ( if (
!number || !number ||
@ -231,17 +172,16 @@ const ReportByNumberQueueService = async ({
JOIN Messages m ON t.id = m.ticketId JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId JOIN Queues q ON q.id = t.queueId
WHERE ${includeIsRemote} DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' 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.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromAgent = 1 AND m.fromAgent = 1
AND q.id = ${q.id};`, AND q.id = ${q.id};`,
{ type: QueryTypes.SELECT } { type: QueryTypes.SELECT }
); );
if (!isRemote) { // CHAT STARTED BY CLIENT
// CHAT STARTED BY CLIENT const startedByClient: any = await sequelize.query(
startedByClient = await sequelize.query( `SELECT COUNT(DISTINCT t.id) AS ticket_count
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t FROM Tickets t
JOIN Messages m ON t.id = m.ticketId JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id JOIN Whatsapps w ON t.whatsappId = w.id
@ -250,97 +190,64 @@ const ReportByNumberQueueService = async ({
AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id) AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromMe = 0 AND m.fromMe = 0
AND q.id = ${q.id};`, AND q.id = ${q.id};`,
{ type: QueryTypes.SELECT } { type: QueryTypes.SELECT }
); );
} else {
// CHAT RESPONSE BY CLIENT // CHAT CLOSED
startedByClient = await sequelize.query( const closedChat: any = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count `SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t FROM Tickets t
JOIN Messages m ON t.id = m.ticketId JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId JOIN Queues q ON q.id = t.queueId
WHERE ${includeIsRemote} DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND m.createdAt = (SELECT MAX(createdAt) FROM Messages WHERE ticketId = t.id) AND t.status = 'closed'
AND m.fromMe = 0 AND q.id = ${q.id};`,
AND q.id = ${q.id};`, { type: QueryTypes.SELECT }
{ type: QueryTypes.SELECT } );
);
}
if (!isRemote) { // CHAT WAINTING TIME
// CHAT CLOSED const avgChatWaitingTime: any = await sequelize.query(
closedChat = await sequelize.query( `SELECT TIME_FORMAT(
`SELECT COUNT(DISTINCT t.id) AS ticket_count SEC_TO_TIME(
FROM Tickets t TIMESTAMPDIFF(
JOIN Messages m ON t.id = m.ticketId SECOND,
JOIN Whatsapps w ON t.whatsappId = w.id (
JOIN Queues q ON q.id = t.queueId SELECT createdAt
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' FROM Messages
AND t.status = 'closed' WHERE ticketId = m.ticketId
AND q.id = ${q.id};`, AND fromMe = 0
{ type: QueryTypes.SELECT } ORDER BY createdAt ASC
); LIMIT 1
} ),
else{ (
// CHAT CLOSED REMOTE SELECT createdAt
closedChat = await sequelize.query( FROM Messages
`SELECT COUNT(DISTINCT t.id) AS ticket_count WHERE ticketId = m.ticketId
FROM Tickets t AND fromAgent = 1
JOIN Whatsapps w ON t.whatsappId = w.id ORDER BY createdAt ASC
JOIN Queues q ON q.id = t.queueId LIMIT 1
WHERE ${includeIsRemote} DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' )
AND t.status = 'closed' )
AND q.id = ${q.id};`, ), '%H:%i:%s') AS WAITING_TIME
{ type: QueryTypes.SELECT } FROM Tickets t
); JOIN Messages m ON t.id = m.ticketId
} JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromMe = 0
AND q.id = ${q.id}
AND t.status IN ('open', 'closed')
HAVING WAITING_TIME IS NOT NULL
ORDER BY
WAITING_TIME;`,
{ type: QueryTypes.SELECT }
);
if (!isRemote) { // CHAT PENDING
// CHAT WAINTING TIME const pendingChat: any = await sequelize.query(
avgChatWaitingTime = await sequelize.query( `SELECT COUNT(DISTINCT t.id) AS ticket_count
`SELECT TIME_FORMAT(
SEC_TO_TIME(
TIMESTAMPDIFF(
SECOND,
(
SELECT createdAt
FROM Messages
WHERE ticketId = m.ticketId
AND fromMe = 0
ORDER BY createdAt ASC
LIMIT 1
),
(
SELECT createdAt
FROM Messages
WHERE ticketId = m.ticketId
AND fromAgent = 1
ORDER BY createdAt ASC
LIMIT 1
)
)
), '%H:%i:%s') AS WAITING_TIME
FROM Tickets t
JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromMe = 0
AND q.id = ${q.id}
AND t.status IN ('open', 'closed')
HAVING WAITING_TIME IS NOT NULL
ORDER BY
WAITING_TIME;`,
{ type: QueryTypes.SELECT }
);
}
if (!isRemote) {
// CHAT PENDING
pendingChat = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t FROM Tickets t
JOIN Messages m ON t.id = m.ticketId JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id JOIN Whatsapps w ON t.whatsappId = w.id
@ -349,22 +256,8 @@ const ReportByNumberQueueService = async ({
AND t.status = 'pending' AND t.status = 'pending'
AND q.id = ${q.id};`, AND q.id = ${q.id};`,
{ type: QueryTypes.SELECT } { type: QueryTypes.SELECT }
); );
} else {
// CHAT PENDING REMOTE
pendingChat = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE ${includeIsRemote} DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND t.status = 'pending'
AND q.id = ${q.id};`,
{ type: QueryTypes.SELECT }
);
}
reportServiceData.push({ reportServiceData.push({
id, id,
@ -373,11 +266,9 @@ const ReportByNumberQueueService = async ({
queueName: q.name, queueName: q.name,
queueColor: q.color, queueColor: q.color,
startedByAgent: startedByAgent[0]?.ticket_count, startedByAgent: startedByAgent[0]?.ticket_count,
startedByClient: startedByClient startedByClient: startedByClient[0]?.ticket_count,
? startedByClient[0]?.ticket_count
: 0,
closedChat: closedChat[0]?.ticket_count, closedChat: closedChat[0]?.ticket_count,
avgChatWaitingTime: avgChatWaitingTime ? avg(avgChatWaitingTime) : 0, avgChatWaitingTime: avg(avgChatWaitingTime),
pendingChat: pendingChat[0]?.ticket_count pendingChat: pendingChat[0]?.ticket_count
}); });
} }
@ -392,23 +283,23 @@ export default ReportByNumberQueueService;
function avg(avgChatWaitingTime: any) { function avg(avgChatWaitingTime: any) {
let waitingAVG: any = avgChatWaitingTime let waitingAVG: any = avgChatWaitingTime
.filter((t: any) => t?.WAITING_TIME) .filter((t: any) => t?.WAITING_TIME)
.map((t: any) => t.WAITING_TIME); .map((t: any) => t.WAITING_TIME)
if (waitingAVG.length > 0) { if (waitingAVG.length > 0) {
let midIndex = Math.floor((0 + waitingAVG.length) / 2); let midIndex = Math.floor((0 + waitingAVG.length) / 2)
if (waitingAVG.length % 2 == 1) { if (waitingAVG.length % 2 == 1) {
waitingAVG = waitingAVG[midIndex]; waitingAVG = waitingAVG[midIndex]
} else { } else {
waitingAVG = calculateAverageTime( waitingAVG = calculateAverageTime(
waitingAVG[midIndex - 1], waitingAVG[midIndex - 1],
waitingAVG[midIndex] waitingAVG[midIndex]
); )
} }
} else { } else {
waitingAVG = 0; waitingAVG = 0
} }
return waitingAVG; return waitingAVG
} }
function calculateAverageTime(time1: string, time2: string) { function calculateAverageTime(time1: string, time2: string) {

View File

@ -4,15 +4,12 @@ import Setting from "../../models/Setting";
interface Request { interface Request {
key: string; key: string;
value: string; value: string;
obj?: string;
} }
const UpdateSettingService = async ({ const UpdateSettingService = async ({
key, key,
value, value
obj
}: Request): Promise<Setting | undefined> => { }: Request): Promise<Setting | undefined> => {
console.log("key: ", key, " | value: ", value, " | obj: ", obj);
try { try {
const setting = await Setting.findOne({ const setting = await Setting.findOne({
@ -23,16 +20,12 @@ const UpdateSettingService = async ({
throw new AppError("ERR_NO_SETTING_FOUND", 404); throw new AppError("ERR_NO_SETTING_FOUND", 404);
} }
if (obj) { await setting.update({ value });
obj = JSON.stringify(obj);
}
await setting.update({ value, obj });
await setting.reload();
return setting; return setting;
} catch (error: any) { } catch (error: any) {
console.error("===> Error on UpdateSettingService.ts file: \n", error); console.error('===> Error on UpdateSettingService.ts file: \n', error)
throw new AppError(error.message); throw new AppError(error.message);
} }
}; };

View File

@ -20,7 +20,6 @@ import ListTicketServiceCache from "./ListTicketServiceCache";
import { searchTicketCache, loadTicketsCache } from "../../helpers/TicketCache"; import { searchTicketCache, loadTicketsCache } from "../../helpers/TicketCache";
import { getWbot } from "../../libs/wbot"; import { getWbot } from "../../libs/wbot";
import User from "../../models/User"; import User from "../../models/User";
import { get } from "../../helpers/RedisClient";
interface Request { interface Request {
searchParam?: string; searchParam?: string;
@ -39,7 +38,6 @@ interface Response {
tickets: Ticket[]; tickets: Ticket[];
count: number; count: number;
hasMore: boolean; hasMore: boolean;
remoteTicketsControll?: object[];
} }
const ListTicketsService = async ({ const ListTicketsService = async ({
@ -232,7 +230,7 @@ const ListTicketsService = async ({
console.log("kkkkkkkkk limit: ", limit); console.log("kkkkkkkkk limit: ", limit);
let { count, rows: tickets } = await Ticket.findAndCountAll({ const { count, rows: tickets } = await Ticket.findAndCountAll({
where: whereCondition, where: whereCondition,
include: includeCondition, include: includeCondition,
distinct: true, distinct: true,
@ -243,13 +241,10 @@ const ListTicketsService = async ({
const hasMore = count > offset + tickets.length; const hasMore = count > offset + tickets.length;
const ticketIds = await get({ key: `remote:controll`, parse: true });
return { return {
tickets, tickets,
count, count,
hasMore, hasMore
remoteTicketsControll: ticketIds ? ticketIds : []
}; };
}; };

View File

@ -10,7 +10,7 @@ import { createOrUpdateTicketCache } from "../../helpers/TicketCache";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import sendWhatsAppMessageSocket from "../../helpers/SendWhatsappMessageSocket"; import sendWhatsAppMessageSocket from "../../helpers/SendWhatsappMessageSocket";
import BotIsOnQueue from "../../helpers/BotIsOnQueue"; import BotIsOnQueue from "../../helpers/BotIsOnQueue";
import { del, deleteObject, get, set } from "../../helpers/RedisClient"; import { deleteObject } from "../../helpers/RedisClient";
var flatten = require("flat"); var flatten = require("flat");
interface TicketData { interface TicketData {
@ -71,23 +71,6 @@ const UpdateTicketService = async ({
await CheckContactOpenTickets(ticket.contact.id, ticket.whatsappId); await CheckContactOpenTickets(ticket.contact.id, ticket.whatsappId);
} }
if (status == "closed") {
del(`remote:ticketId:${ticket.id}`);
let ticketsIds = await get({ key: `remote:controll`, parse: true });
const index = ticketsIds.findIndex((t: any) => t == ticket.id);
console.log("ticketsIds 1: ", ticketsIds);
if (index != -1) {
ticketsIds.splice(index, 1);
console.log("ticketsIds 2: ", ticketsIds);
set(`remote:controll`, JSON.stringify(ticketsIds));
}
}
await ticket.update({ await ticket.update({
status, status,
queueId, queueId,

View File

@ -94,8 +94,7 @@ import {
findByContain, findByContain,
findObject, findObject,
get, get,
getSimple, getSimple
set
} from "../../helpers/RedisClient"; } from "../../helpers/RedisClient";
import FindOrCreateTicketServiceBot from "../TicketServices/FindOrCreateTicketServiceBot"; import FindOrCreateTicketServiceBot from "../TicketServices/FindOrCreateTicketServiceBot";
import ShowTicketService from "../TicketServices/ShowTicketService"; import ShowTicketService from "../TicketServices/ShowTicketService";
@ -170,7 +169,7 @@ const verifyMediaMessage = async (
if (!media) { if (!media) {
throw new Error("ERR_WAPP_DOWNLOAD_MEDIA"); throw new Error("ERR_WAPP_DOWNLOAD_MEDIA");
} }
let mediaAuthorized = true;
let messageData = { let messageData = {
id: msg.id.id, id: msg.id.id,
ticketId: ticket.id, ticketId: ticket.id,
@ -184,7 +183,9 @@ const verifyMediaMessage = async (
phoneNumberId: msg?.phoneNumberId, phoneNumberId: msg?.phoneNumberId,
fromAgent: false fromAgent: false
}; };
if(messageData.mediaType === 'video' || messageData.mediaType === 'audio' && getSettingValue('blockAudioVideoMedia')?.value === 'enabled'){
mediaAuthorized = false;
}
if (msg?.fromMe) { if (msg?.fromMe) {
messageData = { ...messageData, fromAgent: true }; messageData = { ...messageData, fromAgent: true };
} }
@ -199,23 +200,36 @@ const verifyMediaMessage = async (
body: media.filename body: media.filename
}; };
} }
if(mediaAuthorized){
try { try {
await writeFileAsync( await writeFileAsync(
join(__dirname, "..", "..", "..", "..", "..", "public", media.filename), join(__dirname, "..", "..", "..", "..", "..", "public", media.filename),
media.data, media.data,
"base64" "base64"
); );
} catch (err) { } catch (err) {
Sentry.captureException(err); Sentry.captureException(err);
logger.error(`There was an error: wbotMessageLitener.ts: ${err}`); logger.error(`There was an error: wbotMessageLitener.ts: ${err}`);
}
} }
} }
if(mediaAuthorized){
await ticket.update({ lastMessage: msg.body || media.filename }); await ticket.update({ lastMessage: msg.body || media.filename });
const newMessage = await CreateMessageService({ messageData }); const newMessage = await CreateMessageService({ messageData });
return newMessage;
return newMessage; }else{
if (ticket.status !== "queueChoice") {
botSendMessage(
ticket,
`Atenção! Mensagem ignorada, tipo de mídia não suportado.`
);
}
messageData.body = `Mensagem de *${messageData.mediaType}* ignorada, tipo de mídia não suportado.`;
messageData.mediaUrl = '';
await ticket.update({ lastMessage: `Mensagem de *${messageData.mediaType}* ignorada, tipo de mídia não suportado.`});
const newMessage = await CreateMessageService({ messageData });
console.log(`--------->>> Mensagem do tipo: ${messageData.mediaType}, ignorada!`)
}
}; };
// const verifyMediaMessage = async ( // const verifyMediaMessage = async (
@ -326,7 +340,7 @@ const verifyMessage = async (
} else { } else {
messageData = { ...messageData, body: JSON.stringify(msg.vCards) }; messageData = { ...messageData, body: JSON.stringify(msg.vCards) };
} }
} }
await CreateMessageService({ messageData }); await CreateMessageService({ messageData });
}; };
@ -358,7 +372,7 @@ const verifyQueue = async (
//////////////// EXTRAIR APENAS O NÚMERO /////////////////// //////////////// EXTRAIR APENAS O NÚMERO ///////////////////
selectedOption = selectedOption.match(/\d+/); selectedOption = selectedOption.match(/\d+/);
/////////////////////////////////// ///////////////////////////////////
if (selectedOption) choosenQueue = queues[+selectedOption - 1]; if (selectedOption) choosenQueue = queues[+selectedOption - 1];
} }
} }
@ -413,13 +427,15 @@ const verifyQueue = async (
} }
let body = ""; let body = "";
const io = getIO();
if (botOptions.length > 0) { if (botOptions.length > 0) {
body = `\u200e${choosenQueue.greetingMessage}\n\n${botOptions}\n${final_message.msg}`; body = `\u200e${choosenQueue.greetingMessage}\n\n${botOptions}\n${final_message.msg}`;
} else { } else {
body = `\u200e${choosenQueue.greetingMessage}`; body = `\u200e${choosenQueue.greetingMessage}`;
} }
io.emit('notifyPeding', {data: {ticket, queue: choosenQueue}});
sendWhatsAppMessageSocket(ticket, body); sendWhatsAppMessageSocket(ticket, body);
} else { } else {
//test del transfere o atendimento se entrar na ura infinita //test del transfere o atendimento se entrar na ura infinita
@ -762,11 +778,14 @@ const handleMessage = async (
unreadMessages unreadMessages
// groupContact // groupContact
); );
} }
if (getSettingValue("oneContactChatWithManyWhats")?.value == "disabled") { if (getSettingValue("oneContactChatWithManyWhats")?.value == "disabled") {
// Para responder para o cliente pelo mesmo whatsapp que ele enviou a mensagen // Para responder para o cliente pelo mesmo whatsapp que ele enviou a mensagen
if (wbot.id != ticket.whatsappId) { if (wbot.id != ticket.whatsappId) {
// console.log('PARA RESPONDER PELO MEMOS WHATSAPP wbot.id: ', wbot.id, ' | wbot.status: ', wbot.status)
// console.log('WHATSAPP STATUS ticket.whatsappId: ', ticket.whatsappId)
try { try {
await ticket.update({ whatsappId: wbot.id }); await ticket.update({ whatsappId: wbot.id });
} catch (error: any) { } catch (error: any) {

View File

@ -43,7 +43,6 @@ import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketH
import ModalTemplate from "../ModalTemplate" import ModalTemplate from "../ModalTemplate"
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import { countTicketMsgContext } from "../../context/CountTicketMsgProvider/CountTicketMsgProvider"
const Mp3Recorder = new MicRecorder({ bitRate: 128 }) const Mp3Recorder = new MicRecorder({ bitRate: 128 })
@ -209,12 +208,10 @@ const useStyles = makeStyles((theme) => ({
}, },
})) }))
const MessageInput = ({ ticketStatus, ticketLastMessage, ticketIsRemote }) => { const MessageInput = ({ ticketStatus }) => {
const { tabOption, setTabOption } = useContext(TabTicketContext) const { tabOption, setTabOption } = useContext(TabTicketContext)
const { countTicketMsg, setCountTicketMsg } = useContext(countTicketMsgContext)
const classes = useStyles() const classes = useStyles()
const { ticketId } = useParams() const { ticketId } = useParams()
@ -236,21 +233,11 @@ const MessageInput = ({ ticketStatus, ticketLastMessage, ticketIsRemote }) => {
const isRun = useRef(false) const isRun = useRef(false)
useEffect(() => { useEffect(() => {
inputRef.current.focus() inputRef.current.focus()
}, [replyingMessage]) }, [replyingMessage])
useEffect(() => {
if (ticketIsRemote && countTicketMsg === 0 && ticketLastMessage && ticketLastMessage.trim().length > 0) {
setInputMessage(ticketLastMessage)
}
else {
setInputMessage("")
}
}, [countTicketMsg, ticketIsRemote, ticketLastMessage])
useEffect(() => { useEffect(() => {
inputRef.current.focus() inputRef.current.focus()
return () => { return () => {
@ -365,8 +352,6 @@ const MessageInput = ({ ticketStatus, ticketLastMessage, ticketIsRemote }) => {
setTemplates(data.data) setTemplates(data.data)
} }
setCountTicketMsg(1)
} catch (err) { } catch (err) {
toastError(err) toastError(err)
} }
@ -459,7 +444,7 @@ const MessageInput = ({ ticketStatus, ticketLastMessage, ticketIsRemote }) => {
const { data } = await api.get("/quickAnswers/", { const { data } = await api.get("/quickAnswers/", {
params: { searchParam: inputMessage.substring(1), userId: user.id }, params: { searchParam: inputMessage.substring(1), userId: user.id },
}) })
setQuickAnswer(data.quickAnswers) setQuickAnswer(data.quickAnswers)
if (data.quickAnswers.length > 0) { if (data.quickAnswers.length > 0) {
setTypeBar(true) setTypeBar(true)

View File

@ -34,7 +34,6 @@ import whatsBackground from "../../assets/wa-background.png"
import api from "../../services/api" import api from "../../services/api"
import toastError from "../../errors/toastError" import toastError from "../../errors/toastError"
import { CountTicketMsgProvider, countTicketMsgContext } from "../../context/CountTicketMsgProvider/CountTicketMsgProvider"
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
messagesListWrapper: { messagesListWrapper: {
@ -329,8 +328,6 @@ const MessagesList = ({ ticketId, isGroup }) => {
const { user } = useContext(AuthContext) const { user } = useContext(AuthContext)
const { setCountTicketMsg } = useContext(countTicketMsgContext)
useEffect(() => { useEffect(() => {
dispatch({ type: "RESET" }) dispatch({ type: "RESET" })
setPageNumber(1) setPageNumber(1)
@ -412,11 +409,6 @@ const MessagesList = ({ ticketId, isGroup }) => {
}) })
if (currentTicketId.current === ticketId) { if (currentTicketId.current === ticketId) {
if (data?.messages) {
setCountTicketMsg(data.messages.length)
}
dispatch({ type: "LOAD_MESSAGES", payload: data.messages }) dispatch({ type: "LOAD_MESSAGES", payload: data.messages })
setHasMore(data.hasMore) setHasMore(data.hasMore)
setLoading(false) setLoading(false)

View File

@ -20,6 +20,9 @@ import useTickets from "../../hooks/useTickets"
import alertSound from "../../assets/sound.mp3" import alertSound from "../../assets/sound.mp3"
import { AuthContext } from "../../context/Auth/AuthContext" import { AuthContext } from "../../context/Auth/AuthContext"
import api from "../../services/api";
import toastError from "../../errors/toastError";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
tabContainer: { tabContainer: {
overflowY: "auto", overflowY: "auto",
@ -83,7 +86,7 @@ const NotificationsPopOver = () => {
const historyRef = useRef(history) const historyRef = useRef(history)
const { handleLogout } = useContext(AuthContext) const { handleLogout } = useContext(AuthContext)
const [settings, setSettings] = useState([]);
// const [lastRef] = useState(+history.location.pathname.split("/")[2]) // const [lastRef] = useState(+history.location.pathname.split("/")[2])
@ -110,7 +113,22 @@ const NotificationsPopOver = () => {
ticketIdRef.current = ticketIdUrl ticketIdRef.current = ticketIdUrl
}, [ticketIdUrl]) }, [ticketIdUrl])
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(() => { useEffect(() => {
@ -255,49 +273,80 @@ const NotificationsPopOver = () => {
if (shouldNotNotificate) return if (shouldNotNotificate) return
handleNotifications(data) handleNotifications(data)
} }
}) })
socket.on('notifyPeding', data =>{
if(settings?.length > 0 && getSettingValue('notificationTransferQueue') === 'enabled') handleNotifications("", data);
});
return () => { return () => {
socket.disconnect() socket.disconnect()
} }
}, [user]) }, [user, settings])
const handleNotifications = data => { const handleNotifications = (data, notify) => {
const { message, contact, ticket } = data let isQueue = false;
if(!notify){
const { message, contact, ticket } = data
const options = { const options = {
body: `${message.body} - ${format(new Date(), "HH:mm")}`, body: `${message.body} - ${format(new Date(), "HH:mm")}`,
icon: contact.profilePicUrl, icon: contact.profilePicUrl,
tag: ticket.id, tag: ticket.id,
renotify: true, renotify: true,
}
const notification = new Notification(
`${i18n.t("tickets.notification.message")} ${contact.name}`,
options
)
notification.onclick = e => {
e.preventDefault()
window.focus()
historyRef.current.push(`/tickets/${ticket.id}`)
}
setDesktopNotifications(prevState => {
const notfiticationIndex = prevState.findIndex(
n => n.tag === notification.tag
)
if (notfiticationIndex !== -1) {
prevState[notfiticationIndex] = notification
return [...prevState]
} }
return [notification, ...prevState]
})
const notification = new Notification(
`${i18n.t("tickets.notification.message")} ${contact.name}`,
options
)
notification.onclick = e => {
e.preventDefault()
window.focus()
historyRef.current.push(`/tickets/${ticket.id}`)
}
setDesktopNotifications(prevState => {
const notfiticationIndex = prevState.findIndex(
n => n.tag === notification.tag
)
if (notfiticationIndex !== -1) {
prevState[notfiticationIndex] = notification
return [...prevState]
}
return [notification, ...prevState]
})
}else{
user.queues.forEach(queue =>{
if(queue.id === notify.data?.queue?.id){
isQueue = true;
}
})
if(!isQueue){
return;
}else {
const notification = new Notification(`${i18n.t("tickets.notification.messagePeding")} ${notify.data?.queue?.name}`);
notification.onclick = e => {
e.preventDefault()
window.focus()
historyRef.current.push(`/tickets`)
}
setDesktopNotifications(prevState => {
const notfiticationIndex = prevState.findIndex(
n => n.tag === notification.tag
)
if (notfiticationIndex !== -1) {
prevState[notfiticationIndex] = notification
return [...prevState]
}
return [notification, ...prevState]
})
}
}
soundAlertRef.current() soundAlertRef.current()
} }

View File

@ -16,7 +16,6 @@ import MessagesList from "../MessagesList"
import api from "../../services/api" import api from "../../services/api"
import { ReplyMessageProvider } from "../../context/ReplyingMessage/ReplyingMessageContext" import { ReplyMessageProvider } from "../../context/ReplyingMessage/ReplyingMessageContext"
import toastError from "../../errors/toastError" import toastError from "../../errors/toastError"
import { CountTicketMsgProvider } from "../../context/CountTicketMsgProvider/CountTicketMsgProvider"
const drawerWidth = 320 const drawerWidth = 320
@ -193,21 +192,11 @@ const Ticket = () => {
</div> </div>
</TicketHeader> </TicketHeader>
<ReplyMessageProvider> <ReplyMessageProvider>
<MessagesList
<CountTicketMsgProvider> ticketId={ticketId}
isGroup={ticket.isGroup}
<MessagesList ></MessagesList>
ticketId={ticketId} <MessageInput ticketStatus={ticket.status} />
isGroup={ticket.isGroup}
/>
<MessageInput
ticketStatus={ticket.status}
ticketLastMessage={ticket?.lastMessage}
ticketIsRemote={ticket?.isRemote} />
</CountTicketMsgProvider>
</ReplyMessageProvider> </ReplyMessageProvider>
</Paper> </Paper>
<ContactDrawer <ContactDrawer

View File

@ -22,7 +22,6 @@ import MarkdownWrapper from "../MarkdownWrapper"
import { Tooltip } from "@material-ui/core" import { Tooltip } from "@material-ui/core"
import { AuthContext } from "../../context/Auth/AuthContext" import { AuthContext } from "../../context/Auth/AuthContext"
import toastError from "../../errors/toastError" import toastError from "../../errors/toastError"
import openSocket from 'socket.io-client'
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
ticket: { ticket: {
@ -102,15 +101,13 @@ const useStyles = makeStyles(theme => ({
}, },
})) }))
const TicketListItem = ({ ticket, remoteTicketsControll, settings }) => { const TicketListItem = ({ ticket }) => {
const classes = useStyles() const classes = useStyles()
const history = useHistory() const history = useHistory()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { ticketId } = useParams() const { ticketId } = useParams()
const isMounted = useRef(true) const isMounted = useRef(true)
const { user, getSettingValue } = useContext(AuthContext) const { user } = useContext(AuthContext)
const [_remoteTicketsControll, setRemoteTicketsControll] = useState([])
const [_settings, setSettings] = useState(null)
useEffect(() => { useEffect(() => {
return () => { return () => {
@ -118,14 +115,6 @@ const TicketListItem = ({ ticket, remoteTicketsControll, settings }) => {
} }
}, []) }, [])
useEffect(() => {
setSettings(settings)
}, [settings])
useEffect(() => {
setRemoteTicketsControll(remoteTicketsControll)
}, [remoteTicketsControll, settings])
const handleAcepptTicket = async id => { const handleAcepptTicket = async id => {
setLoading(true) setLoading(true)
try { try {
@ -142,6 +131,8 @@ const TicketListItem = ({ ticket, remoteTicketsControll, settings }) => {
setLoading(false) setLoading(false)
} }
history.push(`/tickets/${id}`) history.push(`/tickets/${id}`)
} }
@ -149,192 +140,126 @@ const TicketListItem = ({ ticket, remoteTicketsControll, settings }) => {
history.push(`/tickets/${id}`) history.push(`/tickets/${id}`)
} }
useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL)
socket.on('remoteTickesControll', (data) => {
console.log('REMOTE TICKETS CONTROLL UPDATE2: ', data.tickets)
if (data.action === 'update') {
setRemoteTicketsControll(data.tickets)
}
})
socket.on('settings', (data) => {
if (data.action === 'update') {
setSettings((prevState) => {
const aux = [...prevState]
const settingIndex = aux.findIndex((s) => s.key === data.setting.key)
aux[settingIndex].value = data.setting.value
return aux
})
}
})
return () => {
socket.disconnect()
}
}, [])
return ( return (
<React.Fragment key={ticket.id}> <React.Fragment key={ticket.id}>
<Tooltip <ListItem
arrow dense
placement="right" button
title={ onClick={e => {
(ticket?.isRemote && ticket?.remoteDone && ticket.status === 'pending') ? "Mensagem de campanha enviada" : (ticket?.isRemote && ticket.status === 'pending') ? "Mensagem de campanha ainda não enviada" : if (ticket.status === "pending") return
"" handleSelectTicket(ticket.id)
} }}
selected={ticketId && +ticketId === ticket.id}
className={clsx(classes.ticket, {
[classes.pendingTicket]: ticket.status === "pending",
})}
> >
<ListItem <Tooltip
dense arrow
button placement="right"
onClick={e => { title={ticket.queue?.name || "Sem fila"}
if (ticket.status === "pending") return
handleSelectTicket(ticket.id)
}}
selected={ticketId && +ticketId === ticket.id}
className={clsx(classes.ticket, {
[classes.pendingTicket]: ticket.status === "pending",
})}
style={((ticket.status === "open" || ticket.status === "closed") && ticket?.isRemote) ? {
border: (ticket.status === "open" || ticket.status === "closed") ? "1px solid rgba(121,123,127,0.9)" : "1px solid transparent",
} : {}}
> >
<Tooltip <span
arrow style={{ backgroundColor: ticket.queue?.color || "#7C7C7C" }}
placement="right" className={classes.ticketQueueColor}
title={ticket.queue?.name || "Sem fila"} ></span>
> </Tooltip>
<span <ListItemAvatar>
style={{ backgroundColor: ticket.queue?.color || "#7C7C7C" }} <Avatar src={ticket?.contact?.profilePicUrl} />
className={classes.ticketQueueColor} </ListItemAvatar>
></span> <ListItemText
</Tooltip> disableTypography
<ListItemAvatar> primary={
<Avatar src={ticket?.contact?.profilePicUrl} /> <span className={classes.contactNameWrapper}>
</ListItemAvatar> <Typography
<ListItemText noWrap
disableTypography component="span"
primary={ variant="body2"
<span className={classes.contactNameWrapper}> color="textPrimary"
>
{ticket.contact.name}
</Typography>
{ticket.status === "closed" && (
<Badge
className={classes.closedBadge}
badgeContent={"closed"}
color="primary"
/>
)}
{ticket.lastMessage && (
<Typography <Typography
noWrap className={classes.lastMessageTime}
component="span"
variant="body2"
color="textPrimary"
>
{ticket.contact.name}
</Typography>
{ticket.status === "closed" && (
<Badge
className={classes.closedBadge}
badgeContent={"closed"}
color="primary"
/>
)}
{ticket.lastMessage && (
<Typography
className={classes.lastMessageTime}
component="span"
variant="body2"
color="textSecondary"
>
{ticket?.phoneNumberId && <span style={{ 'fontWeight': 'bold' }}>Oficial</span>}{" "}
{isSameDay(parseISO(ticket.updatedAt), new Date()) ? (
<>{format(parseISO(ticket.updatedAt), "HH:mm")}</>
) : (
<>{format(parseISO(ticket.updatedAt), "dd/MM/yyyy")}</>
)}
</Typography>
)}
</span>
}
secondary={
<span className={classes.contactNameWrapper}>
<Typography
className={classes.contactLastMessage}
noWrap
component="span" component="span"
variant="body2" variant="body2"
color="textSecondary" color="textSecondary"
> >
{ticket.lastMessage ? ( {ticket?.phoneNumberId && <span style={{ 'fontWeight': 'bold' }}>Oficial</span>}{" "}
<MarkdownWrapper>{ticket.lastMessage}</MarkdownWrapper> {isSameDay(parseISO(ticket.updatedAt), new Date()) ? (
<>{format(parseISO(ticket.updatedAt), "HH:mm")}</>
) : ( ) : (
<br /> <>{format(parseISO(ticket.updatedAt), "dd/MM/yyyy")}</>
)} )}
</Typography> </Typography>
)}
</span>
}
secondary={
<span className={classes.contactNameWrapper}>
<Typography
className={classes.contactLastMessage}
noWrap
component="span"
variant="body2"
color="textSecondary"
>
{ticket.lastMessage ? (
<MarkdownWrapper>{ticket.lastMessage}</MarkdownWrapper>
) : (
<br />
)}
</Typography>
<Badge <Badge
className={classes.newMessagesCount} className={classes.newMessagesCount}
badgeContent={+ticket.unreadMessages} badgeContent={+ticket.unreadMessages}
classes={{ classes={{
badge: classes.badgeStyle, badge: classes.badgeStyle,
}} }}
/> />
{/* <Badge {/* <Badge
className={classes.newMessagesCount} className={classes.newMessagesCount}
badgeContent={ticket.unreadMessages} badgeContent={ticket.unreadMessages}
classes={{ classes={{
badge: classes.badgeStyle, badge: classes.badgeStyle,
}} }}
/> */} /> */}
</span> </span>
} }
/> />
{ticket.status === "pending" && ( {ticket.status === "pending" && (
<ButtonWithSpinner
color="primary"
variant="contained"
className={classes.acceptButton}
size="small"
loading={loading}
onClick={e => handleAcepptTicket(ticket.id)}
>
{/* {i18n.t("ticketsList.buttons.accept")} */}
<>
{/* {i18n.t("ticketsList.buttons.accept")}<br />CAMPANHA */}
{ticket?.isRemote ? (
<>{i18n.t("ticketsList.buttons.accept")}<br />CAMPANHA</>
) : (
<>{i18n.t("ticketsList.buttons.accept")}</>
)}
<ButtonWithSpinner </>
// color="primary" </ButtonWithSpinner>
)}
{...((ticket?.isRemote) ? </ListItem>
((settings &&
settings.length > 0 &&
getSettingValue('remoteTicketSendControll') &&
getSettingValue('remoteTicketSendControll') === 'enabled') && !ticket?.remoteDone && !_remoteTicketsControll?.includes(+ticket.id)) ?
{ style: { backgroundColor: "rgba(121,123,127,0.5)", color: "white" } } :
{ style: { backgroundColor: "rgba(121,123,127,0.9)", color: "white" } } :
{ color: "primary" })}
variant="contained"
disabled={true ? ((settings &&
settings.length > 0 &&
getSettingValue('remoteTicketSendControll') &&
getSettingValue('remoteTicketSendControll') === 'enabled') && ticket?.isRemote && !ticket?.remoteDone && !_remoteTicketsControll?.includes(+ticket.id)) : false}
className={classes.acceptButton}
size="small"
loading={loading}
onClick={e => handleAcepptTicket(ticket.id)}
>
<>
{(ticket?.isRemote && !ticket?.remoteDone) ? (
<>{i18n.t("ticketsList.buttons.accept")}<br />CAMPANHA</>
) : (
<>{i18n.t("ticketsList.buttons.accept")}</>
)}
</>
</ButtonWithSpinner>
)}
</ListItem>
</Tooltip>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
</React.Fragment> </React.Fragment>
) )

View File

@ -15,7 +15,6 @@ import { i18n } from "../../translate/i18n"
import { AuthContext } from "../../context/Auth/AuthContext" import { AuthContext } from "../../context/Auth/AuthContext"
import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket" import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket"
import { Divider } from "@material-ui/core"
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
ticketsListWrapper: { ticketsListWrapper: {
@ -182,18 +181,10 @@ const TicketsList = (props) => {
const classes = useStyles() const classes = useStyles()
const [pageNumber, setPageNumber] = useState(1) const [pageNumber, setPageNumber] = useState(1)
const [ticketsList, dispatch] = useReducer(reducer, []) const [ticketsList, dispatch] = useReducer(reducer, [])
const { user, setting, } = useContext(AuthContext) const { user } = useContext(AuthContext)
const { searchTicket } = useContext(SearchTicketContext) const { searchTicket } = useContext(SearchTicketContext)
const [_remoteTicketsControll, setRemoteTicketsControll] = useState([])
const [settings, setSettings] = useState([])
useEffect(() => {
setSettings(setting)
}, [setting])
useEffect(() => { useEffect(() => {
dispatch({ type: "RESET" }) dispatch({ type: "RESET" })
@ -201,22 +192,17 @@ const TicketsList = (props) => {
}, [status, searchParam, searchParamContent, showAll, selectedQueueIds, searchTicket]) }, [status, searchParam, searchParamContent, showAll, selectedQueueIds, searchTicket])
let { tickets, hasMore, loading, remoteTicketsControll } = useTickets({ const { tickets, hasMore, loading } = useTickets({
pageNumber, pageNumber,
searchParam, searchParam,
searchParamContent, searchParamContent,
status, status,
showAll, showAll,
queueIds: JSON.stringify(selectedQueueIds), queueIds: JSON.stringify(selectedQueueIds),
tab, tab,
unlimited: status === 'open' ? "all" : "false" unlimited: status === 'open' ? "all" : "false"
}) })
useEffect(() => {
setRemoteTicketsControll(remoteTicketsControll)
}, [remoteTicketsControll])
useEffect(() => { useEffect(() => {
if (!status && !searchParam) return if (!status && !searchParam) return
@ -315,27 +301,6 @@ const TicketsList = (props) => {
} }
}) })
socket.on('remoteTickesControll', (data) => {
console.log('REMOTE TICKETS CONTROLL UPDATE 1: ', data.tickets)
if (data.action === 'update') {
setRemoteTicketsControll(data.tickets)
}
})
socket.on('settings', (data) => {
if (data.action === 'update') {
setSettings((prevState) => {
const aux = [...prevState]
const settingIndex = aux.findIndex((s) => s.key === data.setting.key)
aux[settingIndex].value = data.setting.value
return aux
})
}
})
return () => { return () => {
socket.disconnect() socket.disconnect()
} }
@ -389,7 +354,7 @@ const TicketsList = (props) => {
) : ( ) : (
<> <>
{ticketsList.map(ticket => ( {ticketsList.map(ticket => (
<TicketListItem ticket={ticket} key={ticket.id} remoteTicketsControll={_remoteTicketsControll} settings={settings} /> <TicketListItem ticket={ticket} key={ticket.id} />
))} ))}
</> </>
)} )}

View File

@ -33,6 +33,9 @@ import { Button } from "@material-ui/core";
import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption"; import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption";
import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket"; import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket";
import useTickets from "../../hooks/useTickets"
import api from "../../services/api";
import toastError from "../../errors/toastError";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
ticketsWrapper: { ticketsWrapper: {
@ -157,6 +160,10 @@ const TicketsManager = () => {
const [openTooltipSearch, setOpenTooltipSearch] = useState(false) const [openTooltipSearch, setOpenTooltipSearch] = useState(false)
const [waitingTime, setWaitingTime] = useState('00:00');
const [tickets, setTickets] = useState([]);
const [settings, setSettings] = useState([])
let searchTimeout; let searchTimeout;
let searchContentTimeout; let searchContentTimeout;
@ -178,6 +185,76 @@ const TicketsManager = () => {
}, [tab, setTabOption]); }, [tab, setTabOption]);
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
}
const fetchTickets = async () =>{
try {
const { data } = await api.get("/tickets", {
params: {
status: 'pending',
queueIds: JSON.stringify(selectedQueueIds)
},
});
setTickets(data.tickets);
} catch (err) {
toastError(err);
}
}
useEffect(() => {
if(settings?.length > 0 && getSettingValue('waitingTimeTickets') === 'enabled') {
fetchTickets();
const intervalId = setInterval(fetchTickets, 7000);
return () => {
clearInterval(intervalId);
};
}
}, [selectedQueueIds, settings]);
useEffect(() => {
const calculateAverageTime = () => {
if(tickets.length > 0){
const now = new Date();
const differenceTime = tickets?.map(ticket => {
const updatedAt = new Date(ticket.updatedAt);
const difference = now - updatedAt;
return difference;
});
const sumDifferences = differenceTime.reduce((total, difference) => total + difference, 0);
const averageTimeMilliseconds = sumDifferences / tickets?.length;
let hours = Math.floor(averageTimeMilliseconds / 3600000);
const minutes = Math.floor((averageTimeMilliseconds % 3600000) / 60000);
let days = hours >= 24 ? parseInt(hours/24) : '';
if(days != '') hours = hours - (24*days);
const averageTimeFormated = `${days != '' ? `${days}d ` : days}${hours.toString().padStart(2, '0')}h${minutes.toString().padStart(2, '0')}`;
return averageTimeFormated;
}else return '00:00';
}
setWaitingTime(calculateAverageTime());
},[tickets]);
useEffect(() => { useEffect(() => {
// clearTimeout(searchContentTimeout); // clearTimeout(searchContentTimeout);
@ -203,7 +280,7 @@ const TicketsManager = () => {
useEffect(() => { useEffect(() => {
//console.log(selectedQueueIds);
if (tabOption === 'open') { if (tabOption === 'open') {
setTabOption('') setTabOption('')
@ -448,7 +525,17 @@ const TicketsManager = () => {
</Badge> </Badge>
} }
value={"pending"} value={"pending"}
/> />{
(settings?.length > 0 && getSettingValue('waitingTimeTickets') === 'enabled') &&
<span style={{display: 'flex', alignItems: 'center', flexDirection:'column', justifyContent: 'flex-start'}}>
<label style ={{color: 'red',fontWeight: 'bold', padding: '.1rem', fontSize: '8px', textAlign:'center', margin:'0'}}>
<i>ESPERA</i>
</label>
<label style={{color: 'gray',fontWeight: 'bold', padding: '.1rem', textDecoration: 'underline', fontSize: '13px'}}>
{waitingTime}
</label>
</span>
}
</Tabs> </Tabs>
<Paper className={classes.ticketsWrapper}> <Paper className={classes.ticketsWrapper}>
<TicketsList <TicketsList

View File

@ -1,17 +0,0 @@
import React, { useState, createContext } from "react"
const countTicketMsgContext = createContext()
const CountTicketMsgProvider = ({ children }) => {
const [countTicketMsg, setCountTicketMsg] = useState(0)
return (
<countTicketMsgContext.Provider value={{ countTicketMsg, setCountTicketMsg }}>
{children}
</countTicketMsgContext.Provider>
)
}
export { countTicketMsgContext, CountTicketMsgProvider }

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from "react" import { useState, useEffect } from "react";
import toastError from "../../errors/toastError" import toastError from "../../errors/toastError";
import api from "../../services/api" import api from "../../services/api";
const useTickets = ({ const useTickets = ({
searchParam, searchParam,
@ -15,21 +15,21 @@ const useTickets = ({
unlimited, unlimited,
tab tab
}) => { }) => {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true);
const [hasMore, setHasMore] = useState(false) const [hasMore, setHasMore] = useState(false);
const [tickets, setTickets] = useState([]) const [tickets, setTickets] = useState([]);
const [remoteTicketsControll, setRemoteTicketsControll] = useState([])
useEffect(() => { useEffect(() => {
setLoading(true) setLoading(true);
const delayDebounceFn = setTimeout(() => { const delayDebounceFn = setTimeout(() => {
const fetchTickets = async () => { const fetchTickets = async () => {
try { try {
if ((tab === 'search') && (!searchParam || searchParam.trim().length === 0 || searchParam.trim().length > 40 || searchParam.endsWith(' '))) { if ((tab === 'search') && ( !searchParam || searchParam.trim().length === 0 || searchParam.trim().length >40 || searchParam.endsWith(' '))) {
return return
} }
@ -45,26 +45,24 @@ const useTickets = ({
withUnreadMessages, withUnreadMessages,
unlimited unlimited
}, },
}) });
setTickets(data.tickets)
setHasMore(data.hasMore)
setLoading(false)
if (data?.remoteTicketsControll) {
setRemoteTicketsControll(data.remoteTicketsControll.map(t => +t))
}
setTickets(data.tickets);
setHasMore(data.hasMore);
setLoading(false);
} catch (err) { } catch (err) {
setLoading(false) setLoading(false);
toastError(err) toastError(err);
} }
} };
fetchTickets() fetchTickets();
}, 500) }, 500);
return () => clearTimeout(delayDebounceFn) return () => clearTimeout(delayDebounceFn);
}, [ }, [
searchParam, searchParam,
searchParamContent, searchParamContent,
@ -76,9 +74,9 @@ const useTickets = ({
withUnreadMessages, withUnreadMessages,
tab, tab,
unlimited unlimited
]) ]);
return { tickets, loading, hasMore, remoteTicketsControll } return { tickets, loading, hasMore };
} };
export default useTickets export default useTickets;

View File

@ -11,7 +11,7 @@ import { AuthContext } from "../../context/Auth/AuthContext"
import { Can } from "../../components/Can" import { Can } from "../../components/Can"
import FormControlLabel from "@mui/material/FormControlLabel" import FormControlLabel from "@mui/material/FormControlLabel"
import Checkbox from '@mui/material/Checkbox' import Checkbox from '@mui/material/Checkbox'
import { Button, Tooltip } from "@material-ui/core" import { Button } from "@material-ui/core"
import ReportModal from "../../components/ReportModal" import ReportModal from "../../components/ReportModal"
import ReportModalType from "../../components/ReportModalType" import ReportModalType from "../../components/ReportModalType"
import MaterialTable from 'material-table' import MaterialTable from 'material-table'
@ -237,7 +237,8 @@ let columnsData = [
{ title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' }, { title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' },
{ title: `Espera`, field: 'waiting_time' }, { title: `Espera`, field: 'waiting_time' },
{ title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true }, { title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true },
] { title: `Link`, field: 'link', searchable: false, hidden: true, export: true },
]
let columnsDataSuper = [ let columnsDataSuper = [
{ title: `${i18n.t("reports.listColumns.column1_1")}`, field: 'whatsapp.name' }, { title: `${i18n.t("reports.listColumns.column1_1")}`, field: 'whatsapp.name' },
@ -252,6 +253,7 @@ let columnsDataSuper = [
{ title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' }, { title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' },
{ title: `Espera`, field: 'waiting_time' }, { title: `Espera`, field: 'waiting_time' },
{ title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true }, { title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true },
{ title: `Link`, field: 'link', searchable: false, hidden: true, export: true },
] ]
@ -299,7 +301,6 @@ const Report = () => {
const [csvFile, setCsvFile] = useState() const [csvFile, setCsvFile] = useState()
const [selectedValue, setSelectedValue] = useState('created') const [selectedValue, setSelectedValue] = useState('created')
const [checked, setChecked] = useState(true) const [checked, setChecked] = useState(true)
const [checkedRemote, setCheckedRemote] = useState(false)
const [queues, setQueues] = useState([]) const [queues, setQueues] = useState([])
const [queueId, setQueue] = useState(null) const [queueId, setQueue] = useState(null)
@ -364,7 +365,7 @@ const Report = () => {
if (reportOption === '1') { if (reportOption === '1') {
const { data } = await api.get("/reports/", { params: { userId, startDate, endDate, pageNumber: pageNumberTickets, createdOrUpdated: selectedValue, queueId }, userQueues: userA.queues }) const { data } = await api.get("/reports/", { params: { userId, startDate, endDate, pageNumber: pageNumberTickets, createdOrUpdated: selectedValue, queueId }, userQueues: userA.queues })
let ticketsQueue = data.tickets let ticketsQueue = data.tickets
let userQueues = userA.queues let userQueues = userA.queues
let filterQueuesTickets = [] let filterQueuesTickets = []
@ -377,9 +378,9 @@ const Report = () => {
const tickets = data.tickets.map(ticket => ({ const tickets = data.tickets.map(ticket => ({
...ticket, ...ticket,
messagesToFilter: ticket.messages.map(message => message.body).join(' '), messagesToFilter: ticket.messages.map(message => message.body).join(' '),
link: `${process.env.REACT_APP_FRONTEND_URL}/tickets/${ticket.id}`
})) }))
dispatchQ({ type: "LOAD_QUERY", payload: tickets }) dispatchQ({ type: "LOAD_QUERY", payload: tickets })
// console.log(tickets)
setHasMore(data.hasMore) setHasMore(data.hasMore)
setTotalCountTickets(data.count) setTotalCountTickets(data.count)
setLoading(false) setLoading(false)
@ -396,13 +397,15 @@ const Report = () => {
} }
else if (reportOption === '3') { else if (reportOption === '3') {
const dataQuery = await api.get("/reports/services/numbers", { params: { startDate, endDate, isRemote: checkedRemote }, }) const dataQuery = await api.get("/reports/services/numbers", { params: { startDate, endDate }, })
dispatchQ({ type: "RESET" }) dispatchQ({ type: "RESET" })
dispatchQ({ type: "LOAD_QUERY", payload: dataQuery?.data?.reportService }) dispatchQ({ type: "LOAD_QUERY", payload: dataQuery?.data?.reportService })
} }
else if (reportOption === '4') { else if (reportOption === '4') {
const dataQuery = await api.get("/reports/services/queues", { params: { startDate, endDate, isRemote: checkedRemote }, }) const dataQuery = await api.get("/reports/services/queues", { params: { startDate, endDate }, })
console.log(' dataQuery?.data?.reportService: ', dataQuery?.data?.reportService)
dispatchQ({ type: "RESET" }) dispatchQ({ type: "RESET" })
dispatchQ({ type: "LOAD_QUERY", payload: dataQuery?.data?.reportService }) dispatchQ({ type: "LOAD_QUERY", payload: dataQuery?.data?.reportService })
@ -418,7 +421,7 @@ const Report = () => {
}, 500) }, 500)
return () => clearTimeout(delayDebounceFn) return () => clearTimeout(delayDebounceFn)
}, [userId, queueId, checked, checkedRemote, startDate, endDate, reportOption, pageNumberTickets, totalCountTickets, selectedValue]) }, [userId, queueId, checked, startDate, endDate, reportOption, pageNumberTickets, totalCountTickets, selectedValue])
const handleCheckBoxChange = (value) => { const handleCheckBoxChange = (value) => {
@ -459,7 +462,7 @@ const Report = () => {
} }
// Get from report type option // Get from report type option
const reportTypeValue = (data) => { const reportTypeValue = (data) => {
let type = '1' let type = '1'
if (data === '1') type = 'default' if (data === '1') type = 'default'
if (data === '2') type = 'synthetic' if (data === '2') type = 'synthetic'
@ -680,54 +683,56 @@ const Report = () => {
const renderSwitch = (param) => { const renderSwitch = (param) => {
switch (param) { if(userA.profile !== 'supervisor'){
case 'empty': switch (param) {
return ( case 'empty':
<> return (
{query && query.length > 0 && <>
<ReportModalType currencies={reportTypeList} func={reportTypeValue} reportOption={reportType} /> {query && query.length > 0 &&
} <ReportModalType currencies={reportTypeList} func={reportTypeValue} reportOption={reportType} />
{/* <Button }
disabled={query && query.length > 0 ? false : true} {/* <Button
variant="contained" disabled={query && query.length > 0 ? false : true}
color="primary" variant="contained"
onClick={(e) => { color="primary"
handleCSVMessages() onClick={(e) => {
}} handleCSVMessages()
> }}
{"CSV ALL"} >
{"CSV ALL"}
</Button> */}
</>) </Button> */}
</>)
case 'pending' || 'processing': case 'pending' || 'processing':
return ( return (
<> <>
<span>PROCESSING...</span> <span>PROCESSING...</span>
</>) </>)
case 'success': case 'success':
return ( return (
<> <>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={(e) => { onClick={(e) => {
handleCSVDownload(e) handleCSVDownload(e)
}} }}
> >
{'CSV DOWNLOAD'} {'CSV DOWNLOAD'}
</Button> </Button>
</>) </>)
case 'downloading': case 'downloading':
return ( return (
<> <>
<span>DOWNLOADING...</span> <span>DOWNLOADING...</span>
</>) </>)
default: default:
return (<><span>WAITING...</span></>) return (<><span>WAITING...</span></>)
}
} }
} }
@ -736,10 +741,6 @@ const Report = () => {
setChecked(event.target.checked) setChecked(event.target.checked)
} }
const handleChangeRemote = (event) => {
setCheckedRemote(event.target.checked)
}
return ( return (
<Can <Can
@ -785,35 +786,6 @@ const Report = () => {
</label> </label>
</div> </div>
} }
{(reportOption === '3' || reportOption === '4') &&
<div>
<Tooltip
arrow
placement="top"
title="Todos os tickets incluindo os criados remotamente"
><label>
normal
</label>
</Tooltip>
<Switch
checked={checkedRemote}
onChange={handleChangeRemote}
inputProps={{ 'aria-label': 'controlled' }}
/>
<Tooltip
arrow
placement="top"
title="Apenas tickets criados remotamente"
><label>
remoto
</label>
</Tooltip>
</div>
}
@ -871,7 +843,7 @@ const Report = () => {
<> <>
<MTable data={query} <MTable data={query}
columns={userA.profile !== 'supervisor' ? columnsData : columnsDataSuper} columns={userA.profile !== 'supervisor' ? columnsData : columnsDataSuper}
hasChild={true} hasChild={userA.profile !== 'supervisor' ? true :false}
removeClickRow={false} removeClickRow={false}
handleScroll={handleScroll} handleScroll={handleScroll}
@ -991,7 +963,7 @@ const Report = () => {
title={i18n.t("reports.listTitles.title4_1")} title={i18n.t("reports.listTitles.title4_1")}
columns={ columns={
!checkedRemote ? [ [
{ title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, }, { title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, },
{ title: 'Conversas iniciadas', field: 'startedByAgent', }, { title: 'Conversas iniciadas', field: 'startedByAgent', },
{ title: 'Conversas recebidas', field: 'startedByClient' }, { title: 'Conversas recebidas', field: 'startedByClient' },
@ -999,15 +971,7 @@ const Report = () => {
{ title: `Tempo médio de espera`, field: 'avgChatWaitingTime' }, { title: `Tempo médio de espera`, field: 'avgChatWaitingTime' },
{ title: 'Aguardando', field: 'pendingChat' } { title: 'Aguardando', field: 'pendingChat' }
] : ]
[
{ title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, },
{ title: 'Conversas iniciadas', field: 'startedByAgent', },
{ title: 'Conversas respondidas', field: 'startedByClient' },
{ title: `Conversas finalizadas`, field: 'closedChat' },
{ title: 'Aguardando', field: 'pendingChat' }
]
} }
data={dataRows} data={dataRows}
@ -1043,7 +1007,7 @@ const Report = () => {
title={i18n.t("reports.listTitles.title5_1")} title={i18n.t("reports.listTitles.title5_1")}
columns={ columns={
!checkedRemote ? [ [
{ title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, }, { title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, },
{ {
@ -1063,28 +1027,7 @@ const Report = () => {
{ title: `Tempo médio de espera`, field: 'avgChatWaitingTime' }, { title: `Tempo médio de espera`, field: 'avgChatWaitingTime' },
{ title: 'Aguardando', field: 'pendingChat' } { title: 'Aguardando', field: 'pendingChat' }
] : ]
[
{ title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, },
{
title: 'Fila', field: 'queueName',
cellStyle: (evt, rowData) => {
return {
whiteSpace: 'nowrap',
backgroundColor: rowData?.queueColor || 'inherit',
color: 'white'
}
}
},
{ title: 'Conversas iniciadas', field: 'startedByAgent', },
{ title: 'Conversas respondidas', field: 'startedByClient' },
{ title: `Conversas finalizadas`, field: 'closedChat' },
{ title: 'Aguardando', field: 'pendingChat' }
]
} }
data={dataRows} data={dataRows}

View File

@ -12,38 +12,13 @@ import api from '../../services/api'
import { i18n } from '../../translate/i18n.js' import { i18n } from '../../translate/i18n.js'
import toastError from '../../errors/toastError' import toastError from '../../errors/toastError'
import TextField from '@material-ui/core/TextField'
import Button from '@material-ui/core/Button'
//-------- //--------
import { AuthContext } from '../../context/Auth/AuthContext' import { AuthContext } from '../../context/Auth/AuthContext'
import { Can } from '../../components/Can' import { Can } from '../../components/Can'
import { boolean } from 'yup'
// import Button from "@material-ui/core/Button"; // import Button from "@material-ui/core/Button";
const IntegerInput = ({ value, onChange }) => {
const handleChange = (event) => {
const inputValue = event.target.value
// Only allow digits 0-9
if (/^\d{0,3}$/.test(inputValue)) {
onChange(inputValue)
}
}
return (
<TextField
type="text"
variant="outlined"
value={value}
onChange={handleChange}
style={{ marginRight: '10px' }}
/>
)
}
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
display: 'flex', display: 'flex',
@ -73,48 +48,11 @@ const Settings = () => {
const [settings, setSettings] = useState([]) const [settings, setSettings] = useState([])
const [number1, setNumber1] = useState('')
const [number2, setNumber2] = useState('')
const handleNumber1Change = (value) => {
setNumber1(value)
}
const handleNumber2Change = (value) => {
setNumber2(value)
}
const handleGetValues = () => {
let e = {
target: {
value: 'enabled', name: 'remoteTicketSendControll', obj: (number1.trim().length > 0 && number2.trim().length > 0) ? { seconds1: number1, seconds2: number2 } : null
}
}
handleChangeSetting(e)
}
useEffect(() => { useEffect(() => {
const fetchSession = async () => { const fetchSession = async () => {
try { try {
const { data } = await api.get('/settings') const { data } = await api.get('/settings')
console.log('data.settings: ', data.settings)
setSettings(data.settings) setSettings(data.settings)
if (data?.settings) {
let { obj } = data.settings.find((s) => s.key === 'remoteTicketSendControll')
if (!obj) return
obj = JSON.parse(obj)
console.log('SETTING obj: ', obj)
setNumber1(obj.seconds1)
setNumber2(obj.seconds2)
}
} catch (err) { } catch (err) {
toastError(err) toastError(err)
} }
@ -122,7 +60,6 @@ const Settings = () => {
fetchSession() fetchSession()
}, []) }, [])
useEffect(() => { useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL) const socket = openSocket(process.env.REACT_APP_BACKEND_URL)
@ -144,15 +81,17 @@ const Settings = () => {
} }
}, []) }, [])
// useEffect(() => {
// console.log('------> settings: ', settings)
// }, [settings])
const handleChangeSetting = async (e) => { const handleChangeSetting = async (e) => {
const selectedValue = e.target.value const selectedValue = e.target.value
const settingKey = e.target.name const settingKey = e.target.name
try { try {
await api.put(`/settings/${settingKey}`, { await api.put(`/settings/${settingKey}`, {
value: selectedValue, value: selectedValue,
// obj: e.target?.obj ? e.target.obj : null
}) })
if (settingKey === 'farewellMessageByQueue' && if (settingKey === 'farewellMessageByQueue' &&
@ -178,20 +117,12 @@ const Settings = () => {
} }
} }
const getSettingValue = (key, _obj = false) => { const getSettingValue = (key) => {
const { value, obj } = settings.find((s) => s.key === key) const { value } = settings.find((s) => s.key === key)
if (_obj)
return obj
return value return value
} }
const isSaveDisabled = (settings &&
settings.length > 0 &&
getSettingValue('remoteTicketSendControll') === 'disabled')
return ( return (
<Can <Can
role={user.profile} role={user.profile}
@ -515,57 +446,10 @@ const Settings = () => {
</Container> </Container>
</div> </div>
<div className={classes.root}>
<Container className={classes.container} maxWidth="sm">
<Paper className={classes.paper}>
<Typography variant="body1">
Controle de envio de mensagem de ticket remoto por numero
</Typography>
<Select
margin="dense"
variant="outlined"
native
id="remoteTicketSendControll-setting"
name="remoteTicketSendControll"
value={
settings &&
settings.length > 0 &&
getSettingValue('remoteTicketSendControll')
}
className={classes.settingOption}
onChange={handleChangeSetting}
>
<option value="enabled">Ativado</option>
<option value="disabled">Desativado</option>
</Select>
</Paper>
{/* <Paper>
<div style={{ padding: '10px', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<h3>Tempo aleatorio em segundos</h3>
<div style={{ marginBottom: '20px', display: 'flex' }}>
<IntegerInput title="Number 1" value={number1} onChange={handleNumber1Change} />
<IntegerInput title="Number 2" value={number2} onChange={handleNumber2Change} />
<Button variant="contained" color="primary" onClick={handleGetValues} disabled={
(isSaveDisabled) ? true : false
}>
Save
</Button>
</div>
</div>
</Paper> */}
</Container>
</div>
</div> </div>
)} )}
/> />
) )
} }
export default Settings export default Settings