Atualização: ao criar ticket associar a uma fila, busca por ticket e conteudo, trasnferencia de ticket sem necessidade de escolher usuario, correção de notificações de mensagens recebidas para ter o mesmo comportamento do whatsapp web.

pull/21/head
adriano 2023-07-12 11:54:29 -03:00
parent 1246f59441
commit 92f7d8b4db
24 changed files with 960 additions and 433 deletions

View File

@ -176,7 +176,7 @@ client.on("qr", async qr => {
// omnihit.qrcode(process.env.MOBILEUID, process.env.MOBILENAME, qr);
// omnihit.monitor(process.env.MOBILEUID, process.env.MOBILENAME, "STARTUP");
asking_qrcode = true
asking_qrcode = true
await new Promise((resolve, reject) => {
@ -193,8 +193,8 @@ client.on("qr", async qr => {
}
});
})
})
let url = process.env.CLIENT_URL + '/whatsapp/connection/qrcode'
try {
@ -605,6 +605,37 @@ app.post('/api/restore', async (req, res) => {
res.status(200).json({ message: "ok" });
})
app.post('/api/sendSeen', async (req, res) => {
let stat
const { number } = req.body
try {
stat = await client.getState();
// await syncUnreadMessages(client)
const wbotChat = await client.getChatById(number);
wbotChat.sendSeen();
// const chatMessages = await wbotChat.fetchMessages({ limit: 100 });
// console.log('=============> wbotChat: ', chatMessages)
} catch (err) {
let terr = err.message;
stat = (terr.search('Session closed') > -1 ? 'SESSIONCLOSED' : 'UNKNOWN')
}
res.status(200).json({ message: "ok" });
})
app.get('/api/connection/status', async (req, res) => {
let stat
@ -636,6 +667,8 @@ const syncUnreadMessages = async (wbot) => {
/* eslint-disable no-await-in-loop */
for (const chat of chats) {
// console.log('chat: ', chat)
if (chat.unreadCount > 0) {
const unreadMessages = await chat.fetchMessages({
@ -698,7 +731,7 @@ const getWbotMessage = async (messageId, number, limit,) => {
}
async function whatsappMonitor(newState, omnihit_url, data) {
async function whatsappMonitor(newState, omnihit_url, data) {
const whatsapp = await whatsappUpdateStatus(newState)

View File

@ -19,9 +19,10 @@ import ptBR from 'date-fns/locale/pt-BR';
import { splitDateTime } from "../helpers/SplitDateTime";
import format from 'date-fns/format';
import ListTicketsServiceCache from "../services/TicketServices/ListTicketServiceCache";
import ListTicketsServiceCache from "../services/TicketServices/ListTicketServiceCache";
import { searchTicketCache, loadTicketsCache, } from '../helpers/TicketCache'
import { Op } from "sequelize";
@ -34,6 +35,7 @@ type IndexQuery = {
withUnreadMessages: string;
queueIds: string;
unlimited?: string;
searchParamContent?: string
};
interface TicketData {
@ -41,6 +43,10 @@ interface TicketData {
status: string;
queueId: number;
userId: number;
whatsappId?: string | number
msg?: string,
transfer?: boolean | undefined,
fromMe?: boolean
}
@ -51,6 +57,16 @@ import TicketEmiterSumOpenClosedByUser from "../helpers/OnlineReporEmiterInfoByU
import CountTicketService from "../services/TicketServices/CountTicketService";
import CountTicketsByUserQueue from "../services/UserServices/CountTicketsByUserQueue";
import ShowUserService from "../services/UserServices/ShowUserService";
import axios from "axios";
import User from "../models/User";
import CheckContactOpenTickets from "../helpers/CheckContactOpenTickets";
import QueuesByUser from "../services/UserServices/ShowQueuesByUser";
import GetDefaultWhatsApp from "../helpers/GetDefaultWhatsApp";
import { getWbot } from "../libs/wbot";
import endPointQuery from "../helpers/old_EndPointQuery";
import Contact from "../models/Contact";
import BotIsOnQueue from "../helpers/BotIsOnQueue";
import { setMessageAsRead } from "../helpers/SetMessageAsRead";
export const index = async (req: Request, res: Response): Promise<Response> => {
@ -62,7 +78,8 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
showAll,
queueIds: queueIdsStringified,
withUnreadMessages,
unlimited
unlimited,
searchParamContent
} = req.query as IndexQuery;
@ -83,26 +100,33 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
userId,
queueIds,
withUnreadMessages,
unlimited
unlimited,
searchParamContent
});
return res.status(200).json({ tickets, count, hasMore });
};
export const store = async (req: Request, res: Response): Promise<Response> => {
const { contactId, status, userId }: TicketData = req.body;
console.log('TICKET CREATE: ', 'contactId: ', contactId, ' | status: ', status, ' | userId: ', userId)
const { contactId, status, userId, msg, queueId }: TicketData = req.body;
// test del
let ticket = await Ticket.findOne({ where: { contactId, status: 'queueChoice' } });
const botInfo = await BotIsOnQueue('botqueue')
let ticket = await Ticket.findOne({
where: {
[Op.or]: [
{ contactId, status: 'queueChoice' },
{ contactId, status: 'open', userId: botInfo.userIdBot }
]
}
});
if (ticket) {
await UpdateTicketService({ ticketData: { status: 'open', userId: userId, }, ticketId: ticket.id });
await UpdateTicketService({ ticketData: { status: 'open', userId: userId, queueId }, ticketId: ticket.id });
}
else {
ticket = await CreateTicketService({ contactId, status, userId });
ticket = await CreateTicketService({ contactId, status, userId, queueId });
}
const io = getIO();
@ -140,7 +164,6 @@ export const show = async (req: Request, res: Response): Promise<Response> => {
return res.status(200).json({ contact, statusChatEnd, schedulesContact });
};
export const count = async (req: Request, res: Response): Promise<Response> => {
// type indexQ = { status: string; date?: string; };
@ -151,11 +174,11 @@ export const count = async (req: Request, res: Response): Promise<Response> => {
return res.status(200).json(ticketCount);
};
export const update = async (req: Request, res: Response): Promise<Response> => {
console.log('ENTROU NO UPDATE TICKET CONTROLLER')
const { ticketId } = req.params;
const userOldInfo = await Ticket.findByPk(ticketId)
@ -177,10 +200,6 @@ export const update = async (req: Request, res: Response): Promise<Response> =>
});
///////////////////////////////
//
if (scheduleData.farewellMessage) {
const whatsapp = await ShowWhatsAppService(ticket.whatsappId);
@ -190,11 +209,11 @@ export const update = async (req: Request, res: Response): Promise<Response> =>
await SendWhatsAppMessage({ body: farewellMessage, ticket });
}
}
// lembrete // agendamento
if (scheduleData.statusChatEndId === '2' || scheduleData.statusChatEndId === '3') {
if (isScheduling(scheduleData.schedulingDate, scheduleData.schedulingTime)) {
console.log('*** É AGENDAMENTO!')
@ -219,15 +238,52 @@ export const update = async (req: Request, res: Response): Promise<Response> =>
}
else {
const ticketData: TicketData = req.body;
// Para aparecer pendente para todos usuarios que estao na fila
if (req.body.transfer) {
req.body.userId = null
}
let ticketData: TicketData = req.body;
// console.log('ticketData: ', ticketData)
// console.log('ticketData.transfer', ticketData.transfer)
// return res.send()
// if (ticketData.transfer) {
// const defaultWhatsapp: any = await GetDefaultWhatsApp(ticketData.userId);
// const _ticket: any = await Ticket.findByPk(ticketId)
// if (defaultWhatsapp && ticketData.status != 'open') {
// await CheckContactOpenTickets(_ticket.dataValues.contactId, defaultWhatsapp.dataValues.id)
// }
// ticketData.whatsappId = defaultWhatsapp.dataValues.id
// }
console.log('--------> ticketData.status: ', ticketData.status, ' | ticketData.fromMe: ', ticketData.fromMe)
//ticketData: { status: 'open', userId: 4 } , ticketId
const { ticket } = await UpdateTicketService({
ticketData,
ticketId
ticketId,
});
if (ticketData.status == 'open' && !ticketData.fromMe) {
await setMessageAsRead(ticket);
}
console.log('ticket.unreadMessages: ', ticket.unreadMessages)
if (ticketData.userId) {
const dateToday = splitDateTime(new Date(format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ptBR })))
@ -246,7 +302,7 @@ export const update = async (req: Request, res: Response): Promise<Response> =>
const dateToday = splitDateTime(new Date(format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ptBR })))
if (userOldInfo.userId) {
if (userOldInfo.userId) {
TicketEmiterSumOpenClosedByUser(userOldInfo.userId.toString(), dateToday.fullDate, dateToday.fullDate)
@ -308,3 +364,11 @@ export const remove = async (
return res.status(200).json({ message: "ticket deleted" });
};
// export async function setMessageAsRead(ticket: Ticket) {
// const wbot_url = await getWbot(ticket.whatsappId);
// console.log('wbot_url: ', wbot_url, ' | ticket.contact.number: ', ticket.contact.number);
// await endPointQuery(`${wbot_url}/api/sendSeen`, { number: `${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us` });
// }

View File

@ -10,6 +10,9 @@ import path from "path";
import { convertBytes } from "./ConvertBytes";
import { deleteScheduleByTicketIdCache } from "./SchedulingNotifyCache";
import SchedulingNotify from "../models/SchedulingNotify";
import Ticket from "../models/Ticket";
import { Sequelize, Op } from "sequelize";
const fastFolderSize = require('fast-folder-size')
const { promisify } = require('util')
@ -41,33 +44,39 @@ const monitor = async () => {
try {
const { schedulingNotifies, count, hasMore } = await ListSchedulingNotifyService({ searchParam: dateParm, pageNumber: "1" });
if (schedulingNotifies && schedulingNotifies.length > 0) {
for (let i = 0; i < schedulingNotifies.length; i++) {
if (schedulingNotifies && schedulingNotifies.length > 0) {
for (let i = 0; i < schedulingNotifies.length; i++) {
const ticket: any = await ShowTicketService(+schedulingNotifies[i].ticketId);
let _ticket = await Ticket.findOne({
where: {
contactId: ticket.contactId,
status: { [Op.in]: ['open', 'pending'] }
}
})
await deleteScheduleByTicketIdCache(schedulingNotifies[i].ticketId)
await DeleteSchedulingNotifyService(schedulingNotifies[i].id)
if (_ticket) continue
if (ticket.dataValues.status == 'closed') {
await ticket.update({ status: 'pending' })
}
const ticket = await ShowTicketService(+schedulingNotifies[i].ticketId);
await new Promise(f => setTimeout(f, 3000));
if(!ticket.queue){
await ticket.update({status: 'open'})
}
// SetTicketMessagesAsRead(ticket);
await SendWhatsAppMessage({
body: schedulingNotifies[i].message, ticket
});
await deleteScheduleByTicketIdCache(schedulingNotifies[i].ticketId)
await DeleteSchedulingNotifyService(schedulingNotifies[i].id)
}
}
}
@ -173,7 +182,7 @@ _fifo = setInterval(SchedulingNotifySendMessage, 5000);
module.exports = SchedulingNotifySendMessage

View File

@ -2,31 +2,19 @@ import { getIO } from "../libs/socket";
import Ticket from "../models/Ticket";
function sendWhatsAppMessageSocket(ticket: Ticket, body: string, quotedMsgSerializedId?: string | undefined) {
function sendWhatsAppMessageSocket(ticket: Ticket, body: string, quotedMsgSerializedId?: string | undefined, number?: string ) {
const io = getIO();
io.to(`session_${ticket.whatsappId.toString()}`).emit("send_message", {
action: "create",
msg: {
number: `${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`,
number: number ? number : `${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`,
body: body,
quotedMessageId: quotedMsgSerializedId,
linkPreview: false
}
});
// io.emit("send_message", {
// action: "create",
// msg: {
// number: `${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`,
// body: body,
// quotedMessageId: quotedMsgSerializedId,
// linkPreview: false
// }
// });
});
}

View File

@ -0,0 +1,12 @@
import { getWbot } from "../libs/wbot";
import Ticket from "../models/Ticket";
import endPointQuery from "./old_EndPointQuery";
export async function setMessageAsRead(ticket: Ticket) {
const wbot_url = await getWbot(ticket.whatsappId);
console.log('from wbotMessagelistener wbot_url: ', wbot_url, ' | ticket.contact.number: ', ticket.contact.number);
await endPointQuery(`${wbot_url}/api/sendSeen`, { number: `${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us` });
}

View File

@ -0,0 +1,39 @@
const fsPromises = require("fs/promises");
const fs = require('fs')
import axios from 'axios';
import * as https from "https";
const endPointQuery = async (url: string, data: any) => {
let response: any = null
try {
response = await axios.post(url, data);
console.log(`TEST URL CLIENT POST ROUTE: ${url} | STATUS CODE: ${response.status}`);
} catch (err: any) {
if (err.response) {
// The client was given an error response (5xx, 4xx)
// console.log('err.response: ', err.response)
console.log('err.response: ', err.response)
// return { data: err.response.data, status: err.response.status }
} else if (err.request) {
// The client never received a response, and the request was never left
console.log('err.request: ', err.request)
} else {
// Anything else
console.error(`Erro ao consultar endpoint ${url}: ${err}`);
}
}
return response
}
export default endPointQuery;

View File

@ -21,18 +21,23 @@ let flatten = require('flat')
interface Request {
contactId: number;
status: string;
userId: number;
userId: number;
queueId?: number | undefined;
}
const CreateTicketService = async ({
contactId,
status,
userId
userId,
queueId = undefined
}: Request): Promise<Ticket> => {
console.log('========> queueId: ', queueId)
try {
const defaultWhatsapp = await GetDefaultWhatsApp(userId);
const defaultWhatsapp = await GetDefaultWhatsApp(userId);
await CheckContactOpenTickets(contactId);
@ -42,7 +47,8 @@ const CreateTicketService = async ({
contactId,
status,
isGroup,
userId
userId,
queueId
});
const ticket = await Ticket.findByPk(id, { include: ["contact"] });

View File

@ -17,6 +17,7 @@ import ListTicketServiceCache from "./ListTicketServiceCache"
import { searchTicketCache, loadTicketsCache } from '../../helpers/TicketCache'
import { getWbot } from "../../libs/wbot";
import User from "../../models/User";
@ -31,6 +32,7 @@ interface Request {
withUnreadMessages?: string;
queueIds: number[];
unlimited?: string;
searchParamContent?: string;
}
interface Response {
@ -49,9 +51,12 @@ const ListTicketsService = async ({
showAll,
userId,
withUnreadMessages,
unlimited = 'false'
unlimited = 'false',
searchParamContent = ""
}: Request): Promise<Response> => {
console.log('----------> searchParamContent: ', searchParamContent)
let whereCondition: Filterable["where"] = { [Op.or]: [{ userId }, { status: "pending" }], queueId: { [Op.or]: [queueIds, null] } };
console.log('PAGE NUMBER TICKET: ', pageNumber)
@ -146,20 +151,33 @@ const ListTicketsService = async ({
if (searchParam) {
const sanitizedSearchParam = searchParam.toLocaleLowerCase().trim();
const sanitizedSearchParamContent = searchParamContent.toLocaleLowerCase().trim();
if (searchParamContent.length > 0) {
includeCondition = [
...includeCondition,
{
model: Message,
as: "messages",
attributes: ["id", "body"],
where: {
body: where(fn("LOWER", col("body")), "LIKE", `%${sanitizedSearchParamContent}%`)
},
required: false,
duplicating: false
}
];
whereCondition = {
...whereCondition,
"$message.body$": where(fn("LOWER", col("body")), "LIKE", `%${sanitizedSearchParamContent}%`)
};
}
// includeCondition = [
// ...includeCondition,
// {
// model: Message,
// as: "messages",
// attributes: ["id", "body"],
// where: {
// body: where(fn("LOWER", col("body")), "LIKE", `%${sanitizedSearchParam}%`)
// },
// required: false,
// duplicating: false
// }
// ];
whereCondition = {
...whereCondition,
@ -170,11 +188,17 @@ const ListTicketsService = async ({
{ "$contact.number$": { [Op.like]: `%${sanitizedSearchParam}%` } },
// {
// "$message.body$": where(fn("LOWER", col("body")), "LIKE", `%${sanitizedSearchParam}%`)
// }
]
],
};
const userProfile: any = await User.findByPk(userId)
if (userProfile.dataValues.profile != 'admin' && userProfile.dataValues.profile != 'master') {
whereCondition = { ...whereCondition, userId }
}
}
if (date) {
@ -200,8 +224,6 @@ const ListTicketsService = async ({
const offset = limit * (+pageNumber - 1);
const { count, rows: tickets } = await Ticket.findAndCountAll({
where: whereCondition,
include: includeCondition,

View File

@ -8,20 +8,23 @@ import ShowTicketService from "./ShowTicketService";
import { createOrUpdateTicketCache } from '../../helpers/TicketCache'
import AppError from "../../errors/AppError";
import sendWhatsAppMessageSocket from "../../helpers/SendWhatsappMessageSocket";
var flatten = require('flat')
interface TicketData {
status?: string;
userId?: number;
queueId?: number;
statusChatEnd?: string
statusChatEnd?: string;
unreadMessages?: number;
}
interface Request {
ticketData: TicketData;
ticketId: string | number;
ticketId: string | number;
msg?: string
}
interface Response {
@ -32,83 +35,94 @@ interface Response {
const UpdateTicketService = async ({
ticketData,
ticketId
ticketId,
msg=''
}: Request): Promise<Response> => {
try {
const { status, userId, queueId, statusChatEnd } = ticketData;
const { status, userId, queueId, statusChatEnd, unreadMessages } = ticketData;
const ticket = await ShowTicketService(ticketId);
// await SetTicketMessagesAsRead(ticket);
const ticket = await ShowTicketService(ticketId);
// await SetTicketMessagesAsRead(ticket);
const oldStatus = ticket.status;
const oldUserId = ticket.user?.id;
const oldStatus = ticket.status;
const oldUserId = ticket.user?.id;
if (oldStatus === "closed") {
await CheckContactOpenTickets(ticket.contact.id);
}
await ticket.update({
status,
queueId,
userId,
statusChatEnd
});
if (oldStatus === "closed") {
await CheckContactOpenTickets(ticket.contact.id);
}
await ticket.reload();
// TEST DEL
try {
// const { name, number } = await ShowContactService(ticket.contactId)
let jsonString = JSON.stringify(ticket); //convert to string to remove the sequelize specific meta data
let ticket_obj = JSON.parse(jsonString); //to make plain json
delete ticket_obj['contact']['extraInfo']
delete ticket_obj['user']
ticket_obj = flatten(ticket_obj)
await createOrUpdateTicketCache(`ticket:${ticket.id}`, ticket_obj)
} catch (error) {
console.log('There was an error on UpdateTicketService.ts on createTicketCache: ', error)
}
//
let io = getIO();
if (ticket.status !== oldStatus || ticket.user?.id !== oldUserId) {
io.to(oldStatus).emit("ticket", {
action: "delete",
ticketId: ticket.id
await ticket.update({
status,
queueId,
userId,
unreadMessages,
statusChatEnd
});
}
await ticket.reload();
if (msg.length > 0) {
setTimeout(async () => {
sendWhatsAppMessageSocket(ticket, msg)
}, 2000)
}
// TEST DEL
try {
// const { name, number } = await ShowContactService(ticket.contactId)
let jsonString = JSON.stringify(ticket); //convert to string to remove the sequelize specific meta data
let ticket_obj = JSON.parse(jsonString); //to make plain json
delete ticket_obj['contact']['extraInfo']
delete ticket_obj['user']
ticket_obj = flatten(ticket_obj)
await createOrUpdateTicketCache(`ticket:${ticket.id}`, ticket_obj)
} catch (error) {
console.log('There was an error on UpdateTicketService.ts on createTicketCache: ', error)
}
//
let io = getIO();
if (ticket.status !== oldStatus || ticket.user?.id !== oldUserId) {
io.to(oldStatus).emit("ticket", {
action: "delete",
ticketId: ticket.id
});
}
io.to(ticket.status)
.to("notification")
.to(ticketId.toString())
.emit("ticket", {
io.to(ticket.status)
.to("notification")
.to(ticketId.toString())
.emit("ticket", {
action: "update",
ticket
});
io.emit("ticketStatus", {
action: "update",
ticket
ticketStatus: { ticketId: ticket.id, status: ticket.status }
});
io.emit("ticketStatus", {
action: "update",
ticketStatus: { ticketId: ticket.id, status: ticket.status }
});
return { ticket, oldStatus, oldUserId };
return { ticket, oldStatus, oldUserId };
} catch (error: any) {
console.error('===> Error on UpdateTicketService.ts file: \n', error)
throw new AppError(error.message);
}
};
export default UpdateTicketService;
export default UpdateTicketService;

View File

@ -33,21 +33,25 @@ interface Request {
body: string;
ticket: Ticket;
quotedMsg?: Message;
number?: string
}
const SendWhatsAppMessage = async ({
body,
ticket,
quotedMsg
quotedMsg,
number
}: Request): Promise<WbotMessage | any> => {
try {
let timestamp = Math.floor(Date.now() / 1000)
// let timestamp = Math.floor(Date.now() / 1000)
let timestamp = Date.now() + String(Math.floor(Math.random() * 1000))
var timetaken = `########################################${timestamp}| TicketId: ${ticket.id} => Time taken to send the message`;
console.time(timetaken)
let quotedMsgSerializedId: string | undefined;
if (quotedMsg) {
@ -64,9 +68,15 @@ const SendWhatsAppMessage = async ({
let listWhatsapp = null
// listWhatsapp = await searchWhatsappCache(`${ticket.whatsappId}`, 'CONNECTED')
// listWhatsapp = await searchWhatsappCache(`${ticket.whatsappId}`, 'CONNECTED')
console.log('ticket.whatsappIdticket.whatsappIdticket.whatsappIdticket: ', ticket.whatsappId)
if (!ticket.whatsappId) {
const defaultWhatsapp: any = await GetDefaultWhatsApp(ticket.userId);
await ticket.update({ whatsappId: +defaultWhatsapp.id });
}
if (!listWhatsapp) {
listWhatsapp = await ListWhatsAppsNumber(ticket.whatsappId, 'CONNECTED')
@ -74,12 +84,8 @@ const SendWhatsAppMessage = async ({
if (listWhatsapp.whatsapp && listWhatsapp.whatsapp.status != 'CONNECTED' && listWhatsapp.whatsapps.length > 0) {
// console.log('kkkkkkkkkkkkkkkkkkkkkkkkkkkk: ', listWhatsapp.whatsapps[0].id)
await ticket.update({ whatsappId: + listWhatsapp.whatsapps[0].id });
let _ticket = await Ticket.findByPk(listWhatsapp.whatsapps[0].id)
}
@ -120,9 +126,9 @@ const SendWhatsAppMessage = async ({
try {
sendWhatsAppMessageSocket(ticket, body, quotedMsgSerializedId);
sendWhatsAppMessageSocket(ticket, body, quotedMsgSerializedId, number);
await ticket.update({ lastMessage: body });
await ticket.update({ lastMessage: body });
await updateTicketCacheByTicketId(ticket.id, { lastMessage: body, updatedAt: new Date(ticket.updatedAt).toISOString() })

View File

@ -60,6 +60,7 @@ import { _restore } from "../../helpers/RestoreControll";
import sendWhatsAppMessageSocket from "../../helpers/SendWhatsappMessageSocket";
import { getWhatsappIds, setWhatsappId } from "../../helpers/WhatsappIdMultiSessionControl";
import AppError from "../../errors/AppError";
import { setMessageAsRead } from "../../helpers/SetMessageAsRead";
@ -888,15 +889,13 @@ const handleMessage = async (
}
//
if (msg && !msg.fromMe && ticket.status == 'pending') {
await setMessageAsRead(ticket)
// test del
// if (msg.body.trim() == 'broken') {
// throw new Error('Throw makes it go boom!')
// }
//
}
} catch (err) {
Sentry.captureException(err);
@ -926,9 +925,7 @@ const handleMsgAck = async (msg_id: any, ack: any) => {
return;
}
await messageToUpdate.update({ ack });
console.log('ACK messageToUpdate: ', JSON.parse(JSON.stringify(messageToUpdate)))
io.to(messageToUpdate.ticketId.toString()).emit("appMessage", {
action: "update",
message: messageToUpdate

View File

@ -0,0 +1,156 @@
import React, { useState, useEffect, useContext, useRef, useCallback } from "react";
import { useHistory } from "react-router-dom";
import { toast } from "react-toastify";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import Select from "@material-ui/core/Select";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import LinearProgress from "@material-ui/core/LinearProgress";
import { makeStyles } from "@material-ui/core";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import { i18n } from "../../translate/i18n";
import ButtonWithSpinner from "../ButtonWithSpinner";
import { AuthContext } from "../../context/Auth/AuthContext";
import toastError from "../../errors/toastError";
import api from "../../services/api";
const useStyles = makeStyles((theme) => ({
maxWidth: {
width: "100%",
},
paper: {
minWidth: "300px"
},
linearProgress: {
marginTop: "5px"
}
}));
const ContactCreateTicketModal = ({ modalOpen, onClose, contactId }) => {
const { user } = useContext(AuthContext);
let isMounted = useRef(true)
const history = useHistory();
const [queues, setQueues] = useState([]);
const [loading, setLoading] = useState(false);
const [selectedQueue, setSelectedQueue] = useState('');
const [itemHover, setItemHover] = useState(-1)
const classes = useStyles();
useEffect(() => {
const userQueues = user.queues.map(({ id, name, color }) => { return { id, name, color } })
if (userQueues.length === 1) setSelectedQueue(userQueues[0].id)
setQueues(userQueues)
}, [user]);
const handleClose = () => {
onClose();
};
const handleSaveTicket = useCallback(async (contactId, userId, queueId) => {
if (!contactId || !userId) {
console.log("Missing contactId or userId")
return
};
if (!queueId) {
toast.warning("Nenhuma Fila Selecionada")
return
}
if (isMounted.current) setLoading(true);
const delayDebounceFn = setTimeout(() => {
const ticketCreate = async () => {
try {
const { data: ticket } = await api.post("/tickets", {
contactId: contactId,
userId: userId,
queueId: queueId,
status: "open",
});
history.push(`/tickets/${ticket.id}`);
} catch (err) {
toastError(err);
}
if (isMounted.current) setLoading(false);
};
ticketCreate();
}, 300);
return () => clearTimeout(delayDebounceFn);
}, [history])
useEffect(() => {
if (modalOpen && queues.length <= 1) {
handleSaveTicket(contactId, user.id, selectedQueue)
}
return () => {
isMounted.current = false;
};
}, [modalOpen, contactId, user.id, selectedQueue, handleSaveTicket, queues.length]);
if (modalOpen && queues.length <= 1) {
return <LinearProgress />
}
return (
<Dialog open={modalOpen} onClose={handleClose} maxWidth="xs" scroll="paper" classes={{ paper: classes.paper }}>
<DialogTitle id="form-dialog-title">
{i18n.t("newTicketModal.title")}
</DialogTitle>
<DialogContent dividers>
<FormControl variant="outlined" className={classes.maxWidth}>
<InputLabel>{i18n.t("Selecionar Fila")}</InputLabel>
<Select
value={selectedQueue}
onChange={(e) => setSelectedQueue(e.target.value)}
label={i18n.t("Filas")}
>
<MenuItem value={''}>&nbsp;</MenuItem>
{queues.map(({ id, color, name }) => (
<MenuItem
key={id}
value={id}
onMouseEnter={() => setItemHover(id)}
onMouseLeave={() => setItemHover(-1)}
style={{
background: id !== itemHover ? "white" : color,
}}
>{name[0].toUpperCase() + name.slice(1).toLowerCase()}</MenuItem>
))}
</Select>
</FormControl>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={loading}
variant="outlined"
>
{i18n.t("newTicketModal.buttons.cancel")}
</Button>
<ButtonWithSpinner
onClick={() => handleSaveTicket(contactId, user.id, selectedQueue)}
variant="contained"
color="primary"
loading={loading}
>
{i18n.t("newTicketModal.buttons.ok")}
</ButtonWithSpinner>
</DialogActions>
</Dialog>
);
};
export default ContactCreateTicketModal;

View File

@ -73,6 +73,7 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
name: "",
number: "",
email: "",
useDialogflow: true,
};
const [contact, setContact] = useState(initialState);

View File

@ -1,8 +1,9 @@
import React, { 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 { green } from "@material-ui/core/colors";
import {
@ -318,6 +319,9 @@ const MessagesList = ({ ticketId, isGroup }) => {
const [anchorEl, setAnchorEl] = useState(null);
const messageOptionsMenuOpen = Boolean(anchorEl);
const currentTicketId = useRef(ticketId);
const [sendSeen, setSendSeen] = useState(false)
const { user } = useContext(AuthContext);
useEffect(() => {
dispatch({ type: "RESET" });
@ -327,13 +331,77 @@ const MessagesList = ({ ticketId, isGroup }) => {
}, [ticketId]);
useEffect(() => {
let url_split
let url_ticketId
try {
url_split = window.location.href.split('tickets')
url_ticketId = url_split[url_split.length - 1].match(/\d+/)[0]
} catch (error) {
console.log('error on try do the send seen: ', error)
}
if (!url_ticketId) return
if (!sendSeen) return
const delayDebounceFn = setTimeout(() => {
const sendSeenMessage = async () => {
try {
const { data } = await api.get("/messages/" + ticketId, {
params: { pageNumber },
});
setSendSeen(false)
if (!data) return
if (data.ticket.status === "open" && /*data.ticket.unreadMessages > 0 &&*/
data.ticket.userId === user.id) {
let fromMe = false
if (data.messages.length > 0) {
fromMe = data.messages[data.messages.length - 1].fromMe
}
// Atualiza Unread messages para 0
// Atualizei função no back-end para receber o novo parametro unreadMessages
const ticketUpdate = { ...data.ticket, unreadMessages: 0, fromMe }
await api.put("/tickets/" + ticketId, ticketUpdate)
}
} catch (err) {
setLoading(false);
toastError(err);
}
};
sendSeenMessage();
}, 500);
return () => {
clearTimeout(delayDebounceFn);
};
}, [sendSeen, pageNumber, ticketId, user.id]);
useEffect(() => {
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 });
@ -365,17 +433,12 @@ const MessagesList = ({ ticketId, isGroup }) => {
if (data.action === "create") {
console.log('ADD_MESSAGE: ', data.message)
dispatch({ type: "ADD_MESSAGE", payload: data.message });
scrollToBottom();
}
if (data.action === "update") {
console.log('joinChatBox update: ',data.action)
dispatch({ type: "UPDATE_MESSAGE", payload: data.message });
}
});
@ -391,7 +454,11 @@ const MessagesList = ({ ticketId, isGroup }) => {
const scrollToBottom = () => {
if (lastMessageRef.current) {
setSendSeen(true)
lastMessageRef.current.scrollIntoView({});
}
};
@ -513,7 +580,7 @@ const MessagesList = ({ ticketId, isGroup }) => {
if (index === messagesList.length - 1) {
let messageDay = parseISO(messagesList[index].createdAt);
let previousMessageDay = parseISO(messagesList[index - 1].createdAt);
let previousMessageDay = parseISO(messagesList[index - 1].createdAt);
return (
<>
@ -682,4 +749,4 @@ const MessagesList = ({ ticketId, isGroup }) => {
);
};
export default MessagesList;
export default MessagesList;

View File

@ -116,17 +116,13 @@ const NotificationsPopOver = () => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("reload_page", (data) => {
console.log('UPDATING THE PAGE: ', data.userId, ' | user.id: ', user.id)
socket.on("reload_page", (data) => {
if (user.id === data.userId) {
window.location.reload(true);
}
}
})
@ -167,8 +163,7 @@ const NotificationsPopOver = () => {
clearInterval(_fifo);
}
_fifo = setInterval(() => {
console.log('user.id: ', user.id)
_fifo = setInterval(() => {
socket.emit("online", user.id)
}, 3000);

View File

@ -249,4 +249,4 @@ const QueueModal = ({ open, onClose, queueId }) => {
);
};
export default QueueModal;
export default QueueModal;

View File

@ -11,6 +11,8 @@ import {
// import { i18n } from "../../../translate/i18n";
import ptBrLocale from "date-fns/locale/pt-BR";

View File

@ -83,14 +83,14 @@ const Ticket = () => {
const [contact, setContact] = useState({});
const [ticket, setTicket] = useState({});
const [statusChatEnd, setStatusChatEnd] = useState({})
const [statusChatEnd, setStatusChatEnd] = useState({})
useEffect(() => {
setLoading(true);
const delayDebounceFn = setTimeout(() => {
const fetchTicket = async () => {
try {
// maria julia
const { data } = await api.get("/tickets/" + ticketId);
@ -101,7 +101,7 @@ const Ticket = () => {
setContact(data.contact.contact);
setTicket(data.contact);
setStatusChatEnd(data.statusChatEnd)
setStatusChatEnd(data.statusChatEnd)
setLoading(false);
} catch (err) {
@ -171,8 +171,8 @@ const Ticket = () => {
onClick={handleDrawerOpen}
/>
</div>
<div className={classes.ticketActionButtons}>
<TicketActionButtons ticket={ticket} statusChatEnd={statusChatEnd}/>
<div className={classes.ticketActionButtons}>
<TicketActionButtons ticket={ticket} statusChatEnd={statusChatEnd} />
</div>
</TicketHeader>
<ReplyMessageProvider>

View File

@ -12,7 +12,7 @@ import ButtonWithSpinner from "../ButtonWithSpinner";
import toastError from "../../errors/toastError";
import { AuthContext } from "../../context/Auth/AuthContext";
import Modal from "../ChatEnd/ModalChatEnd";
import Modal from "../ChatEnd/ModalChatEnd";
import { render } from '@testing-library/react';
import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption";
@ -29,17 +29,30 @@ const useStyles = makeStyles(theme => ({
},
}));
const TicketActionButtons = ({ ticket, statusChatEnd }) => {
const TicketActionButtons = ({ ticket, statusChatEnd }) => {
const classes = useStyles();
const history = useHistory();
const history = useHistory();
const [anchorEl, setAnchorEl] = useState(null);
const [loading, setLoading] = useState(false);
// const [useDialogflow, setUseDialogflow] = useState(ticket.contact.useDialogflow);
// const [/*useDialogflow*/, setUseDialogflow] = useState(() => {
// if (Object.keys(ticket).length !== 0) {
// return ticket.contact.useDialogflow;
// } else {
// // Set a default value if `ticket.contact.useDialogflow` is null
// return true
// }
// });
const ticketOptionsMenuOpen = Boolean(anchorEl);
const { user } = useContext(AuthContext);
const { tabOption, setTabOption } = useContext(TabTicketContext);
const handleOpenTicketOptionsMenu = e => {
const handleOpenTicketOptionsMenu = e => {
setAnchorEl(e.currentTarget);
};
@ -47,83 +60,91 @@ const TicketActionButtons = ({ ticket, statusChatEnd }) => {
setAnchorEl(null);
};
const chatEndVal = (data) => {
if(data){
data = {...data, 'ticketId': ticket.id}
const chatEndVal = (data) => {
if (data) {
data = { ...data, 'ticketId': ticket.id }
handleUpdateTicketStatus(null, "closed", user?.id, data)
}
}
}
const handleModal = (/*status, userId*/) => {
const handleModal = (/*status, userId*/) => {
render(<Modal
modal_header={'Finalização de Atendimento'}
func={chatEndVal}
statusChatEnd={statusChatEnd}
ticketId={ticket.id}
/>)
render(<Modal
modal_header={'Finalização de Atendimento'}
func={chatEndVal}
statusChatEnd={statusChatEnd}
ticketId={ticket.id}
/>)
};
const handleUpdateTicketStatus = async (e, status, userId, schedulingData={}) => {
setLoading(true);
try {
if(status==='closed'){
if (tabOption === 'search') {
setTabOption('open')
}
const handleUpdateTicketStatus = async (e, status, userId, schedulingData = {}) => {
await api.put(`/tickets/${ticket.id}`, {
status: status,
userId: userId || null,
schedulingNotifyData: JSON.stringify(schedulingData)
});
setLoading(true);
try {
if (status === 'closed') {
if (tabOption === 'search') {
setTabOption('open')
}
else{
if (tabOption === 'search') {
setTabOption('open')
}
await api.put(`/tickets/${ticket.id}`, {
status: status,
userId: userId || null
});
await api.put(`/tickets/${ticket.id}`, {
status: status,
userId: userId || null,
schedulingNotifyData: JSON.stringify(schedulingData)
});
}
}
else {
setLoading(false);
if (status === "open") {
history.push(`/tickets/${ticket.id}`);
} else {
history.push("/tickets");
if (tabOption === 'search') {
setTabOption('open')
}
} catch (err) {
setLoading(false);
toastError(err);
}
await api.put(`/tickets/${ticket.id}`, {
status: status,
userId: userId || null
});
}
setLoading(false);
if (status === "open") {
history.push(`/tickets/${ticket.id}`);
} else {
history.push("/tickets");
}
} catch (err) {
setLoading(false);
toastError(err);
}
};
// const handleContactToggleUseDialogflow = async () => {
// setLoading(true);
// try {
// const contact = await api.put(`/contacts/toggleUseDialogflow/${ticket.contact.id}`);
// setUseDialogflow(contact.data.useDialogflow);
// setLoading(false);
// } catch (err) {
// setLoading(false);
// toastError(err);
// }
// };
return (
<div className={classes.actionButtons}>
{ticket.status === "closed" && (
@ -138,7 +159,7 @@ const TicketActionButtons = ({ ticket, statusChatEnd }) => {
)}
{ticket.status === "open" && (
<>
<ButtonWithSpinner style={{ marginRight: "70px" }}
<ButtonWithSpinner style={{ marginRight: "70px" }}
loading={loading}
startIcon={<Replay />}
size="small"
@ -146,16 +167,16 @@ const TicketActionButtons = ({ ticket, statusChatEnd }) => {
>
{i18n.t("messagesList.header.buttons.return")}
</ButtonWithSpinner>
<ButtonWithSpinner
<ButtonWithSpinner
loading={loading}
size="small"
variant="contained"
color="primary"
onClick={e => {
handleModal()
onClick={e => {
handleModal()
// handleUpdateTicketStatus(e, "closed", user?.id)
}}

View File

@ -1,4 +1,6 @@
import React, { useState, useEffect, useReducer, useContext } from "react";
import openSocket from "socket.io-client";
import { makeStyles } from "@material-ui/core/styles";
@ -175,7 +177,7 @@ const reducer = (state, action) => {
};
const TicketsList = (props) => {
const { status, searchParam, showAll, selectedQueueIds, updateCount, style, tab } = props;
const { status, searchParam, searchParamContent, showAll, selectedQueueIds, updateCount, style, tab } = props;
const classes = useStyles();
const [pageNumber, setPageNumber] = useState(1);
const [ticketsList, dispatch] = useReducer(reducer, []);
@ -184,16 +186,16 @@ const TicketsList = (props) => {
const { searchTicket } = useContext(SearchTicketContext)
useEffect(() => {
dispatch({ type: "RESET" });
setPageNumber(1);
}, [status, searchParam, showAll, selectedQueueIds, searchTicket]);
}, [status, searchParam, searchParamContent, showAll, selectedQueueIds, searchTicket]);
const { tickets, hasMore, loading } = useTickets({
pageNumber,
searchParam,
searchParamContent,
status,
showAll,
queueIds: JSON.stringify(selectedQueueIds),
@ -201,6 +203,7 @@ const TicketsList = (props) => {
});
useEffect(() => {
if (!status && !searchParam) return;
// if (searchParam) {

View File

@ -1,14 +1,21 @@
import React, { useContext, useEffect, useRef, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import { IconButton } from "@mui/material";
import Paper from "@material-ui/core/Paper";
import SearchIcon from "@material-ui/icons/Search";
import InputBase from "@material-ui/core/InputBase";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import Badge from "@material-ui/core/Badge";
import Tooltip from "@material-ui/core/Tooltip";
import SearchIcon from "@material-ui/icons/Search";
import MoveToInboxIcon from "@material-ui/icons/MoveToInbox";
import CheckBoxIcon from "@material-ui/icons/CheckBox";
import MenuIcon from "@material-ui/icons/Menu";
import FindInPageIcon from '@material-ui/icons/FindInPage';
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
@ -63,6 +70,25 @@ const useStyles = makeStyles((theme) => ({
},
serachInputWrapper: {
flex: 1,
display: "flex",
flexDirection: "column",
gap: "10px",
borderRadius: 40,
padding: 4,
marginRight: theme.spacing(1),
},
searchInputHeader: {
flex: 1,
background: "#fff",
display: "flex",
borderRadius: 40,
padding: 4,
marginRight: theme.spacing(1),
},
searchContentInput: {
flex: 1,
background: "#fff",
display: "flex",
@ -78,6 +104,11 @@ const useStyles = makeStyles((theme) => ({
alignSelf: "center",
},
menuSearch: {
color: "grey",
alignSelf: "center",
},
searchInput: {
flex: 1,
border: "none",
@ -95,20 +126,21 @@ const useStyles = makeStyles((theme) => ({
},
}));
const DEFAULT_SEARCH_PARAM = { searchParam: "", searchParamContent: "" }
const TicketsManager = () => {
const { tabOption, setTabOption } = useContext(TabTicketContext);
const {setSearchTicket} = useContext(SearchTicketContext)
const { setSearchTicket } = useContext(SearchTicketContext)
const classes = useStyles();
const [searchParam, setSearchParam] = useState("");
const [searchParam, setSearchParam] = useState(DEFAULT_SEARCH_PARAM);
const [tab, setTab] = useState("open");
const [tabOpen, setTabOpen] = useState("open");
const [newTicketModalOpen, setNewTicketModalOpen] = useState(false);
const [showAllTickets, setShowAllTickets] = useState(false);
const searchInputRef = useRef();
const { user } = useContext(AuthContext);
const [openCount, setOpenCount] = useState(0);
@ -117,7 +149,16 @@ const TicketsManager = () => {
const userQueueIds = user.queues.map((q) => q.id);
const [selectedQueueIds, setSelectedQueueIds] = useState(userQueueIds || []);
const [showContentSearch, setShowContentSearch] = useState(false)
const searchInputRef = useRef();
const searchContentInputRef = useRef();
const [inputSearch, setInputSearch] = useState('');
const [inputContentSearch, setInputContentSearch] = useState("")
const [openTooltipSearch, setOpenTooltipSearch] = useState(false)
let searchTimeout;
let searchContentTimeout;
useEffect(() => {
if (user.profile.toUpperCase() === "ADMIN") {
@ -135,22 +176,45 @@ const TicketsManager = () => {
}, [tab, setTabOption]);
useEffect(() => {
// clearTimeout(searchContentTimeout);
// setSearchParam(prev => ({ ...prev, searchParamContent: "" }))
if (!inputContentSearch) return
if (!searchContentTimeout) return
// searchContentTimeout = setTimeout(() => {
// setSearchParam(prev => ({ ...prev, searchParamContent: inputContentSearch }));
// }, 500);
clearTimeout(searchContentTimeout);
setSearchParam(prev => ({ ...prev, searchParamContent: "" }))
}, [inputContentSearch, searchContentTimeout]);
useEffect(() => {
if (tabOption === 'open') {
setTabOption('')
setSearchParam('');
setSearchParam(DEFAULT_SEARCH_PARAM);
setInputSearch('');
setInputContentSearch('')
setTab("open");
return;
}
}, [tabOption, setTabOption])
let searchTimeout;
const removeExtraSpace = (str) => {
str = str.replace(/^\s+/g, '')
@ -163,24 +227,58 @@ const TicketsManager = () => {
setInputSearch(removeExtraSpace(searchedTerm))
setSearchTicket(searchParam)
setSearchTicket(searchParam.searchParam)
clearTimeout(searchTimeout);
if (searchedTerm === "") {
setSearchParam(searchedTerm);
setSearchParam(prev => ({ ...prev, searchParam: searchedTerm }))
setInputSearch(searchedTerm)
setShowContentSearch(false)
setTab("open");
return;
}
if (searchedTerm.length < 4) {
setSearchParam(prev => ({ ...prev, searchParamContent: "" }))
setInputContentSearch('')
}
searchTimeout = setTimeout(() => {
setSearchParam(searchedTerm);
setSearchParam(prev => ({ ...prev, searchParam: searchedTerm }));
}, 500);
};
const handleContentSearch = e => {
let searchedContentText = removeExtraSpace(e.target.value.toLowerCase())
setInputContentSearch(searchedContentText)
searchContentTimeout = setTimeout(() => {
setSearchParam(prev => ({ ...prev, searchParamContent: searchedContentText }));
}, 500);
}
const handleOpenTooltipSearch = () => {
if (searchParam.searchParam.length < 4) {
setOpenTooltipSearch(true)
}
}
const handleCloseTooltipSearch = () => {
setOpenTooltipSearch(false)
if (searchParam.searchParam.length < 4) {
searchInputRef.current.focus()
}
}
const handleChangeTab = (e, newValue) => {
setTab(newValue);
};
@ -233,15 +331,50 @@ const TicketsManager = () => {
<Paper square elevation={0} className={classes.ticketOptionsBox}>
{tab === "search" ? (
<div className={classes.serachInputWrapper}>
<SearchIcon className={classes.searchIcon} />
<InputBase
className={classes.searchInput}
inputRef={searchInputRef}
placeholder={i18n.t("tickets.search.placeholder")}
type="search"
value={inputSearch}
onChange={handleSearch}
/>
<div className={classes.searchInputHeader}>
<SearchIcon className={classes.searchIcon} />
<InputBase
className={classes.searchInput}
inputRef={searchInputRef}
placeholder={i18n.t("tickets.search.placeholder")}
type="search"
value={inputSearch}
onChange={handleSearch}
/>
{/* <IconButton onClick={() => setShowContentSearch(prev => !prev)}>
<MenuIcon className={classes.menuSearch} />
</IconButton> */}
<Tooltip
open={openTooltipSearch}
onOpen={() => handleOpenTooltipSearch()}
onClose={() => handleCloseTooltipSearch()}
title="Digite pelo menos 4 caracteres"
arrow>
<span>
<IconButton
disabled={searchParam.searchParam.length < 4}
onClick={() => setShowContentSearch(prev => !prev)}
>
<MenuIcon className={classes.menuSearch} />
</IconButton>
</span>
</Tooltip>
</div>
{
// showContentSearch ?
(showContentSearch && searchParam.searchParam.length >= 4) ?
(<div className={classes.searchContentInput}>
<FindInPageIcon className={classes.searchIcon} />
<InputBase
className={classes.searchInput}
inputRef={searchContentInputRef}
placeholder={i18n.t("Busca por conteúdo")}
type="search"
value={inputContentSearch}
onChange={(e) => handleContentSearch(e)}
/>
</div>) : null
}
</div>
) : (
<>
@ -340,17 +473,18 @@ const TicketsManager = () => {
</TabPanel>
<TabPanel value={tab} name="search" className={classes.ticketsWrapper}>
<TicketsList
searchParam={searchParam}
tab={tab}
showAll={true}
selectedQueueIds={selectedQueueIds}
/>
<TicketsList
searchParam={searchParam.searchParam}
searchParamContent={searchParam.searchParamContent}
tab={tab}
showAll={true}
selectedQueueIds={selectedQueueIds}
/>
</TabPanel>
</Paper>
);
};
export default TicketsManager;
export default TicketsManager;

View File

@ -1,8 +1,7 @@
import React, { useState, useEffect } from "react";
import React, { useState, useContext, useMemo } from "react";
import { useHistory } from "react-router-dom";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import Select from "@material-ui/core/Select";
import FormControl from "@material-ui/core/FormControl";
@ -13,102 +12,72 @@ import { makeStyles } from "@material-ui/core";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import Autocomplete, {
createFilterOptions,
} from "@material-ui/lab/Autocomplete";
import CircularProgress from "@material-ui/core/CircularProgress";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import ButtonWithSpinner from "../ButtonWithSpinner";
import toastError from "../../errors/toastError";
import useQueues from "../../hooks/useQueues";
import { WhatsAppsContext } from "../../context/WhatsApp/WhatsAppsContext";
const useStyles = makeStyles((theme) => ({
maxWidth: {
width: "100%",
},
maxWidth: {
width: "100%",
},
}));
const filterOptions = createFilterOptions({
trim: true,
});
// Receive array of queues arrays
// Return a new array with unique queues from all arrays has passed by the parameter
const queueArraysToOneArray = (array) => {
if (!array) return []
const map = {}
const uniqueQueuesAvailable = []
array.forEach((queues) => {
queues.forEach(({ id, name, color }) => {
if (!map[id]) {
map[id] = true
uniqueQueuesAvailable.push({ id, name, color })
}
})
})
return uniqueQueuesAvailable
}
const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => {
const history = useHistory();
const [options, setOptions] = useState([]);
const [queues, setQueues] = useState([]);
const [allQueues, setAllQueues] = useState([]);
const { whatsApps } = useContext(WhatsAppsContext);
const [loading, setLoading] = useState(false);
const [searchParam, setSearchParam] = useState("");
const [selectedUser, setSelectedUser] = useState(null);
const [selectedQueue, setSelectedQueue] = useState('');
const classes = useStyles();
const { findAll: findAllQueues } = useQueues();
useEffect(() => {
const loadQueues = async () => {
const list = await findAllQueues();
setAllQueues(list);
setQueues(list);
}
loadQueues();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!modalOpen || searchParam.length < 3) {
setLoading(false);
return;
}
setLoading(true);
const delayDebounceFn = setTimeout(() => {
const fetchUsers = async () => {
try {
const { data } = await api.get("/users/", {
params: { searchParam },
});
setOptions(data.users);
setLoading(false);
} catch (err) {
setLoading(false);
toastError(err);
}
};
fetchUsers();
}, 500);
return () => clearTimeout(delayDebounceFn);
}, [searchParam, modalOpen]);
const queues = useMemo(() => {
if (!whatsApps) return []
const whatsAppsQueues = whatsApps.map(({ queues }) => queues)
//const whatsAppsQueues = whatsApps.filter(({ status }) => status === "CONNECTED" ).map(({ queues }) => queues)
const uniqueQueuesAvailable = queueArraysToOneArray(whatsAppsQueues)
return uniqueQueuesAvailable
}, [whatsApps])
const [itemHover, setItemHover] = useState(-1)
const handleClose = () => {
onClose();
setSearchParam("");
setSelectedUser(null);
};
const handleSaveTicket = async e => {
e.preventDefault();
if (!ticketid) return;
if (!selectedQueue) return;
setLoading(true);
try {
let data = {};
if (selectedUser) {
data.userId = selectedUser.id
}
if (selectedQueue && selectedQueue !== null) {
data.queueId = selectedQueue
if (!selectedUser) {
data.status = 'pending';
data.userId = null;
}
}
// test del PARA APARECER NA FILA DE OUTRO ATENDENTE E O MESMO CLICAR EM ACEITAR AO INVES DE ENVIAR PARA ATENDENDO
data.status = 'pending'
data.transfer = true
await api.put(`/tickets/${ticketid}`, data);
@ -127,56 +96,26 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => {
{i18n.t("transferTicketModal.title")}
</DialogTitle>
<DialogContent dividers>
<Autocomplete
style={{ width: 300, marginBottom: 20 }}
getOptionLabel={option => `${option.name}`}
onChange={(e, newValue) => {
setSelectedUser(newValue);
if (newValue != null && Array.isArray(newValue.queues)) {
setQueues(newValue.queues);
} else {
setQueues(allQueues);
setSelectedQueue('');
}
}}
options={options}
filterOptions={filterOptions}
freeSolo
autoHighlight
noOptionsText={i18n.t("transferTicketModal.noOptions")}
loading={loading}
renderInput={params => (
<TextField
{...params}
label={i18n.t("transferTicketModal.fieldLabel")}
variant="outlined"
required
autoFocus
onChange={e => setSearchParam(e.target.value)}
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
<FormControl variant="outlined" className={classes.maxWidth}>
<InputLabel>{i18n.t("transferTicketModal.fieldQueueLabel")}</InputLabel>
<Select
value={selectedQueue}
onChange={(e) => setSelectedQueue(e.target.value)}
label={i18n.t("transferTicketModal.fieldQueuePlaceholder")}
required
>
<MenuItem value={''}>&nbsp;</MenuItem>
<MenuItem style={{ background: "white", }} value={''}>&nbsp;</MenuItem>
{queues.map((queue) => (
<MenuItem key={queue.id} value={queue.id}>{queue.name}</MenuItem>
<MenuItem
key={queue.id}
value={queue.id}
onMouseEnter={() => setItemHover(queue.id)}
onMouseLeave={() => setItemHover(-1)}
style={{
background: queue.id !== itemHover ? "white" : queue.color,
}}
>{queue.name}
</MenuItem>
))}
</Select>
</FormControl>
@ -200,8 +139,8 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => {
</ButtonWithSpinner>
</DialogActions>
</form>
</Dialog>
</Dialog >
);
};
export default TransferTicketModal;
export default TransferTicketModal;

View File

@ -5,6 +5,7 @@ import api from "../../services/api";
const useTickets = ({
searchParam,
searchParamContent,
pageNumber,
status,
date,
@ -35,6 +36,7 @@ const useTickets = ({
const { data } = await api.get("/tickets", {
params: {
searchParam,
searchParamContent,
pageNumber,
status,
date,
@ -63,6 +65,7 @@ const useTickets = ({
return () => clearTimeout(delayDebounceFn);
}, [
searchParam,
searchParamContent,
pageNumber,
status,
date,

View File

@ -37,8 +37,9 @@ import { Can } from "../../components/Can";
import apiBroker from "../../services/apiBroker";
import fileDownload from 'js-file-download'
import ContactCreateTicketModal from "../../components/ContactCreateTicketModal";
const reducer = (state, action) => {
@ -111,6 +112,7 @@ const Contacts = () => {
const [contacts, dispatch] = useReducer(reducer, []);
const [selectedContactId, setSelectedContactId] = useState(null);
const [contactModalOpen, setContactModalOpen] = useState(false);
const [isCreateTicketModalOpen, setIsCreateTicketModalOpen] = useState(false)
const [deletingContact, setDeletingContact] = useState(null);
const [confirmOpen, setConfirmOpen] = useState(false);
const [hasMore, setHasMore] = useState(false);
@ -118,17 +120,17 @@ const Contacts = () => {
const [onQueueStatus, setOnQueueProcessStatus] = useState(undefined)
const [zipfile, setZipFile] = useState()
const [zipfile, setZipFile] = useState()
async function handleChange(event) {
try {
if (event.target.files[0].size > 1024 * 1024 * 4){
if (event.target.files[0].size > 1024 * 1024 * 4) {
alert('Arquivo não pode ser maior que 4 MB!')
return
}
}
const formData = new FormData();
formData.append("adminId", user.id);
@ -302,21 +304,30 @@ const Contacts = () => {
setContactModalOpen(false);
};
const handleSaveTicket = async (contactId) => {
if (!contactId) return;
setLoading(true);
try {
const { data: ticket } = await api.post("/tickets", {
contactId: contactId,
userId: user?.id,
status: "open",
});
history.push(`/tickets/${ticket.id}`);
} catch (err) {
toastError(err);
}
setLoading(false);
};
const handleOpenCreateTicketModal = (contactId) => {
setSelectedContactId(contactId)
setIsCreateTicketModalOpen(true)
}
const handleCloseCreateTicketModal = () => {
setIsCreateTicketModalOpen(false)
}
// const handleSaveTicket = async (contactId) => {
// if (!contactId) return;
// setLoading(true);
// try {
// const { data: ticket } = await api.post("/tickets", {
// contactId: contactId,
// userId: user?.id,
// status: "open",
// });
// history.push(`/tickets/${ticket.id}`);
// } catch (err) {
// toastError(err);
// }
// setLoading(false);
// };
const hadleEditContact = (contactId) => {
setSelectedContactId(contactId);
@ -415,21 +426,21 @@ const Contacts = () => {
switch (param) {
case 'empty':
return (
<>
<>
<input
type="file"
accept=".csv"
style={{ display: 'none' }}
onChange={handleChange}
id="contained-button-file"
/>
<input
type="file"
accept=".csv"
style={{ display: 'none' }}
onChange={handleChange}
id="contained-button-file"
/>
<label htmlFor="contained-button-file">
<Button variant="contained" color="primary" component="span">
CSV UPLOAD
</Button>
</label>
<label htmlFor="contained-button-file">
<Button variant="contained" color="primary" component="span">
CSV UPLOAD
</Button>
</label>
{/* <Button
disabled={query && query.length > 0 ? false : true}
@ -495,6 +506,11 @@ const Contacts = () => {
aria-labelledby="form-dialog-title"
contactId={selectedContactId}
></ContactModal>
<ContactCreateTicketModal
modalOpen={isCreateTicketModalOpen}
onClose={handleCloseCreateTicketModal}
contactId={selectedContactId}
/>
<ConfirmationModal
title={
deletingContact
@ -609,7 +625,7 @@ const Contacts = () => {
<TableCell align="center">
<IconButton
size="small"
onClick={() => handleSaveTicket(contact.id)}
onClick={() => handleOpenCreateTicketModal(contact.id)}
>
<WhatsAppIcon />
</IconButton>
@ -646,4 +662,4 @@ const Contacts = () => {
);
};
export default Contacts;
export default Contacts;