Compare commits

...

7 Commits

Author SHA1 Message Date
gustavo-gsp 4a6fb3f61e fix: correct bug in the reports table, invalid column displaying
Details:
- Addressed a bug in the reports table where an invalid column was being displayed.
2024-05-02 13:55:50 -03:00
adriano 6baca795c3 chore: Update search title in pt.js translation file 2024-05-02 08:23:47 -03:00
adriano a2e0e53540 Merge branch 'merge_el_melhorias' of github.com:AdrianoRobson/projeto-hit into merge_el_melhorias 2024-04-30 17:02:14 -03:00
gustavo-gsp fea60cf80c fix: correct code errors
Details:
- Addressed various code errors and made necessary fixes.

feat: add column 'type' to the Reports Mtable to differentiate remote tickets from regular ones

Details:
- Added a new column named 'type' to the Reports Mtable to differentiate between remote tickets and regular ones.
2024-04-30 16:20:48 -03:00
gustavo-gsp 0e8fbd8400 feat: add new column 'transferToOtherQueues' to the Users table
Details:
- Created a new column named 'transferToOtherQueues' in the Users table.

feat: add field 'can transfer to other queues' in the user modal

Details:
- Added a field in the user modal to indicate if the user can transfer tickets to other queues.

feat: implement functionality to allow users with 'transferToOtherTickets' set to true to transfer tickets to other queues, even when disabled in OmniHit

Details:
- Implemented functionality to allow users with 'transferToOtherTickets' set to true to transfer tickets to other queues, even when this feature is disabled in OmniHit.

feat: add options to enable or disable functionalities: notify on new ticket arrival, block audio and video messages, show average waiting time of tickets in waiting status

Details:
- Added options to enable or disable the following functionalities: notification on new ticket arrival, blocking of audio and video messages, and displaying average waiting time of tickets in waiting status.

feat: add filter in the dashboard to show data only from the queues the user is linked to, and allow the user to view data from a selected queue only

Details:
- Added a filter in the dashboard to display data only from the queues the user is linked to, and to allow the user to view data from a selected queue only.
2024-04-29 10:00:51 -03:00
gustavo-gsp cc12cafb99 merge changes, to merge the updated versions
Merge branch 'el_lojas_melhorias' of github.com:AdrianoRobson/projeto-hit into el_lojas_melhorias
2024-04-18 10:19:36 -03:00
gustavo-gsp 860d462d37 feat: add sound notifications and popOver for new tickets in waiting status, with option to enable in settings
Details:
- Implemented functionality to provide sound notifications and popOver alerts for new tickets in waiting status. Users can enable this feature in the settings.

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

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

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

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

feat: remove report export options for supervisor profile

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

View File

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

View File

@ -66,7 +66,7 @@ export const reportUserByDateStartDateEnd = async (
endDate, endDate,
pageNumber, pageNumber,
createdOrUpdated, createdOrUpdated,
queueId queueId,
}); });
const queues = await Queue.findAll({ attributes: ["id", "name"] }); const queues = await Queue.findAll({ attributes: ["id", "name"] });
@ -85,11 +85,12 @@ export const reportUserService = async (
) { ) {
throw new AppError("ERR_NO_PERMISSION", 403); throw new AppError("ERR_NO_PERMISSION", 403);
} }
const { userId, startDate, endDate } = req.query as IndexQuery; const { userId, startDate, endDate, userQueues} = req.query as IndexQuery;
// let usersProfile = await ListUserParamiterService({ profile: 'user' }) // let usersProfile = await ListUserParamiterService({ profile: 'user' })
let usersProfile = await ListUserParamiterService({ let usersProfile = await ListUserParamiterService({
profiles: ["user", "supervisor"], profiles: ["user", "supervisor"],
userQueues: userQueues ? userQueues : undefined,
raw: true raw: true
}); });
@ -357,15 +358,18 @@ export const reportTicksCountByStatusChatEnds = async (
throw new AppError("ERR_NO_PERMISSION", 403); throw new AppError("ERR_NO_PERMISSION", 403);
} }
const { startDate, endDate } = req.query as IndexQuery; const { startDate, endDate, userQueues } = req.query as IndexQuery;
const dateToday = splitDateTime( const dateToday = splitDateTime(
new Date(format(new Date(), "yyyy-MM-dd HH:mm:ss", { locale: ptBR })) new Date(format(new Date(), "yyyy-MM-dd HH:mm:ss", { locale: ptBR }))
); );
const queueIds = userQueues ? userQueues.map(queue => parseInt(queue)) : [];
const reportStatusChatEnd = await CountStatusChatEndService( const reportStatusChatEnd = await CountStatusChatEndService(
startDate || dateToday.fullDate, startDate || dateToday.fullDate,
endDate || dateToday.fullDate endDate || dateToday.fullDate,
queueIds
); );
return res.status(200).json({ reportStatusChatEnd }); return res.status(200).json({ reportStatusChatEnd });

View File

@ -409,9 +409,8 @@ export const show = async (req: Request, res: Response): Promise<Response> => {
export const count = async (req: Request, res: Response): Promise<Response> => { export const count = async (req: Request, res: Response): Promise<Response> => {
// type indexQ = { status: string; date?: string; }; // type indexQ = { status: string; date?: string; };
const { status, date } = req.query as IndexQuery; const { status, date, queueIds } = req.query as IndexQuery;
const ticketCount = await CountTicketService(status, date, queueIds);
const ticketCount = await CountTicketService(status, date);
return res.status(200).json(ticketCount); return res.status(200).json(ticketCount);
}; };

View File

@ -100,7 +100,7 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
// }; // };
export const all = async (req: Request, res: Response): Promise<Response> => { export const all = async (req: Request, res: Response): Promise<Response> => {
let { userId, profile }: any = req.query as IndexQuery; let { userId, profile, transferToOtherQueues }: any = req.query as IndexQuery;
console.log( console.log(
"userId: ", "userId: ",
@ -111,7 +111,7 @@ export const all = async (req: Request, res: Response): Promise<Response> => {
getSettingValue("queueTransferByWhatsappScope")?.value getSettingValue("queueTransferByWhatsappScope")?.value
); );
if (getSettingValue("queueTransferByWhatsappScope")?.value == "enabled") { if (getSettingValue("queueTransferByWhatsappScope")?.value == "enabled" && !transferToOtherQueues) {
if (!userId) return res.json({ users: [], queues: [] }); if (!userId) return res.json({ users: [], queues: [] });
const obj = await ListUserByWhatsappQueuesService( const obj = await ListUserByWhatsappQueuesService(
@ -145,7 +145,8 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
profile, profile,
positionCompany, positionCompany,
positionId, positionId,
queueIds queueIds,
transferToOtherQueues
} = req.body; } = req.body;
console.log("===========> req.url: ", req.url); console.log("===========> req.url: ", req.url);
@ -172,7 +173,8 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
positionCompany, positionCompany,
positionId, positionId,
profile, profile,
queueIds queueIds,
transferToOtherQueues
}); });
if (user) { if (user) {

View File

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

View File

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

View File

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

View File

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

View File

@ -51,6 +51,9 @@ class User extends Model<User> {
@Column @Column
secondaryId: string; secondaryId: string;
@Column
transferToOtherQueues: boolean;
@Default("admin") @Default("admin")
@Column @Column
profile: string; profile: string;

View File

@ -11,15 +11,29 @@ const { QueryTypes } = require("sequelize");
const CountStatusChatEndService = async ( const CountStatusChatEndService = async (
startDate: string, startDate: string,
endDate: string endDate: string,
queueIds?: number[]
) => { ) => {
let countStatusChatEnd: any
const countStatusChatEnd: any = await sequelize.query( if(queueIds && queueIds.length > 0){
countStatusChatEnd = await sequelize.query(
`select t.id, s.name, count(t.id) as count from Tickets t join StatusChatEnds s on
t.statusChatEndId = s.id and DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND t.queueId IN (${queueIds})
group by s.id;`,
{ type: QueryTypes.SELECT }
);
}
else{
countStatusChatEnd = await sequelize.query(
`select t.id, s.name, count(t.id) as count from Tickets t join StatusChatEnds s on `select t.id, s.name, count(t.id) as count from Tickets t join StatusChatEnds s on
t.statusChatEndId = s.id and DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999' t.statusChatEndId = s.id and DATE(t.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
group by s.id;`, group by s.id;`,
{ type: QueryTypes.SELECT } { type: QueryTypes.SELECT }
); );
}
return countStatusChatEnd; return countStatusChatEnd;
}; };

View File

@ -9,7 +9,7 @@ import ptBR from 'date-fns/locale/pt-BR';
import { splitDateTime } from "../../helpers/SplitDateTime"; import { splitDateTime } from "../../helpers/SplitDateTime";
const dateToday = splitDateTime(new Date(format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ptBR }))) const dateToday = splitDateTime(new Date(format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ptBR })))
const CountTicketService = async (status: string, date?: string): Promise<any> => { const CountTicketService = async (status: string, date?: string, queueIds?: string): Promise<any> => {
let where_clause = {} let where_clause = {}
@ -30,8 +30,8 @@ const CountTicketService = async (status: string, date?: string): Promise<any> =
// } // }
} }
if(queueIds) where_clause = { ...where_clause, status: status, queueId: { [Op.or]: [queueIds, null] } };
where_clause = { ...where_clause, status: status } else where_clause = { ...where_clause, status: status};
const ticket = await Ticket.findAll({ const ticket = await Ticket.findAll({
where: where_clause, where: where_clause,

View File

@ -257,7 +257,8 @@ const ListTicketsService = async ({
whereCondition = { whereCondition = {
createdAt: { createdAt: {
[Op.between]: [+startOfDay(parseISO(date)), +endOfDay(parseISO(date))] [Op.between]: [+startOfDay(parseISO(date)), +endOfDay(parseISO(date))]
} },
queueId: { [Op.or]: [queueIds, null] },
}; };
} }

View File

@ -98,6 +98,7 @@ const ShowTicketReport = async ({
"id", "id",
"status", "status",
"statusChatEnd", "statusChatEnd",
"isRemote",
[ [
Sequelize.fn( Sequelize.fn(
"DATE_FORMAT", "DATE_FORMAT",

View File

@ -14,6 +14,7 @@ interface Request {
queueIds?: number[]; queueIds?: number[];
profile?: string; profile?: string;
ignoreThrow?: boolean; ignoreThrow?: boolean;
transferToOtherQueues?: boolean;
} }
interface Response { interface Response {
@ -23,6 +24,7 @@ interface Response {
positionId: string; positionId: string;
id: number; id: number;
profile: string; profile: string;
transferToOtherQueues: boolean;
} }
const CreateUserService = async ({ const CreateUserService = async ({
@ -33,7 +35,8 @@ const CreateUserService = async ({
positionId, positionId,
queueIds = [], queueIds = [],
profile = "master", profile = "master",
ignoreThrow = false ignoreThrow = false,
transferToOtherQueues
}: Request): Promise<Response | any> => { }: Request): Promise<Response | any> => {
try { try {
const schema = Yup.object().shape({ const schema = Yup.object().shape({
@ -84,7 +87,8 @@ const CreateUserService = async ({
name, name,
positionCompany, positionCompany,
positionId: !positionId ? null : positionId, positionId: !positionId ? null : positionId,
profile profile,
transferToOtherQueues: transferToOtherQueues? transferToOtherQueues : false
}, },
{ include: ["queues"] } { include: ["queues"] }
); );

View File

@ -10,6 +10,7 @@ interface Request {
profiles?: Array<string>; profiles?: Array<string>;
raw?: boolean; raw?: boolean;
userIds?: string | number; userIds?: string | number;
userQueues?: string[];
} }
const ListUser = async ({ const ListUser = async ({
@ -17,10 +18,31 @@ const ListUser = async ({
userId, userId,
raw, raw,
userIds, userIds,
profiles profiles,
userQueues: userQueuesToNumber
}: Request): Promise<User[]> => { }: Request): Promise<User[]> => {
let where_clause = {}; let where_clause = {};
let userIdInQueues: number[] = [];
if(userQueuesToNumber !== undefined){
let userQueues = userQueuesToNumber.map(id => parseInt(id));
const userQueuesFiltered = await UserQueue.findAll({
where: { queueId: { [Op.or]: [userQueues, null] } },
order: [
['userId', 'ASC']
],
raw: true
});
if(userQueuesFiltered) for(let queueId of userQueues){
for(let userQueue of userQueuesFiltered){
if(queueId == userQueue.queueId){
const isAlready = userIdInQueues.indexOf(userQueue.userId);
if(isAlready === -1) userIdInQueues.push(userQueue.userId);
}
}
}
}
if (userId && profile) { if (userId && profile) {
where_clause = { where_clause = {
[Op.and]: [{ userId: userId }, { profile: profile }] [Op.and]: [{ userId: userId }, { profile: profile }]
@ -37,16 +59,23 @@ const ListUser = async ({
where_clause = { where_clause = {
id: { [Op.in]: userIds } id: { [Op.in]: userIds }
}; };
} else if (profiles) { }
else if (profiles && userIdInQueues.length > 0) {
where_clause = { where_clause = {
profile: { [Op.in]: profiles } profile: { [Op.in]: profiles },
id: {[Op.in]: userIdInQueues}
};
}
else if (profiles) {
where_clause = {
profile: { [Op.in]: profiles },
}; };
} }
const users = await User.findAll({ const users = await User.findAll({
where: where_clause, where: where_clause,
raw, raw,
attributes: ["id", "name", "email", "positionCompany"], attributes: ["id", "name", "email", "positionCompany", "transferToOtherQueues"],
include: [ include: [
{ model: Queue, as: "queues", attributes: ["id", "name", "color"] } { model: Queue, as: "queues", attributes: ["id", "name", "color"] }

View File

@ -66,7 +66,8 @@ const ListUsersService = async ({
"email", "email",
"positionCompany", "positionCompany",
"profile", "profile",
"createdAt" "createdAt",
"transferToOtherQueues"
], ],
limit, limit,
offset, offset,

View File

@ -12,7 +12,8 @@ const ShowUserService = async (id: string | number): Promise<User> => {
"profile", "profile",
"positionCompany", "positionCompany",
"positionId", "positionId",
"tokenVersion" "tokenVersion",
"transferToOtherQueues"
], ],
include: [ include: [
{ model: Queue, as: "queues", attributes: ["id", "name", "color"] }, { model: Queue, as: "queues", attributes: ["id", "name", "color"] },

View File

@ -12,6 +12,7 @@ interface UserData {
positionId?: string; positionId?: string;
profile?: string; profile?: string;
queueIds?: number[]; queueIds?: number[];
transferToOtherQueues?: boolean;
} }
interface Request { interface Request {
@ -75,7 +76,8 @@ const UpdateUserService = async ({
name, name,
positionCompany, positionCompany,
positionId, positionId,
queueIds = [] queueIds = [],
transferToOtherQueues
} = userData; } = userData;
try { try {
@ -90,7 +92,8 @@ const UpdateUserService = async ({
profile, profile,
positionCompany, positionCompany,
positionId: !positionId ? null : positionId, positionId: !positionId ? null : positionId,
name name,
transferToOtherQueues
}); });
await user.$set("queues", queueIds); await user.$set("queues", queueIds);
@ -117,7 +120,8 @@ const UpdateUserService = async ({
profile: _user.profile, profile: _user.profile,
queues: _user.queues, queues: _user.queues,
positionId: _user?.positionId, positionId: _user?.positionId,
position: _user.position position: _user.position,
transferToOtherQueues: _user.transferToOtherQueues
}; };
return serializedUser; return serializedUser;

View File

@ -170,7 +170,7 @@ const verifyMediaMessage = async (
if (!media) { if (!media) {
throw new Error("ERR_WAPP_DOWNLOAD_MEDIA"); throw new Error("ERR_WAPP_DOWNLOAD_MEDIA");
} }
let mediaAuthorized = true;
let messageData = { let messageData = {
id: msg.id.id, id: msg.id.id,
ticketId: ticket.id, ticketId: ticket.id,
@ -184,7 +184,9 @@ const verifyMediaMessage = async (
phoneNumberId: msg?.phoneNumberId, phoneNumberId: msg?.phoneNumberId,
fromAgent: false fromAgent: false
}; };
if(messageData.mediaType === 'video' || messageData.mediaType === 'audio' && getSettingValue('blockAudioVideoMedia')?.value === 'enabled'){
mediaAuthorized = false;
}
if (msg?.fromMe) { if (msg?.fromMe) {
messageData = { ...messageData, fromAgent: true }; messageData = { ...messageData, fromAgent: true };
} }
@ -199,23 +201,36 @@ const verifyMediaMessage = async (
body: media.filename body: media.filename
}; };
} }
if(mediaAuthorized){
try { try {
await writeFileAsync( await writeFileAsync(
join(__dirname, "..", "..", "..", "..", "..", "public", media.filename), join(__dirname, "..", "..", "..", "..", "..", "public", media.filename),
media.data, media.data,
"base64" "base64"
); );
} catch (err) { } catch (err) {
Sentry.captureException(err); Sentry.captureException(err);
logger.error(`There was an error: wbotMessageLitener.ts: ${err}`); logger.error(`There was an error: wbotMessageLitener.ts: ${err}`);
}
} }
} }
if(mediaAuthorized){
await ticket.update({ lastMessage: msg.body || media.filename }); await ticket.update({ lastMessage: msg.body || media.filename });
const newMessage = await CreateMessageService({ messageData }); const newMessage = await CreateMessageService({ messageData });
return newMessage;
return newMessage; }else{
if (ticket.status !== "queueChoice") {
botSendMessage(
ticket,
`Atenção! Mensagem ignorada, tipo de mídia não suportado.`
);
}
messageData.body = `Mensagem de *${messageData.mediaType}* ignorada, tipo de mídia não suportado.`;
messageData.mediaUrl = '';
await ticket.update({ lastMessage: `Mensagem de *${messageData.mediaType}* ignorada, tipo de mídia não suportado.`});
const newMessage = await CreateMessageService({ messageData });
console.log(`--------->>> Mensagem do tipo: ${messageData.mediaType}, ignorada!`)
}
}; };
// const verifyMediaMessage = async ( // const verifyMediaMessage = async (
@ -413,13 +428,15 @@ const verifyQueue = async (
} }
let body = ""; let body = "";
const io = getIO();
if (botOptions.length > 0) { if (botOptions.length > 0) {
body = `\u200e${choosenQueue.greetingMessage}\n\n${botOptions}\n${final_message.msg}`; body = `\u200e${choosenQueue.greetingMessage}\n\n${botOptions}\n${final_message.msg}`;
} else { } else {
body = `\u200e${choosenQueue.greetingMessage}`; body = `\u200e${choosenQueue.greetingMessage}`;
} }
io.emit('notifyPeding', {data: {ticket, queue: choosenQueue}});
sendWhatsAppMessageSocket(ticket, body); sendWhatsAppMessageSocket(ticket, body);
} else { } else {
//test del transfere o atendimento se entrar na ura infinita //test del transfere o atendimento se entrar na ura infinita

View File

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

View File

@ -10,11 +10,19 @@ const SelectTextFields = (props) => {
if (!props.textBoxFieldSelected) { if (!props.textBoxFieldSelected) {
props.currencies.push({ 'value': 0, 'label': '' }) props.currencies.push({ 'value': 0, 'label': '' })
} }
if(props.textBoxFieldSelected === 'All'){
const already = props.currencies.findIndex(obj => obj.value === 'All');
if (already === -1) {
props.currencies.push({ 'value': 'All', 'label': 'All' })
}
}
useEffect(() => { useEffect(() => {
props.func(currency) props.func(currency)
}, [currency, props]) }, [currency, props.textBoxFieldSelected])
const handleChange = (event) => { const handleChange = (event) => {

View File

@ -16,6 +16,7 @@ import { AuthContext } from "../../context/Auth/AuthContext"
import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket" import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket"
import { Divider } from "@material-ui/core" import { Divider } from "@material-ui/core"
import { ticketsContext } from "../../context/TicketsProvider/TicketsProvider"
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
ticketsListWrapper: { ticketsListWrapper: {
@ -193,6 +194,8 @@ const TicketsList = (props) => {
useEffect(() => { useEffect(() => {
setSettings(setting) setSettings(setting)
}, [setting]) }, [setting])
const { setTickets } = useContext(ticketsContext)
useEffect(() => { useEffect(() => {
@ -346,6 +349,12 @@ const TicketsList = (props) => {
if (typeof updateCount === "function") { if (typeof updateCount === "function") {
updateCount(ticketsList.length) updateCount(ticketsList.length)
} }
if (ticketsList && status === "pending"){
setTickets(ticketsList)
}
// else{
// setTickets([])
// }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ticketsList]) }, [ticketsList])

View File

@ -1,38 +1,45 @@
import React, { useContext, useEffect, useRef, useState } from "react"; import React, { useContext, useEffect, useRef, useState } from "react"
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles"
import { IconButton } from "@mui/material"; import { IconButton } from "@mui/material"
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper"
import InputBase from "@material-ui/core/InputBase"; import InputBase from "@material-ui/core/InputBase"
import Tabs from "@material-ui/core/Tabs"; import Tabs from "@material-ui/core/Tabs"
import Tab from "@material-ui/core/Tab"; import Tab from "@material-ui/core/Tab"
import Badge from "@material-ui/core/Badge"; import Badge from "@material-ui/core/Badge"
import Tooltip from "@material-ui/core/Tooltip"; import Tooltip from "@material-ui/core/Tooltip"
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search"
import MoveToInboxIcon from "@material-ui/icons/MoveToInbox"; import MoveToInboxIcon from "@material-ui/icons/MoveToInbox"
import CheckBoxIcon from "@material-ui/icons/CheckBox"; import CheckBoxIcon from "@material-ui/icons/CheckBox"
import MenuIcon from "@material-ui/icons/Menu"; import MenuIcon from "@material-ui/icons/Menu"
import FindInPageIcon from '@material-ui/icons/FindInPage'; import FindInPageIcon from '@material-ui/icons/FindInPage'
import FormControlLabel from "@material-ui/core/FormControlLabel"; import FormControlLabel from "@material-ui/core/FormControlLabel"
import Switch from "@material-ui/core/Switch"; import Switch from "@material-ui/core/Switch"
import openSocket from "socket.io-client"
import NewTicketModal from "../NewTicketModal"; import NewTicketModal from "../NewTicketModal"
import TicketsList from "../TicketsList"; import TicketsList from "../TicketsList"
import TabPanel from "../TabPanel"; import TabPanel from "../TabPanel"
import { i18n } from "../../translate/i18n"; import { i18n } from "../../translate/i18n"
import { AuthContext } from "../../context/Auth/AuthContext"; import { AuthContext } from "../../context/Auth/AuthContext"
import { Can } from "../Can"; import { Can } from "../Can"
import TicketsQueueSelect from "../TicketsQueueSelect"; import TicketsQueueSelect from "../TicketsQueueSelect"
import { Button } from "@material-ui/core"; import { Button } from "@material-ui/core"
import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption"; import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption"
import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket"
import useTickets from "../../hooks/useTickets"
import api from "../../services/api"
import toastError from "../../errors/toastError"
import { ticketsContext } from "../../context/TicketsProvider/TicketsProvider"
import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
ticketsWrapper: { ticketsWrapper: {
@ -124,59 +131,106 @@ const useStyles = makeStyles((theme) => ({
hide: { hide: {
display: "none !important", display: "none !important",
}, },
})); }))
const DEFAULT_SEARCH_PARAM = { searchParam: "", searchParamContent: "" } const DEFAULT_SEARCH_PARAM = { searchParam: "", searchParamContent: "" }
const TicketsManager = () => { const TicketsManager = () => {
const { tabOption, setTabOption } = useContext(TabTicketContext); const { tabOption, setTabOption } = useContext(TabTicketContext)
const { setSearchTicket } = useContext(SearchTicketContext) const { setSearchTicket } = useContext(SearchTicketContext)
const classes = useStyles(); const classes = useStyles()
const [searchParam, setSearchParam] = useState(DEFAULT_SEARCH_PARAM); const [searchParam, setSearchParam] = useState(DEFAULT_SEARCH_PARAM)
const [tab, setTab] = useState("open"); const [tab, setTab] = useState("open")
const [tabOpen, setTabOpen] = useState("open"); const [tabOpen, setTabOpen] = useState("open")
const [newTicketModalOpen, setNewTicketModalOpen] = useState(false); const [newTicketModalOpen, setNewTicketModalOpen] = useState(false)
const [showAllTickets, setShowAllTickets] = useState(false); const [showAllTickets, setShowAllTickets] = useState(false)
const { user } = useContext(AuthContext); const { user, setting, getSettingValue } = useContext(AuthContext)
const [openCount, setOpenCount] = useState(0); const [openCount, setOpenCount] = useState(0)
const [pendingCount, setPendingCount] = useState(0); const [pendingCount, setPendingCount] = useState(0)
const userQueueIds = user.queues.map((q) => q.id); const userQueueIds = user.queues.map((q) => q.id)
const [selectedQueueIds, setSelectedQueueIds] = useState(userQueueIds || []); const [selectedQueueIds, setSelectedQueueIds] = useState(userQueueIds || [])
const [showContentSearch, setShowContentSearch] = useState(false) const [showContentSearch, setShowContentSearch] = useState(false)
const searchInputRef = useRef(); const searchInputRef = useRef()
const searchContentInputRef = useRef(); const searchContentInputRef = useRef()
const [inputSearch, setInputSearch] = useState(''); const [inputSearch, setInputSearch] = useState('')
const [inputContentSearch, setInputContentSearch] = useState("") const [inputContentSearch, setInputContentSearch] = useState("")
const [openTooltipSearch, setOpenTooltipSearch] = useState(false) const [openTooltipSearch, setOpenTooltipSearch] = useState(false)
let searchTimeout; const [waitingTime, setWaitingTime] = useState('00:00')
let searchContentTimeout; // const [tickets, setTickets] = useState([]);
const [settings, setSettings] = useState([])
let searchTimeout
let searchContentTimeout
const { tickets, } = useContext(ticketsContext)
useEffect(() => {
setSettings(setting)
}, [setting])
useEffect(() => { useEffect(() => {
if (user.profile.toUpperCase() === "ADMIN" || if (user.profile.toUpperCase() === "ADMIN" ||
user.profile.toUpperCase() === "SUPERVISOR" || user.profile.toUpperCase() === "SUPERVISOR" ||
user.profile.toUpperCase() === "MASTER") { user.profile.toUpperCase() === "MASTER") {
setShowAllTickets(true); setShowAllTickets(true)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [])
useEffect(() => { useEffect(() => {
if (tab === "search") { if (tab === "search") {
searchInputRef.current.focus(); searchInputRef.current.focus()
} }
setTabOption(tab) setTabOption(tab)
}, [tab, setTabOption]); }, [tab, setTabOption])
useEffect(() => {
if (settings?.length > 0 && getSettingValue('waitingTimeTickets') !== 'enabled') return
const calculateAverageTime = () => {
if (tickets.length > 0) {
const now = new Date()
const differenceTime = tickets?.map(ticket => {
const createdAt = new Date(ticket.createdAt)
const difference = now - createdAt
return difference
})
const sumDifferences = differenceTime.reduce((total, difference) => total + difference, 0)
const averageTimeMilliseconds = sumDifferences / tickets?.length
let hours = Math.floor(averageTimeMilliseconds / 3600000)
const minutes = Math.floor((averageTimeMilliseconds % 3600000) / 60000)
let days = hours >= 24 ? parseInt(hours / 24) : ''
if (days != '') hours = hours - (24 * days)
const averageTimeFormated = `${days != '' ? `${days}d ` : days}${hours.toString().padStart(2, '0')}h${minutes.toString().padStart(2, '0')}`
return averageTimeFormated
} else return '00:00'
}
setWaitingTime(calculateAverageTime())
const intervalId = setInterval(() => {
setWaitingTime(calculateAverageTime())
}, 10000)
return () => clearInterval(intervalId)
}, [tickets])
useEffect(() => { useEffect(() => {
@ -194,24 +248,24 @@ const TicketsManager = () => {
// }, 500); // }, 500);
clearTimeout(searchContentTimeout); clearTimeout(searchContentTimeout)
setSearchParam(prev => ({ ...prev, searchParamContent: "" })) setSearchParam(prev => ({ ...prev, searchParamContent: "" }))
}, [inputContentSearch, searchContentTimeout]); }, [inputContentSearch, searchContentTimeout])
useEffect(() => { useEffect(() => {
//console.log(selectedQueueIds);
if (tabOption === 'open') { if (tabOption === 'open') {
setTabOption('') setTabOption('')
setSearchParam(DEFAULT_SEARCH_PARAM); setSearchParam(DEFAULT_SEARCH_PARAM)
setInputSearch(''); setInputSearch('')
setInputContentSearch('') setInputContentSearch('')
setTab("open"); setTab("open")
return; return
} }
}, [tabOption, setTabOption]) }, [tabOption, setTabOption])
@ -231,14 +285,14 @@ const TicketsManager = () => {
setSearchTicket(searchParam.searchParam) setSearchTicket(searchParam.searchParam)
clearTimeout(searchTimeout); clearTimeout(searchTimeout)
if (searchedTerm === "") { if (searchedTerm === "") {
setSearchParam(prev => ({ ...prev, searchParam: searchedTerm })) setSearchParam(prev => ({ ...prev, searchParam: searchedTerm }))
setInputSearch(searchedTerm) setInputSearch(searchedTerm)
setShowContentSearch(false) setShowContentSearch(false)
setTab("open"); setTab("open")
return; return
} }
if (searchedTerm.length < 4) { if (searchedTerm.length < 4) {
@ -249,10 +303,10 @@ const TicketsManager = () => {
searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
setSearchParam(prev => ({ ...prev, searchParam: searchedTerm })); setSearchParam(prev => ({ ...prev, searchParam: searchedTerm }))
}, 500); }, 500)
}; }
const handleContentSearch = e => { const handleContentSearch = e => {
@ -262,9 +316,9 @@ const TicketsManager = () => {
searchContentTimeout = setTimeout(() => { searchContentTimeout = setTimeout(() => {
setSearchParam(prev => ({ ...prev, searchParamContent: searchedContentText })); setSearchParam(prev => ({ ...prev, searchParamContent: searchedContentText }))
}, 500); }, 500)
} }
@ -282,18 +336,18 @@ const TicketsManager = () => {
} }
const handleChangeTab = (e, newValue) => { const handleChangeTab = (e, newValue) => {
setTab(newValue); setTab(newValue)
}; }
const handleChangeTabOpen = (e, newValue) => { const handleChangeTabOpen = (e, newValue) => {
setTabOpen(newValue); setTabOpen(newValue)
}; }
const applyPanelStyle = (status) => { const applyPanelStyle = (status) => {
if (tabOpen !== status) { if (tabOpen !== status) {
return { width: 0, height: 0 }; return { width: 0, height: 0 }
} }
}; }
return ( return (
<Paper elevation={0} variant="outlined" className={classes.ticketsWrapper}> <Paper elevation={0} variant="outlined" className={classes.ticketsWrapper}>
@ -448,7 +502,25 @@ const TicketsManager = () => {
</Badge> </Badge>
} }
value={"pending"} value={"pending"}
/> />{
(settings?.length > 0 && getSettingValue('waitingTimeTickets') === 'enabled') &&
<Tooltip
arrow
placement="right"
title={"Tempo de espera aguardando"}
>
<span style={{ display: 'flex', alignItems: 'center', flexDirection: 'column', justifyContent: 'flex-start', marginRight: '20px', marginTop: '10px' }}>
{/* <label style={{ color: 'red', fontWeight: 'bold', padding: '.1rem', fontSize: '8px', textAlign: 'center', margin: '0' }}>
<i>ESPERA</i>
</label> */}
<label style={{ color: 'gray', fontWeight: 'bold', padding: '5px'/*, textDecoration: 'underline'*/, fontSize: '13px' }}>
{waitingTime}
</label>
</span>
</Tooltip>
}
</Tabs> </Tabs>
<Paper className={classes.ticketsWrapper}> <Paper className={classes.ticketsWrapper}>
<TicketsList <TicketsList
@ -486,7 +558,7 @@ const TicketsManager = () => {
</TabPanel> </TabPanel>
</Paper> </Paper>
); )
}; }
export default TicketsManager; export default TicketsManager

View File

@ -97,7 +97,7 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => {
} }
else { else {
if (settings?.find(e => e?.key === 'queueTransferByWhatsappScope')?.value === 'enabled') { if (settings?.find(e => e?.key === 'queueTransferByWhatsappScope')?.value === 'enabled' && !user.transferToOtherQueues) {
setQueues(_queues) setQueues(_queues)
} }
else { else {
@ -190,7 +190,7 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => {
try { try {
if (settings?.find(e => e?.key === 'queueTransferByWhatsappScope')?.value === 'enabled') { if (settings?.find(e => e?.key === 'queueTransferByWhatsappScope')?.value === 'enabled' && !user.transferToOtherQueues) {
const { data } = await api.get(`/users/all`, { const { data } = await api.get(`/users/all`, {
params: { userId: user.id }, params: { userId: user.id },
}) })
@ -202,7 +202,7 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => {
else { else {
const { data } = await api.get(`/users/all`, { const { data } = await api.get(`/users/all`, {
params: { profile: 'user' }, params: { profile: 'user', transferToOtherQueues: user.transferToOtherQueues },
}) })
setUsers(data.users) setUsers(data.users)

View File

@ -32,6 +32,7 @@ import toastError from "../../errors/toastError"
import QueueSelect from "../QueueSelect" import QueueSelect from "../QueueSelect"
import { AuthContext } from "../../context/Auth/AuthContext" import { AuthContext } from "../../context/Auth/AuthContext"
import { Can } from "../Can" import { Can } from "../Can"
import Switch from '@mui/material/Switch'
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
root: { root: {
@ -95,6 +96,7 @@ const UserModal = ({ open, onClose, userId, }) => {
const [showPassword, setShowPassword] = useState(false) const [showPassword, setShowPassword] = useState(false)
const [positions, setPositions] = useState([]) const [positions, setPositions] = useState([])
const [selectedPosition, setSelectedPosition] = useState('') const [selectedPosition, setSelectedPosition] = useState('')
const [checked, setChecked] = useState(false)
useEffect(() => { useEffect(() => {
const fetchUser = async () => { const fetchUser = async () => {
@ -112,6 +114,9 @@ const UserModal = ({ open, onClose, userId, }) => {
setSelectedPosition(data.positionId) setSelectedPosition(data.positionId)
else else
setSelectedPosition('') setSelectedPosition('')
if(data.transferToOtherQueues) setChecked(data.transferToOtherQueues);
} catch (err) { } catch (err) {
toastError(err) toastError(err)
} }
@ -136,10 +141,15 @@ const UserModal = ({ open, onClose, userId, }) => {
const handleClose = () => { const handleClose = () => {
onClose() onClose()
setUser(initialState) setUser(initialState)
setChecked(false);
}
const handleChange = (event) => {
setChecked(event.target.checked)
} }
const handleSaveUser = async values => { const handleSaveUser = async values => {
const userData = { ...values, queueIds: selectedQueueIds, positionId: selectedPosition } const userData = { ...values, queueIds: selectedQueueIds, positionId: selectedPosition, transferToOtherQueues: checked}
try { try {
if (userId) { if (userId) {
@ -252,7 +262,7 @@ const UserModal = ({ open, onClose, userId, }) => {
fullWidth fullWidth
/> />
<div className={classes.multFieldLine}> <div className={classes.multFieldLine}>
<Field {/* <Field
as={TextField} as={TextField}
label="Cargo" label="Cargo"
name="positionCompany" name="positionCompany"
@ -261,7 +271,17 @@ const UserModal = ({ open, onClose, userId, }) => {
variant="outlined" variant="outlined"
margin="dense" margin="dense"
fullWidth fullWidth
/> /> */}
<label style={{display: 'flex', alignItems:'center'}}>
Transferir para outras filas
<Switch
name= 'transferToOtherQueues'
checked={checked}
onChange={handleChange}
inputProps={{ 'aria-label': 'controlled' }}
/>
</label>
<FormControl <FormControl
variant="outlined" variant="outlined"
className={classes.formControl} className={classes.formControl}

View File

@ -0,0 +1,17 @@
import React, { useState, createContext } from "react"
const ticketsContext = createContext()
const TicketsProvider = ({ children }) => {
const [tickets, setTickets] = useState(0)
return (
<ticketsContext.Provider value={{ tickets, setTickets }}>
{children}
</ticketsContext.Provider>
)
}
export { ticketsContext, TicketsProvider }

View File

@ -20,9 +20,10 @@ const Chart = (props) => {
const theme = useTheme(); const theme = useTheme();
const date = useRef(new Date().toISOString()); const date = useRef(new Date().toISOString());
let { tickets } = useTickets({ date: date.current, unlimited: "current" }); const queueIds = JSON.stringify( props.selectedQueue) || {};
let {tickets} = useTickets({ date: date.current, unlimited: "current", queueIds });
const [chartData, setChartData] = useState([ const modelChar = [
{ time: "08:00", amount: 0 }, { time: "08:00", amount: 0 },
{ time: "09:00", amount: 0 }, { time: "09:00", amount: 0 },
{ time: "10:00", amount: 0 }, { time: "10:00", amount: 0 },
@ -35,11 +36,12 @@ const Chart = (props) => {
{ time: "17:00", amount: 0 }, { time: "17:00", amount: 0 },
{ time: "18:00", amount: 0 }, { time: "18:00", amount: 0 },
{ time: "19:00", amount: 0 }, { time: "19:00", amount: 0 },
]); ]
const [chartData, setChartData] = useState(modelChar);
useEffect(() => { useEffect(() => {
setChartData(prevState => { setChartData(prevState => {
let aux = [...prevState]; let aux = modelChar;
aux.forEach(a => { aux.forEach(a => {
tickets.forEach(ticket => { format(startOfHour(parseISO(ticket.createdAt)), "HH:mm") === a.time && a.amount++; }); tickets.forEach(ticket => { format(startOfHour(parseISO(ticket.createdAt)), "HH:mm") === a.time && a.amount++; });

View File

@ -1,4 +1,4 @@
import React, { useContext, useReducer, useEffect, useState } from "react" import React, { useContext, useReducer, useEffect, useState, useCallback } from "react"
import { addHours, addMinutes, addSeconds, intervalToDuration } from "date-fns" import { addHours, addMinutes, addSeconds, intervalToDuration } from "date-fns"
@ -11,6 +11,7 @@ import Tooltip from "@mui/material/Tooltip"
import Zoom from "@mui/material/Zoom" import Zoom from "@mui/material/Zoom"
import IconButton from "@mui/material/IconButton" import IconButton from "@mui/material/IconButton"
import Info from "@material-ui/icons/Info" import Info from "@material-ui/icons/Info"
import SelectField from "../../components/Report/SelectField"
import { AuthContext } from "../../context/Auth/AuthContext" import { AuthContext } from "../../context/Auth/AuthContext"
// import { i18n } from "../../translate/i18n"; // import { i18n } from "../../translate/i18n";
@ -254,12 +255,15 @@ const reducer = (state, action) => {
} }
const Dashboard = () => { const Dashboard = () => {
const { user } = useContext(AuthContext)
const classes = useStyles() const classes = useStyles()
const [usersOnlineInfo, dispatch] = useReducer(reducer, []) const [usersOnlineInfo, dispatch] = useReducer(reducer, [])
const [ticketStatusChange, setStatus] = useState() const [ticketStatusChange, setStatus] = useState()
const [ticketsStatus, setTicktsStatus] = useState({ open: 0, openAll: 0, pending: 0, closed: 0 }) const [ticketsStatus, setTicktsStatus] = useState({ open: 0, openAll: 0, pending: 0, closed: 0 })
const [ticketStatusChatEnd, setTicketStatusChatEnd] = useState([]) const [ticketStatusChatEnd, setTicketStatusChatEnd] = useState([])
const { user } = useContext(AuthContext)
const userQueueIds = user.queues?.map((q) => q.id);
const [selectedQueue, setSelectedQueue] = useState(userQueueIds || []);
useEffect(() => { useEffect(() => {
dispatch({ type: "RESET" }) dispatch({ type: "RESET" })
@ -286,14 +290,14 @@ const Dashboard = () => {
let dateToday = `${date[2]}-${date[1]}-${date[0]}` let dateToday = `${date[2]}-${date[1]}-${date[0]}`
const { data } = await api.get("/reports/user/services", { const { data } = await api.get("/reports/user/services", {
params: { userId: null, startDate: dateToday, endDate: dateToday }, params: { userId: null, startDate: dateToday, endDate: dateToday, userQueues: selectedQueue },
}) })
dispatch({ type: "RESET" }) dispatch({ type: "RESET" })
dispatch({ type: "LOAD_QUERY", payload: data.usersProfile }) dispatch({ type: "LOAD_QUERY", payload: data.usersProfile })
const { data: ticketStatusChatEndData } = await api.get("/reports/count/statusChatEnd", { const { data: ticketStatusChatEndData } = await api.get("/reports/count/statusChatEnd", {
params: { startDate: dateToday, endDate: dateToday }, params: { startDate: dateToday, endDate: dateToday, userQueues: selectedQueue },
}) })
setTicketStatusChatEnd(ticketStatusChatEndData.reportStatusChatEnd) setTicketStatusChatEnd(ticketStatusChatEndData.reportStatusChatEnd)
@ -306,7 +310,7 @@ const Dashboard = () => {
fetchQueries() fetchQueries()
}, 500) }, 500)
return () => clearTimeout(delayDebounceFn) return () => clearTimeout(delayDebounceFn)
}, []) }, [selectedQueue])
useEffect(() => { useEffect(() => {
@ -381,6 +385,18 @@ const Dashboard = () => {
} }
}, []) }, [])
const handleSelectedQueue = useCallback((queueSelected) => {
if(queueSelected !== 'All'){
const queueIndex = user?.queues?.findIndex((q) => q.id === parseInt(queueSelected));
const queueIds = []
queueIds.push(user?.queues[queueIndex]?.id);
setSelectedQueue(queueIds);
}else{
const queueIds = user?.queues?.map((queue) => queue.id);
setSelectedQueue(queueIds);
}
},[user, setSelectedQueue])
useEffect(() => { useEffect(() => {
if (ticketStatusChange === "") return if (ticketStatusChange === "") return
const delayDebounceFn = setTimeout(() => { const delayDebounceFn = setTimeout(() => {
@ -390,17 +406,17 @@ const Dashboard = () => {
let dateToday = `${date[2]}-${date[1]}-${date[0]}` let dateToday = `${date[2]}-${date[1]}-${date[0]}`
const _open = await api.get("/tickets/count", { const _open = await api.get("/tickets/count", {
params: { status: "open", date: dateToday }, params: { status: "open", date: dateToday, queueIds: selectedQueue },
}) })
const _closed = await api.get("/tickets/count", { const _closed = await api.get("/tickets/count", {
params: { status: "closed", date: dateToday }, params: { status: "closed", date: dateToday, queueIds: selectedQueue },
}) })
const _pending = await api.get("/tickets/count", { const _pending = await api.get("/tickets/count", {
params: { status: "pending" }, params: { status: "pending", queueIds: selectedQueue },
}) })
const _openAll = await api.get("/tickets/count", { const _openAll = await api.get("/tickets/count", {
params: { status: "open" }, params: { status: "open", queueIds: selectedQueue },
}) })
setTicktsStatus({ setTicktsStatus({
open: _open.data.count, open: _open.data.count,
@ -419,7 +435,7 @@ const Dashboard = () => {
fetchQueries() fetchQueries()
}, 500) }, 500)
return () => clearTimeout(delayDebounceFn) return () => clearTimeout(delayDebounceFn)
}, [ticketStatusChange]) }, [ticketStatusChange, selectedQueue])
return ( return (
<Can <Can
@ -451,6 +467,16 @@ const Dashboard = () => {
</Tooltip> </Tooltip>
</Typography> </Typography>
</Grid> </Grid>
<Grid style={{ display: 'flex', flexDirection: 'column', padding: '10px 0', alignItems: 'start' }}>
<SelectField
func={handleSelectedQueue}
textBoxFieldSelected={'All'}
emptyField={false}
header={'Filas'}
currencies={user.queues.map((obj) => {
return { 'value': obj.id, 'label': obj.name }
})} />
</Grid>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12} sm={6} md={6} lg={4}> <Grid item xs={12} sm={6} md={6} lg={4}>
<Paper <Paper
@ -506,7 +532,7 @@ const Dashboard = () => {
<Grid item container spacing={3}> <Grid item container spacing={3}>
<Grid item xs={12} sm={12} md={6} lg={6}> <Grid item xs={12} sm={12} md={6} lg={6}>
<Paper className={classes.fixedHeightPaper} variant="outlined"> <Paper className={classes.fixedHeightPaper} variant="outlined">
<Chart allTickets={usersOnlineInfo} /> <Chart allTickets={usersOnlineInfo} selectedQueue = {selectedQueue}/>
</Paper> </Paper>
</Grid> </Grid>
<Grid item xs={12} sm={12} md={6} lg={6}> <Grid item xs={12} sm={12} md={6} lg={6}>

View File

@ -224,6 +224,7 @@ Item.propTypes = {
let columnsData = [ let columnsData = [
{ title: `Tipo`, field: 'isRemote' },
{ title: `${i18n.t("reports.listColumns.column1_1")}`, field: 'whatsapp.name' }, { title: `${i18n.t("reports.listColumns.column1_1")}`, field: 'whatsapp.name' },
{ title: `${i18n.t("reports.listColumns.column1_2")}`, field: 'user.name' }, { title: `${i18n.t("reports.listColumns.column1_2")}`, field: 'user.name' },
{ title: `${i18n.t("reports.listColumns.column0_4")}`, field: 'contact.number' }, { title: `${i18n.t("reports.listColumns.column0_4")}`, field: 'contact.number' },
@ -237,9 +238,11 @@ let columnsData = [
{ title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' }, { title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' },
{ title: `Espera`, field: 'waiting_time' }, { title: `Espera`, field: 'waiting_time' },
{ title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true }, { title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true },
{ title: `Link`, field: 'link', searchable: false, hidden: true, export: true },
] ]
let columnsDataSuper = [ let columnsDataSuper = [
{ title: `Tipo`, field: 'isRemote' },
{ title: `${i18n.t("reports.listColumns.column1_1")}`, field: 'whatsapp.name' }, { title: `${i18n.t("reports.listColumns.column1_1")}`, field: 'whatsapp.name' },
{ title: `${i18n.t("reports.listColumns.column1_2")}`, field: 'user.name' }, { title: `${i18n.t("reports.listColumns.column1_2")}`, field: 'user.name' },
{ title: `${i18n.t("reports.listColumns.column0_3")}`, field: 'contact.name' }, { title: `${i18n.t("reports.listColumns.column0_3")}`, field: 'contact.name' },
@ -252,6 +255,7 @@ let columnsDataSuper = [
{ title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' }, { title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' },
{ title: `Espera`, field: 'waiting_time' }, { title: `Espera`, field: 'waiting_time' },
{ title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true }, { title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true },
{ title: `Link`, field: 'link', searchable: false, hidden: true, export: true },
] ]
@ -374,12 +378,15 @@ const Report = () => {
filterQueuesTickets = ticketsQueue.filter(ticket => ticket?.queue?.name === userQueues[0]?.name) filterQueuesTickets = ticketsQueue.filter(ticket => ticket?.queue?.name === userQueues[0]?.name)
} }
data.tickets = filterQueuesTickets data.tickets = filterQueuesTickets
const tickets = data.tickets.map(ticket => ({ const tickets = data.tickets.map(ticket => {
ticket.isRemote = ticket.isRemote ? 'Remoto' : 'Comum';
return ({
...ticket, ...ticket,
messagesToFilter: ticket.messages.map(message => message.body).join(' '), messagesToFilter: ticket.messages.map(message => message.body).join(' '),
})) link: `${process.env.REACT_APP_FRONTEND_URL}/tickets/${ticket.id}`
})
})
dispatchQ({ type: "LOAD_QUERY", payload: tickets }) dispatchQ({ type: "LOAD_QUERY", payload: tickets })
// console.log(tickets)
setHasMore(data.hasMore) setHasMore(data.hasMore)
setTotalCountTickets(data.count) setTotalCountTickets(data.count)
setLoading(false) setLoading(false)
@ -680,54 +687,56 @@ const Report = () => {
const renderSwitch = (param) => { const renderSwitch = (param) => {
switch (param) { if(userA.profile !== 'supervisor'){
case 'empty': switch (param) {
return ( case 'empty':
<> return (
{query && query.length > 0 && <>
<ReportModalType currencies={reportTypeList} func={reportTypeValue} reportOption={reportType} /> {query && query.length > 0 &&
} <ReportModalType currencies={reportTypeList} func={reportTypeValue} reportOption={reportType} />
{/* <Button }
disabled={query && query.length > 0 ? false : true} {/* <Button
variant="contained" disabled={query && query.length > 0 ? false : true}
color="primary" variant="contained"
onClick={(e) => { color="primary"
handleCSVMessages() onClick={(e) => {
}} handleCSVMessages()
> }}
{"CSV ALL"} >
{"CSV ALL"}
</Button> */} </Button> */}
</>) </>)
case 'pending' || 'processing': case 'pending' || 'processing':
return ( return (
<> <>
<span>PROCESSING...</span> <span>PROCESSING...</span>
</>) </>)
case 'success': case 'success':
return ( return (
<> <>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={(e) => { onClick={(e) => {
handleCSVDownload(e) handleCSVDownload(e)
}} }}
> >
{'CSV DOWNLOAD'} {'CSV DOWNLOAD'}
</Button> </Button>
</>) </>)
case 'downloading': case 'downloading':
return ( return (
<> <>
<span>DOWNLOADING...</span> <span>DOWNLOADING...</span>
</>) </>)
default: default:
return (<><span>WAITING...</span></>) return (<><span>WAITING...</span></>)
}
} }
} }
@ -871,7 +880,7 @@ const Report = () => {
<> <>
<MTable data={query} <MTable data={query}
columns={userA.profile !== 'supervisor' ? columnsData : columnsDataSuper} columns={userA.profile !== 'supervisor' ? columnsData : columnsDataSuper}
hasChild={true} hasChild={userA.profile !== 'supervisor' ? true :false}
removeClickRow={false} removeClickRow={false}
handleScroll={handleScroll} handleScroll={handleScroll}

View File

@ -560,6 +560,88 @@ const Settings = () => {
</Container> </Container>
</div> </div>
<div className={classes.root}>
<Container className={classes.container} maxWidth="sm">
<Paper className={classes.paper}>
<Typography variant="body1">
Noficar quando entrar novo ticket na fila
</Typography>
<Select
margin="dense"
variant="outlined"
native
id="notificationTransferQueue-setting"
name="notificationTransferQueue"
value={
settings &&
settings.length > 0 &&
getSettingValue('notificationTransferQueue')
}
className={classes.settingOption}
onChange={handleChangeSetting}
>
<option value="enabled">Ativado</option>
<option value="disabled">Desativado</option>
</Select>
</Paper>
</Container>
</div>
<div className={classes.root}>
<Container className={classes.container} maxWidth="sm">
<Paper className={classes.paper}>
<Typography variant="body1">
Bloquear mídias de Audio e Video
</Typography>
<Select
margin="dense"
variant="outlined"
native
id="blockAudioVideoMedia-setting"
name="blockAudioVideoMedia"
value={
settings &&
settings.length > 0 &&
getSettingValue('blockAudioVideoMedia')
}
className={classes.settingOption}
onChange={handleChangeSetting}
>
<option value="enabled">Ativado</option>
<option value="disabled">Desativado</option>
</Select>
</Paper>
</Container>
</div>
<div className={classes.root}>
<Container className={classes.container} maxWidth="sm">
<Paper className={classes.paper}>
<Typography variant="body1">
Mostrar tempo de espera dos tickets aguardando
</Typography>
<Select
margin="dense"
variant="outlined"
native
id="waitingTimeTickets-setting"
name="waitingTimeTickets"
value={
settings &&
settings.length > 0 &&
getSettingValue('waitingTimeTickets')
}
className={classes.settingOption}
onChange={handleChangeSetting}
>
<option value="enabled">Ativado</option>
<option value="disabled">Desativado</option>
</Select>
</Paper>
</Container>
</div>
</div> </div>
)} )}
/> />

View File

@ -11,6 +11,7 @@ import { i18n } from "../../translate/i18n";
import Hidden from "@material-ui/core/Hidden"; import Hidden from "@material-ui/core/Hidden";
import { SearchTicketProvider } from "../../context/SearchTicket/SearchTicket"; import { SearchTicketProvider } from "../../context/SearchTicket/SearchTicket";
import { TicketsProvider } from "../../context/TicketsProvider/TicketsProvider"
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
chatContainer: { chatContainer: {
@ -82,7 +83,9 @@ const Chat = () => {
} }
> >
<SearchTicketProvider> <SearchTicketProvider>
<TicketsManager /> <TicketsProvider>
<TicketsManager />
</TicketsProvider>
</SearchTicketProvider> </SearchTicketProvider>

View File

@ -252,7 +252,7 @@ const messages = {
search: { title: "Busca" }, search: { title: "Busca" },
}, },
search: { search: {
placeholder: "Busca telefone/nome", placeholder: "Tel/nome/conteúdo",
}, },
buttons: { buttons: {
showAll: "Todos", showAll: "Todos",