Compare commits
No commits in common. "8447628fbf2de319679a0e06300009f3fa8411ab" and "52e63706c1b25601be4505cb33ee0c8ba99f6907" have entirely different histories.
8447628fbf
...
52e63706c1
|
@ -19,7 +19,6 @@ import CountTicketsByUserQueue from "../services/UserServices/CountTicketsByUser
|
|||
import ShowQueuesByUser from "../services/UserServices/ShowQueuesByUser";
|
||||
import { getIO } from "../libs/socket";
|
||||
import { Json } from "sequelize/types/lib/utils";
|
||||
import ReportByNumberQueueService from "../services/ReportServices/ReportByNumberQueueService";
|
||||
|
||||
type IndexQuery = {
|
||||
userId: string;
|
||||
|
@ -54,10 +53,23 @@ export const reportUserByDateStartDateEnd = async (
|
|||
endDate,
|
||||
pageNumber,
|
||||
userQueues,
|
||||
createdOrUpdated,
|
||||
createdOrUpdated,
|
||||
queueId
|
||||
} = req.query as IndexQuery;
|
||||
|
||||
console.log(
|
||||
"userId, startDate, endDate, pageNumber, userQueues, createdOrUpdated, queueId: ",
|
||||
userId,
|
||||
startDate,
|
||||
endDate,
|
||||
pageNumber,
|
||||
userQueues,
|
||||
createdOrUpdated,
|
||||
queueId
|
||||
);
|
||||
|
||||
// return res.status(200).json({ tickets:[], count:0, hasMore:false, queues:[] });
|
||||
|
||||
const { tickets, count, hasMore } = await ShowTicketReport({
|
||||
userId,
|
||||
startDate,
|
||||
|
@ -290,56 +302,3 @@ export const reportOnQueue = async (
|
|||
|
||||
return res.status(200).json({ message: "ok" });
|
||||
};
|
||||
|
||||
export const reportService = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
if (
|
||||
req.user.profile !== "master" &&
|
||||
req.user.profile !== "admin" &&
|
||||
req.user.profile !== "supervisor"
|
||||
) {
|
||||
throw new AppError("ERR_NO_PERMISSION", 403);
|
||||
}
|
||||
|
||||
const { startDate, endDate, queueId } = req.query as IndexQuery;
|
||||
|
||||
console.log(
|
||||
`startDate: ${startDate} | endDate: ${endDate} | queueId: ${queueId}`
|
||||
);
|
||||
|
||||
const reportService = await ReportByNumberQueueService({
|
||||
startDate,
|
||||
endDate
|
||||
});
|
||||
|
||||
return res.status(200).json({ reportService });
|
||||
};
|
||||
|
||||
export const reportServiceByQueue = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<Response> => {
|
||||
if (
|
||||
req.user.profile !== "master" &&
|
||||
req.user.profile !== "admin" &&
|
||||
req.user.profile !== "supervisor"
|
||||
) {
|
||||
throw new AppError("ERR_NO_PERMISSION", 403);
|
||||
}
|
||||
|
||||
const { startDate, endDate, queueId } = req.query as IndexQuery;
|
||||
|
||||
console.log(
|
||||
`startDate: ${startDate} | endDate: ${endDate} | queueId: ${queueId}`
|
||||
);
|
||||
|
||||
const reportService = await ReportByNumberQueueService({
|
||||
startDate,
|
||||
endDate,
|
||||
queue: true
|
||||
});
|
||||
|
||||
return res.status(200).json({ reportService });
|
||||
};
|
||||
|
|
|
@ -13,14 +13,6 @@ reportRoutes.post("/reports/onqueue", ReportController.reportOnQueue);
|
|||
|
||||
reportRoutes.get("/reports/user/services", isAuth, ReportController.reportUserService);
|
||||
|
||||
reportRoutes.get(
|
||||
"/reports/services/numbers",
|
||||
isAuth,
|
||||
ReportController.reportService
|
||||
);
|
||||
|
||||
reportRoutes.get("/reports/services/queues", isAuth, ReportController.reportServiceByQueue);
|
||||
|
||||
reportRoutes.get("/reports/messages", isAuth, ReportController.reportMessagesUserByDateStartDateEnd);
|
||||
|
||||
export default reportRoutes;
|
||||
|
|
|
@ -1,280 +0,0 @@
|
|||
import { Sequelize } from "sequelize";
|
||||
|
||||
const dbConfig = require("../../config/database");
|
||||
const sequelize = new Sequelize(dbConfig);
|
||||
const { QueryTypes } = require("sequelize");
|
||||
|
||||
import { splitDateTime } from "../../helpers/SplitDateTime";
|
||||
import format from "date-fns/format";
|
||||
import ptBR from "date-fns/locale/pt-BR";
|
||||
import Whatsapp from "../../models/Whatsapp";
|
||||
import { number } from "yup";
|
||||
import ShowWhatsAppService from "../WhatsappService/ShowWhatsAppService";
|
||||
|
||||
interface Request {
|
||||
startDate: string | number;
|
||||
endDate: string;
|
||||
queue?: boolean;
|
||||
}
|
||||
|
||||
const ReportByNumberQueueService = async ({
|
||||
startDate,
|
||||
endDate,
|
||||
queue = false
|
||||
}: Request): Promise<any[]> => {
|
||||
let reportServiceData: any[] = [];
|
||||
|
||||
const whatsapps = await Whatsapp.findAll();
|
||||
|
||||
if (!queue) {
|
||||
for (const whatsapp of whatsapps) {
|
||||
const { id, name, number } = whatsapp;
|
||||
|
||||
if (
|
||||
!number ||
|
||||
reportServiceData.findIndex((w: any) => w?.number == number) != -1
|
||||
)
|
||||
continue;
|
||||
|
||||
console.log("NUMBER: ", number);
|
||||
|
||||
// CHAT STARTED BY AGENT
|
||||
const startedByAgent: any = 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 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.fromAgent = 1
|
||||
AND w.number = ${number};`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// CHAT STARTED BY CLIENT
|
||||
const startedByClient: any = 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 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 w.number = ${number};`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// CHAT CLOSED
|
||||
const closedChat: any = 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 DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
|
||||
AND t.status = 'closed'
|
||||
AND w.number = ${number};`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// CHAT AVG WAINTING TIME
|
||||
const avgChatWaitingTime: any = await sequelize.query(
|
||||
`SELECT SEC_TO_TIME(
|
||||
AVG(
|
||||
TIMESTAMPDIFF(
|
||||
SECOND,
|
||||
(
|
||||
SELECT createdAt
|
||||
FROM Messages
|
||||
WHERE ticketId = m.ticketId
|
||||
AND fromMe = 0
|
||||
ORDER BY createdAt ASC
|
||||
LIMIT 1
|
||||
),
|
||||
(
|
||||
SELECT createdAt
|
||||
FROM Messages
|
||||
WHERE ticketId = m.ticketId
|
||||
AND fromAgent = 1
|
||||
ORDER BY createdAt ASC
|
||||
LIMIT 1
|
||||
)
|
||||
)
|
||||
)
|
||||
) AS AVG_AWAITING_TIME
|
||||
FROM Tickets t
|
||||
JOIN Messages m ON t.id = m.ticketId
|
||||
JOIN Whatsapps w ON t.whatsappId = w.id
|
||||
JOIN Queues q ON q.id = t.queueId
|
||||
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
|
||||
AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
|
||||
AND m.fromMe = 0
|
||||
-- AND q.id = 2
|
||||
AND w.number = ${number}
|
||||
AND (t.status = 'open' OR t.status = 'closed');`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// CHAT PENDING
|
||||
const pendingChat: any = 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 DATE(m.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({
|
||||
id,
|
||||
name,
|
||||
number,
|
||||
startedByAgent: startedByAgent[0]?.ticket_count,
|
||||
startedByClient: startedByClient[0]?.ticket_count,
|
||||
closedChat: closedChat[0]?.ticket_count,
|
||||
avgChatWaitingTime: avgChatWaitingTime[0]?.AVG_AWAITING_TIME
|
||||
? avgChatWaitingTime[0]?.AVG_AWAITING_TIME
|
||||
: 0,
|
||||
pendingChat: pendingChat[0]?.ticket_count
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (const whatsapp of whatsapps) {
|
||||
const { id, name, number } = whatsapp;
|
||||
|
||||
if (
|
||||
!number ||
|
||||
reportServiceData.findIndex((w: any) => w?.number == number) != -1
|
||||
)
|
||||
continue;
|
||||
|
||||
const data = await ShowWhatsAppService(id);
|
||||
|
||||
const queues: any = data.queues.map((q: any) => {
|
||||
const { id, name, color } = q;
|
||||
return { id, name, color };
|
||||
});
|
||||
|
||||
console.log("NUMBER 2: ", number);
|
||||
|
||||
for (const q of queues) {
|
||||
// CHAT STARTED BY AGENT
|
||||
const startedByAgent: any = 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 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.fromAgent = 1
|
||||
AND q.id = ${q.id};`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// CHAT STARTED BY CLIENT
|
||||
const startedByClient: any = 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 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};`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// CHAT CLOSED
|
||||
const closedChat: any = 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 DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
|
||||
AND t.status = 'closed'
|
||||
AND q.id = ${q.id};`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// CHAT AVG WAINTING TIME
|
||||
const avgChatWaitingTime: any = await sequelize.query(
|
||||
`SELECT SEC_TO_TIME(
|
||||
AVG(
|
||||
TIMESTAMPDIFF(
|
||||
SECOND,
|
||||
(
|
||||
SELECT createdAt
|
||||
FROM Messages
|
||||
WHERE ticketId = m.ticketId
|
||||
AND fromMe = 0
|
||||
ORDER BY createdAt ASC
|
||||
LIMIT 1
|
||||
),
|
||||
(
|
||||
SELECT createdAt
|
||||
FROM Messages
|
||||
WHERE ticketId = m.ticketId
|
||||
AND fromAgent = 1
|
||||
ORDER BY createdAt ASC
|
||||
LIMIT 1
|
||||
)
|
||||
)
|
||||
)
|
||||
) AS AVG_AWAITING_TIME
|
||||
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 = 'open' OR t.status = 'closed');`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// CHAT PENDING
|
||||
const pendingChat: any = 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 DATE(m.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({
|
||||
id,
|
||||
name,
|
||||
number,
|
||||
queueName: q.name,
|
||||
queueColor: q.color,
|
||||
startedByAgent: startedByAgent[0]?.ticket_count,
|
||||
startedByClient: startedByClient[0]?.ticket_count,
|
||||
closedChat: closedChat[0]?.ticket_count,
|
||||
avgChatWaitingTime: avgChatWaitingTime[0]?.AVG_AWAITING_TIME
|
||||
? avgChatWaitingTime[0]?.AVG_AWAITING_TIME
|
||||
: 0,
|
||||
pendingChat: pendingChat[0]?.ticket_count
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reportServiceData;
|
||||
};
|
||||
|
||||
export default ReportByNumberQueueService;
|
|
@ -80,7 +80,7 @@ const UpdateTicketService = async ({
|
|||
|
||||
if (msg?.trim().length > 0) {
|
||||
setTimeout(async () => {
|
||||
sendWhatsAppMessageSocket(ticket, `\u200e${msg}`);
|
||||
sendWhatsAppMessageSocket(ticket, msg);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
|
|
|
@ -392,7 +392,7 @@ const verifyQueue = async (
|
|||
if (outService.length > 0) {
|
||||
const { type, msg: msgOutService } = outService[0];
|
||||
console.log(`${type} message ignored on queue`);
|
||||
botSendMessage(ticket, `\u200e${msgOutService}`);
|
||||
botSendMessage(ticket, msgOutService);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -529,7 +529,6 @@ const transferTicket = async (
|
|||
sendGreetingMessage?: boolean
|
||||
) => {
|
||||
const botInfo = await BotIsOnQueue("botqueue");
|
||||
const io = getIO();
|
||||
|
||||
const queuesWhatsGreetingMessage = await queuesOutBot(
|
||||
wbot,
|
||||
|
@ -549,7 +548,6 @@ const transferTicket = async (
|
|||
}
|
||||
|
||||
if (queue) await botTransferTicket(queue, ticket, sendGreetingMessage);
|
||||
io.emit('notifyPeding', {data: {ticket, queue}});
|
||||
};
|
||||
|
||||
const botTransferTicket = async (
|
||||
|
@ -585,7 +583,7 @@ const botTransferTicketToUser = async (
|
|||
};
|
||||
|
||||
const botSendMessage = (ticket: Ticket, msg: string) => {
|
||||
const { phoneNumberId } = ticket;
|
||||
const { phoneNumberId } = ticket;
|
||||
|
||||
const debouncedSentMessage = debounce(
|
||||
async () => {
|
||||
|
@ -643,10 +641,27 @@ const handleMessage = async (
|
|||
let msgContact: any = wbot.msgContact;
|
||||
// let groupContact: Contact | undefined;
|
||||
|
||||
if (msg.fromMe) {
|
||||
if (msg.fromMe) {
|
||||
const whatsapp = await whatsappInfo(wbot.id);
|
||||
|
||||
if (whatsapp?.number) {
|
||||
const ticketExpiration = await SettingTicket.findOne({
|
||||
where: { key: "ticketExpiration", number: whatsapp.number }
|
||||
});
|
||||
|
||||
if (
|
||||
ticketExpiration &&
|
||||
ticketExpiration.value == "enabled" &&
|
||||
ticketExpiration?.message.trim() == msg.body.trim()
|
||||
) {
|
||||
console.log("*********** TICKET EXPIRATION");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// messages sent automatically by wbot have a special character in front of it
|
||||
// if so, this message was already been stored in database;
|
||||
if (/\u200e/.test(msg.body[0])) return;
|
||||
// if so, this message was already been stored in database;
|
||||
// if (/\u200e/.test(msg.body[0])) return;
|
||||
|
||||
// media messages sent from me from cell phone, first comes with "hasMedia = false" and type = "image/ptt/etc"
|
||||
// in this case, return and let this message be handled by "media_uploaded" event, when it will have "hasMedia = true"
|
||||
|
@ -918,28 +933,25 @@ const handleMessage = async (
|
|||
ticket.status == "pending" &&
|
||||
ticket.queueId
|
||||
) {
|
||||
if (botInfo.isOnQueue) {
|
||||
let choosenQueue = await ShowQueueService(botInfo.botQueueId);
|
||||
let choosenQueue = await ShowQueueService(botInfo.botQueueId);
|
||||
|
||||
await UpdateTicketService({
|
||||
ticketData: {
|
||||
status: "open",
|
||||
userId: botInfo.userIdBot,
|
||||
queueId: choosenQueue.id
|
||||
},
|
||||
ticketId: ticket.id
|
||||
});
|
||||
const menuMsg: any = await menu(msg.body, wbot.id, contact.id);
|
||||
await botSendMessage(ticket, menuMsg.value);
|
||||
}
|
||||
await UpdateTicketService({
|
||||
ticketData: {
|
||||
status: "open",
|
||||
userId: botInfo.userIdBot,
|
||||
queueId: choosenQueue.id
|
||||
},
|
||||
ticketId: ticket.id
|
||||
});
|
||||
const menuMsg: any = await menu(msg.body, wbot.id, contact.id);
|
||||
await botSendMessage(ticket, menuMsg.value);
|
||||
|
||||
return;
|
||||
} else if (
|
||||
!msg.fromMe &&
|
||||
msg.body == "#" &&
|
||||
ticket.status == "pending" &&
|
||||
ticket.queueId &&
|
||||
botInfo.isOnQueue
|
||||
ticket.queueId
|
||||
) {
|
||||
let choosenQueue = await ShowQueueService(botInfo.botQueueId);
|
||||
|
||||
|
@ -976,7 +988,7 @@ const handleMessage = async (
|
|||
if (ticket?.queueId) {
|
||||
ticketHasQueue = true;
|
||||
}
|
||||
|
||||
|
||||
if (ticketHasQueue && ticket.status != "open") {
|
||||
let whatsapp: any = await whatsappInfo(ticket?.whatsappId);
|
||||
|
||||
|
@ -990,7 +1002,7 @@ const handleMessage = async (
|
|||
return;
|
||||
}
|
||||
|
||||
botSendMessage(ticket, `\u200e${msgOutService}`);
|
||||
botSendMessage(ticket, msgOutService);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,12 @@ const ListWhatsAppsNumber = async (
|
|||
let whatsapps: any = [];
|
||||
|
||||
if (whatsapp) {
|
||||
if (status) {
|
||||
if (status) {
|
||||
whatsapps = await Whatsapp.findAll({
|
||||
raw: true,
|
||||
where: { number: whatsapp.number, status: status, },
|
||||
attributes: ["id", "number", "status", "isDefault", "url", 'isOfficial']
|
||||
});
|
||||
});
|
||||
} else {
|
||||
whatsapps = await Whatsapp.findAll({
|
||||
raw: true,
|
||||
|
|
|
@ -38,29 +38,7 @@ const useStyles = makeStyles(theme => ({
|
|||
noShadow: {
|
||||
boxShadow: "none !important",
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
let _fifo
|
||||
|
||||
// const onlineEmitter = async (socket, user) => {
|
||||
|
||||
// try {
|
||||
// clearInterval(_fifo);
|
||||
|
||||
// socket.emit("online", user.id)
|
||||
|
||||
// } catch (error) {
|
||||
// console.log('error on onlineEmitter: ', error)
|
||||
// }
|
||||
// finally {
|
||||
// _fifo = setInterval(onlineEmitter, 3000);
|
||||
// }
|
||||
// }
|
||||
|
||||
// _fifo = setInterval(onlineEmitter, 3000);
|
||||
|
||||
|
||||
}))
|
||||
|
||||
|
||||
const NotificationsPopOver = () => {
|
||||
|
@ -85,9 +63,7 @@ const NotificationsPopOver = () => {
|
|||
const { handleLogout } = useContext(AuthContext)
|
||||
|
||||
// const [lastRef] = useState(+history.location.pathname.split("/")[2])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
soundAlertRef.current = play
|
||||
|
@ -104,9 +80,6 @@ const NotificationsPopOver = () => {
|
|||
}, [tickets])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
|
||||
ticketIdRef.current = ticketIdUrl
|
||||
}, [ticketIdUrl])
|
||||
|
||||
|
@ -130,8 +103,6 @@ const NotificationsPopOver = () => {
|
|||
|
||||
if (data.action === "logout") {
|
||||
|
||||
|
||||
|
||||
if (`${user.id}` === data.userOnlineTime['userId']) {
|
||||
|
||||
socket.emit("online", { logoutUserId: user.id })
|
||||
|
@ -140,7 +111,6 @@ const NotificationsPopOver = () => {
|
|||
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
|
||||
// socket.on("isOnline", (data) => {
|
||||
|
@ -159,21 +129,24 @@ const NotificationsPopOver = () => {
|
|||
|
||||
if (user.profile === 'user') {
|
||||
|
||||
if (_fifo) {
|
||||
clearInterval(_fifo)
|
||||
}
|
||||
// if (_fifo) {
|
||||
// clearInterval(_fifo);
|
||||
// }
|
||||
|
||||
_fifo = setInterval(() => {
|
||||
// _fifo = setInterval(() => {
|
||||
// socket.emit("online", user.id)
|
||||
// }, 3000);
|
||||
|
||||
const intID = setInterval(() => {
|
||||
console.log('emitting the online')
|
||||
socket.emit("online", user.id)
|
||||
}, 3000)
|
||||
|
||||
return () => clearInterval(intID)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return () => {
|
||||
socket.disconnect()
|
||||
}
|
||||
|
@ -189,9 +162,7 @@ const NotificationsPopOver = () => {
|
|||
|
||||
|
||||
socket.on("ticket", data => {
|
||||
if (data.action === "updateUnread" || data.action === "delete") {
|
||||
|
||||
|
||||
if (data.action === "updateUnread" || data.action === "delete") {
|
||||
|
||||
setNotifications(prevState => {
|
||||
const ticketIndex = prevState.findIndex(t => t.id === data.ticketId)
|
||||
|
@ -216,26 +187,15 @@ const NotificationsPopOver = () => {
|
|||
}
|
||||
})
|
||||
|
||||
socket.on("appMessage", data => {
|
||||
|
||||
|
||||
socket.on("appMessage", data => {
|
||||
|
||||
if (
|
||||
data.action === "create" &&
|
||||
!data.message.read &&
|
||||
(data.ticket.userId === user?.id || !data.ticket.userId)
|
||||
) {
|
||||
) {
|
||||
|
||||
|
||||
|
||||
|
||||
setNotifications(prevState => {
|
||||
|
||||
|
||||
|
||||
// prevState.forEach((e)=>{
|
||||
//
|
||||
// })
|
||||
setNotifications(prevState => {
|
||||
|
||||
const ticketIndex = prevState.findIndex(t => t.id === data.ticket.id)
|
||||
if (ticketIndex !== -1) {
|
||||
|
@ -245,59 +205,88 @@ const NotificationsPopOver = () => {
|
|||
}
|
||||
|
||||
return [data.ticket, ...prevState]
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
const shouldNotNotificate = (data.message.ticketId === ticketIdRef.current && document.visibilityState === "visible") ||
|
||||
(data.ticket.userId && data.ticket.userId !== user?.id) ||
|
||||
data.ticket.isGroup || !data.ticket.userId
|
||||
|
||||
if (shouldNotNotificate) return
|
||||
|
||||
|
||||
if (shouldNotNotificate) return
|
||||
|
||||
handleNotifications(data)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('notifyPeding', data =>{
|
||||
handleNotifications("", data);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.disconnect()
|
||||
}
|
||||
}, [user])
|
||||
|
||||
const handleNotifications = data => {
|
||||
const { message, contact, ticket } = data
|
||||
const handleNotifications = (data, notify) => {
|
||||
let isQueue = false;
|
||||
if(!notify){
|
||||
const { message, contact, ticket } = data
|
||||
|
||||
const options = {
|
||||
body: `${message.body} - ${format(new Date(), "HH:mm")}`,
|
||||
icon: contact.profilePicUrl,
|
||||
tag: ticket.id,
|
||||
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]
|
||||
const options = {
|
||||
body: `${message.body} - ${format(new Date(), "HH:mm")}`,
|
||||
icon: contact.profilePicUrl,
|
||||
tag: ticket.id,
|
||||
renotify: true,
|
||||
}
|
||||
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 && notify){
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -11,31 +11,32 @@ import { AuthContext } from "../../context/Auth/AuthContext"
|
|||
import { Can } from "../../components/Can"
|
||||
import FormControlLabel from "@mui/material/FormControlLabel"
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
|
||||
|
||||
|
||||
import { Button } from "@material-ui/core"
|
||||
|
||||
import ReportModal from "../../components/ReportModal"
|
||||
import ReportModalType from "../../components/ReportModalType"
|
||||
|
||||
import MaterialTable from 'material-table'
|
||||
|
||||
import LogoutIcon from '@material-ui/icons/CancelOutlined'
|
||||
|
||||
import apiBroker from "../../services/apiBroker"
|
||||
import fileDownload from 'js-file-download'
|
||||
|
||||
|
||||
import openSocket from "socket.io-client"
|
||||
|
||||
import { i18n } from "../../translate/i18n"
|
||||
|
||||
import Switch from '@mui/material/Switch'
|
||||
|
||||
const label = { inputProps: { 'aria-label': 'Size switch demo' } }
|
||||
|
||||
const report = [
|
||||
{ 'value': '1', 'label': 'Atendimento por atendentes' },
|
||||
{ 'value': '2', 'label': 'Usuários online/offline' },
|
||||
{ 'value': '3', 'label': 'Relatorio de atendimento por numeros' },
|
||||
{ 'value': '4', 'label': 'Relatorio de atendimento por filas' },
|
||||
]
|
||||
|
||||
const reportOptType = [
|
||||
{ 'value': '1', 'label': 'Padrão' },
|
||||
{ 'value': '2', 'label': 'Sintético' },
|
||||
{ 'value': '3', 'label': 'Analítico' }
|
||||
]
|
||||
const report = [{ 'value': '1', 'label': 'Atendimento por atendentes' }, { 'value': '2', 'label': 'Usuários online/offline' }]
|
||||
const reportOptType = [{ 'value': '1', 'label': 'Padrão' }, { 'value': '2', 'label': 'Sintético' }, { 'value': '3', 'label': 'Analítico' }]
|
||||
|
||||
|
||||
|
||||
|
@ -234,9 +235,7 @@ let columnsData = [
|
|||
|
||||
{ title: `${i18n.t("reports.listColumns.column1_7")}`, field: 'createdAt' },
|
||||
{ title: `${i18n.t("reports.listColumns.column1_8")}`, field: 'updatedAt' },
|
||||
{ title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' },
|
||||
{ title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true },
|
||||
]
|
||||
{ title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' }]
|
||||
|
||||
let columnsDataSuper = [
|
||||
{ title: `${i18n.t("reports.listColumns.column1_1")}`, field: 'whatsapp.name' },
|
||||
|
@ -248,8 +247,7 @@ let columnsDataSuper = [
|
|||
|
||||
{ title: `${i18n.t("reports.listColumns.column1_7")}`, field: 'createdAt' },
|
||||
{ title: `${i18n.t("reports.listColumns.column1_8")}`, field: 'updatedAt' },
|
||||
{ title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' },
|
||||
{ title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true },
|
||||
{ title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' }
|
||||
]
|
||||
|
||||
|
||||
|
@ -302,7 +300,7 @@ const Report = () => {
|
|||
|
||||
const [reportTypeList,] = useState(reportOptType)
|
||||
const [reportType, setReportType] = useState('1')
|
||||
const [firstLoad, setFirstLoad] = useState(true)
|
||||
const [firstLoad, setFirstLoad] = useState(true);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -314,12 +312,12 @@ const Report = () => {
|
|||
|
||||
|
||||
useEffect(() => {
|
||||
if (firstLoad) {
|
||||
if (firstLoad) {
|
||||
setFirstLoad(false)
|
||||
} else {
|
||||
|
||||
|
||||
}
|
||||
}, [firstLoad])
|
||||
}, [firstLoad]);
|
||||
|
||||
useEffect(() => {
|
||||
//setLoading(true);
|
||||
|
@ -354,12 +352,12 @@ const Report = () => {
|
|||
//setLoading(true);
|
||||
if (firstLoad) return
|
||||
|
||||
const delayDebounceFn = setTimeout(() => {
|
||||
const delayDebounceFn = setTimeout(() => {
|
||||
setLoading(true)
|
||||
const fetchQueries = async () => {
|
||||
try {
|
||||
if (reportOption === '1') {
|
||||
|
||||
if (reportOption === '1') {
|
||||
|
||||
const { data } = await api.get("/reports/", { params: { userId, startDate, endDate, pageNumber: pageNumberTickets, createdOrUpdated: selectedValue, queueId }, userQueues: userA.queues })
|
||||
|
||||
let ticketsQueue = data.tickets
|
||||
|
@ -371,12 +369,8 @@ const Report = () => {
|
|||
filterQueuesTickets = ticketsQueue.filter(ticket => ticket?.queue?.name === userQueues[0]?.name)
|
||||
}
|
||||
data.tickets = filterQueuesTickets
|
||||
const tickets = data.tickets.map(ticket => ({
|
||||
...ticket,
|
||||
messagesToFilter: ticket.messages.map(message => message.body).join(' '),
|
||||
}))
|
||||
dispatchQ({ type: "LOAD_QUERY", payload: tickets })
|
||||
console.log(tickets)
|
||||
dispatchQ({ type: "LOAD_QUERY", payload: data.tickets })
|
||||
|
||||
setHasMore(data.hasMore)
|
||||
setTotalCountTickets(data.count)
|
||||
setLoading(false)
|
||||
|
@ -392,22 +386,6 @@ const Report = () => {
|
|||
//setLoading(false);
|
||||
|
||||
}
|
||||
else if (reportOption === '3') {
|
||||
const dataQuery = await api.get("/reports/services/numbers", { params: { startDate, endDate }, })
|
||||
|
||||
console.log('DATA QUERY.data numbers: ', dataQuery.data)
|
||||
|
||||
dispatchQ({ type: "RESET" })
|
||||
dispatchQ({ type: "LOAD_QUERY", payload: dataQuery?.data?.reportService })
|
||||
}
|
||||
else if (reportOption === '4') {
|
||||
const dataQuery = await api.get("/reports/services/queues", { params: { startDate, endDate }, })
|
||||
|
||||
console.log('DATA QUERY.data queues: ', dataQuery.data)
|
||||
|
||||
dispatchQ({ type: "RESET" })
|
||||
dispatchQ({ type: "LOAD_QUERY", payload: dataQuery?.data?.reportService })
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
|
@ -457,7 +435,7 @@ const Report = () => {
|
|||
setChecked(true)
|
||||
}
|
||||
setReport(data)
|
||||
}
|
||||
}
|
||||
|
||||
// Get from report type option
|
||||
const reportTypeValue = (data) => {
|
||||
|
@ -471,7 +449,7 @@ const Report = () => {
|
|||
|
||||
setReportType(data)
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (reportOption === '1') {
|
||||
|
@ -750,26 +728,23 @@ const Report = () => {
|
|||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', padding: '10px 0', alignItems: 'center', }}>
|
||||
|
||||
{(reportOption === '1' || reportOption === '2') &&
|
||||
<>
|
||||
{checked ?
|
||||
<SelectField
|
||||
func={textFieldSelectUser}
|
||||
emptyField={true}
|
||||
header={i18n.t("reports.user")}
|
||||
currencies={users.map((obj) => {
|
||||
return { 'value': obj.id, 'label': obj.name }
|
||||
})} /> :
|
||||
<SelectField
|
||||
func={textFieldSelectQueue}
|
||||
emptyField={true}
|
||||
header={'Filas'}
|
||||
currencies={queues.map((obj) => {
|
||||
return { 'value': obj.id, 'label': obj.name }
|
||||
})} />
|
||||
}
|
||||
</>
|
||||
{checked ?
|
||||
<SelectField
|
||||
func={textFieldSelectUser}
|
||||
emptyField={true}
|
||||
header={i18n.t("reports.user")}
|
||||
currencies={users.map((obj) => {
|
||||
return { 'value': obj.id, 'label': obj.name }
|
||||
})} /> :
|
||||
<SelectField
|
||||
func={textFieldSelectQueue}
|
||||
emptyField={true}
|
||||
header={'Filas'}
|
||||
currencies={queues.map((obj) => {
|
||||
return { 'value': obj.id, 'label': obj.name }
|
||||
})} />
|
||||
}
|
||||
|
||||
{reportOption === '1' &&
|
||||
<div>
|
||||
<label>
|
||||
|
@ -785,8 +760,6 @@ const Report = () => {
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
|
@ -928,215 +901,6 @@ const Report = () => {
|
|||
]}
|
||||
|
||||
|
||||
options={
|
||||
{
|
||||
search: true,
|
||||
selection: false,
|
||||
paging: false,
|
||||
padding: 'dense',
|
||||
sorting: true,
|
||||
searchFieldStyle: {
|
||||
width: 300,
|
||||
},
|
||||
|
||||
pageSize: 20,
|
||||
headerStyle: {
|
||||
position: "sticky",
|
||||
top: "0"
|
||||
},
|
||||
maxBodyHeight: "400px",
|
||||
|
||||
rowStyle: {
|
||||
fontSize: 14,
|
||||
}
|
||||
|
||||
}}
|
||||
/>
|
||||
|
||||
}
|
||||
{reportOption === '3' &&
|
||||
|
||||
<MaterialTable
|
||||
|
||||
localization={{
|
||||
|
||||
// header: {
|
||||
// actions: 'Deslogar'
|
||||
// },
|
||||
|
||||
}}
|
||||
|
||||
title={i18n.t("reports.listTitles.title4_1")}
|
||||
columns={
|
||||
[
|
||||
|
||||
// { title: 'Foto', field: 'ticket.contact.profilePicUrl', render: rowData => <img src={rowData['ticket.contact.profilePicUrl']} alt="imagem de perfil do whatsapp" style={{ width: 40, borderRadius: '50%' }} /> },
|
||||
{ title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, },
|
||||
|
||||
{
|
||||
title: 'Conversas iniciadas', field: 'startedByAgent',
|
||||
|
||||
// cellStyle: (e, rowData) => {
|
||||
|
||||
// if (rowData['statusOnline'] && rowData['statusOnline'].status) {
|
||||
|
||||
// if (rowData['statusOnline'].status === 'offline') {
|
||||
|
||||
// return { color: "red" }
|
||||
// }
|
||||
// else if (rowData['statusOnline'].status === 'online') {
|
||||
// return { color: "green" }
|
||||
// }
|
||||
// else if (rowData['statusOnline'].status === 'logout...') {
|
||||
// return { color: "orange" }
|
||||
// }
|
||||
// else if (rowData['statusOnline'].status === 'waiting...') {
|
||||
// return { color: "orange" }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// },
|
||||
|
||||
},
|
||||
|
||||
{ title: 'Conversas recebidas', field: 'startedByClient' },
|
||||
{ title: `Conversas finalizadas`, field: 'closedChat' },
|
||||
{ title: `Tempo médio de espera`, field: 'avgChatWaitingTime' },
|
||||
{ title: 'Aguardando', field: 'pendingChat' }
|
||||
|
||||
]
|
||||
}
|
||||
data={dataRows}
|
||||
|
||||
// actions={[
|
||||
// (rowData) => {
|
||||
|
||||
// if (rowData.statusOnline &&
|
||||
// rowData.statusOnline['status'] &&
|
||||
// rowData.statusOnline['status'] === 'online') {
|
||||
|
||||
|
||||
// return {
|
||||
// icon: LogoutIcon,
|
||||
// tooltip: 'deslogar',
|
||||
// disable: false,
|
||||
// onClick: (event, rowData) => {
|
||||
// handleLogouOnlineUser(rowData.id)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// }
|
||||
// }
|
||||
// ]}
|
||||
|
||||
|
||||
options={
|
||||
{
|
||||
search: true,
|
||||
selection: false,
|
||||
paging: false,
|
||||
padding: 'dense',
|
||||
sorting: true,
|
||||
searchFieldStyle: {
|
||||
width: 300,
|
||||
},
|
||||
|
||||
pageSize: 20,
|
||||
headerStyle: {
|
||||
position: "sticky",
|
||||
top: "0"
|
||||
},
|
||||
maxBodyHeight: "400px",
|
||||
|
||||
rowStyle: {
|
||||
fontSize: 14,
|
||||
}
|
||||
|
||||
}}
|
||||
/>
|
||||
|
||||
}
|
||||
{reportOption === '4' &&
|
||||
|
||||
<MaterialTable
|
||||
|
||||
localization={{
|
||||
|
||||
// header: {
|
||||
// actions: 'Deslogar'
|
||||
// },
|
||||
|
||||
}}
|
||||
|
||||
title={i18n.t("reports.listTitles.title5_1")}
|
||||
columns={
|
||||
[
|
||||
|
||||
// { title: 'Foto', field: 'ticket.contact.profilePicUrl', render: rowData => <img src={rowData['ticket.contact.profilePicUrl']} alt="imagem de perfil do whatsapp" style={{ width: 40, borderRadius: '50%' }} /> },
|
||||
|
||||
{ title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, },
|
||||
{ title: 'Fila', field: 'queueName', cellStyle: { whiteSpace: 'nowrap' }, },
|
||||
{
|
||||
title: 'Conversas iniciadas', field: 'startedByAgent',
|
||||
|
||||
// cellStyle: (e, rowData) => {
|
||||
|
||||
// if (rowData['statusOnline'] && rowData['statusOnline'].status) {
|
||||
|
||||
// if (rowData['statusOnline'].status === 'offline') {
|
||||
|
||||
// return { color: "red" }
|
||||
// }
|
||||
// else if (rowData['statusOnline'].status === 'online') {
|
||||
// return { color: "green" }
|
||||
// }
|
||||
// else if (rowData['statusOnline'].status === 'logout...') {
|
||||
// return { color: "orange" }
|
||||
// }
|
||||
// else if (rowData['statusOnline'].status === 'waiting...') {
|
||||
// return { color: "orange" }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// },
|
||||
|
||||
},
|
||||
|
||||
{ title: 'Conversas recebidas', field: 'startedByClient' },
|
||||
{ title: `Conversas finalizadas`, field: 'closedChat' },
|
||||
{ title: `Tempo médio de espera`, field: 'avgChatWaitingTime' },
|
||||
{ title: 'Aguardando', field: 'pendingChat' }
|
||||
|
||||
]
|
||||
}
|
||||
data={dataRows}
|
||||
|
||||
// actions={[
|
||||
// (rowData) => {
|
||||
|
||||
// if (rowData.statusOnline &&
|
||||
// rowData.statusOnline['status'] &&
|
||||
// rowData.statusOnline['status'] === 'online') {
|
||||
|
||||
|
||||
// return {
|
||||
// icon: LogoutIcon,
|
||||
// tooltip: 'deslogar',
|
||||
// disable: false,
|
||||
// onClick: (event, rowData) => {
|
||||
// handleLogouOnlineUser(rowData.id)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// }
|
||||
// }
|
||||
// ]}
|
||||
|
||||
|
||||
options={
|
||||
{
|
||||
search: true,
|
||||
|
|
|
@ -313,9 +313,7 @@ const messages = {
|
|||
title0_1: "Lembretes/Agendamentos",
|
||||
title1_1: "Atendimento por atendentes",
|
||||
title2_1: "Chat do atendimento pelo Whatsapp",
|
||||
title3_1: "Usuários online/offline",
|
||||
title4_1: "Relatório de atendimento por números",
|
||||
title5_1: "Relatório de atendimento por filas"
|
||||
title3_1: "Usuários online/offline"
|
||||
},
|
||||
listColumns:{
|
||||
column0_1: 'Ações',
|
||||
|
|
Loading…
Reference in New Issue