Compare commits

..

6 Commits

Author SHA1 Message Date
gustavo.pinho 11e2dc1a50 Merge branch 'integracao_wa_oficial_el_editing' of https://github.com/AdrianoRobson/projeto-hit into integracao_wa_oficial_el_editing 2024-02-01 18:49:13 -03:00
gustavo.pinho c3ffab3819 style: update job column display and enhance job editing
Details:
- Improved the presentation of the job column for better clarity.
- Enhanced the functionality for editing job roles.
2024-02-01 18:49:06 -03:00
adriano 21b19fcfbd fix: Resolve report error at supervisor level in frontend 2024-02-01 18:38:30 -03:00
adriano 09a469e892 Merge branch 'integracao_wa_oficial_el_editing' of github.com:AdrianoRobson/projeto-hit into integracao_wa_oficial_el_editing 2024-02-01 18:30:19 -03:00
gustavo.pinho 6a5a51ff3f chore: update reports tab for supervisors, add reminders functionality, and include user roles
Details:
- Updated the reports tab to enhance functionality for supervisors.
- Implemented a reminders feature for improved user experience.
- Added user roles to enhance user management capabilities.
2024-02-01 17:46:23 -03:00
adriano a2f946d849 feat: Configure business hours for WhatsApp sessions and add setting to hide campaign module 2024-02-01 17:16:56 -03:00
38 changed files with 937 additions and 449 deletions

View File

@ -30,6 +30,8 @@ app.get('/', function (req, res) {
app.post('/api/session', async function (req, res) { app.post('/api/session', async function (req, res) {
let { app_name, whatsappId, client_url, number } = req.body let { app_name, whatsappId, client_url, number } = req.body
let oldNumber = ''
if (app_name) { if (app_name) {
app_name = app_name.trim() app_name = app_name.trim()
} }
@ -67,6 +69,7 @@ app.post('/api/session', async function (req, res) {
} }
} }
let appPort = [] let appPort = []
let existSubDir = false let existSubDir = false
@ -98,7 +101,7 @@ app.post('/api/session', async function (req, res) {
path.join(sessionsPath, directoriesInDIrectory[i], subDir[x]) path.join(sessionsPath, directoriesInDIrectory[i], subDir[x])
) )
let oldNumber = subDir[x].split('_')[1] oldNumber = subDir[x].split('_')[1]
if (oldNumber != number) { if (oldNumber != number) {
deletePm2Process(subDir[x], currPath) deletePm2Process(subDir[x], currPath)
@ -197,7 +200,7 @@ app.post('/api/session', async function (req, res) {
) { ) {
const whatsapp_numbers = await new Promise((resolve, reject) => { const whatsapp_numbers = await new Promise((resolve, reject) => {
mysql_conn(db_credentials.db_conf).query( mysql_conn(db_credentials.db_conf).query(
'SELECT name FROM Whatsapps WHERE name LIKE ?', 'SELECT name, number FROM Whatsapps WHERE name LIKE ?',
[`%${number}%`], [`%${number}%`],
(err, result) => { (err, result) => {
if (err) { if (err) {
@ -209,8 +212,6 @@ app.post('/api/session', async function (req, res) {
) )
}) })
console.log('whatsapp_numbers: ', whatsapp_numbers)
let session_num = [] let session_num = []
if (whatsapp_numbers && whatsapp_numbers.length > 0) { if (whatsapp_numbers && whatsapp_numbers.length > 0) {
@ -341,6 +342,7 @@ app.post('/api/session', async function (req, res) {
stream.write('# NUMBER AND NAME THAT WILL BE DISPLAYED ON CONSOLE\n') stream.write('# NUMBER AND NAME THAT WILL BE DISPLAYED ON CONSOLE\n')
stream.write(`MOBILEUID=${number}\n`) stream.write(`MOBILEUID=${number}\n`)
stream.write(`MOBILENAME=${whatsappName}\n`) stream.write(`MOBILENAME=${whatsappName}\n`)
stream.write(`OLD_MOBILEUID=${oldNumber}\n`)
stream.write('\n') stream.write('\n')
stream.write('# PORT NUMBER FOR THIS API\n') stream.write('# PORT NUMBER FOR THIS API\n')

View File

@ -288,8 +288,6 @@ client.on("ready", async () => {
let url = process.env.CLIENT_URL + '/whatsapp/connection/number' let url = process.env.CLIENT_URL + '/whatsapp/connection/number'
try { try {
await client.logout() await client.logout()
@ -304,6 +302,48 @@ client.on("ready", async () => {
} }
if (process.env.OLD_MOBILEUID) {
const ticketSettingsId = await new Promise((resolve, reject) => {
dbcc.query("select id from SettingTickets where number = ?", [process.env.OLD_MOBILEUID,], (err, result) => {
if (err) {
reject(err)
}
else {
// resolve(result)
const idArray = result.map(row => row.id)
resolve(idArray)
}
})
})
if (ticketSettingsId?.length > 0) {
await new Promise((resolve, reject) => {
const idsToUpdate = ticketSettingsId // Assuming ticketSettingsId is an array of IDs
// Create placeholders for the IN clause based on the number of elements in idsToUpdate
const placeholders = Array(idsToUpdate.length).fill('?').join(',')
dbcc.query(
`UPDATE SettingTickets SET number = ? WHERE id IN (${placeholders})`,
[client.info["wid"]["user"], ...idsToUpdate], // Spread the array to pass individual values
function (err, result) {
if (err) {
console.log("ERROR: " + err)
reject(err)
} else {
resolve(result)
}
}
)
})
}
}
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
@ -324,7 +364,6 @@ client.on("ready", async () => {
}) })
let url = process.env.CLIENT_URL + '/whatsapp/connection/qrcode' let url = process.env.CLIENT_URL + '/whatsapp/connection/qrcode'
try { try {
@ -1193,9 +1232,9 @@ function comercialBuss(until_hour) {
scheduler_monitor = setInterval(monitor, 10000) scheduler_monitor = setInterval(monitor, 10000)
scheduler_campaign_monitor = setInterval(sendCampaignMessage, 3000) // scheduler_campaign_monitor = setInterval(sendCampaignMessage, 3000)
scheduler_internet_conn = setInterval(internetMonitor, 60000) // scheduler_internet_conn = setInterval(internetMonitor, 60000)
app.listen(process.env.PORT || 8003, function () { app.listen(process.env.PORT || 8003, function () {
console.log("\u26A1[server]: Server is running at Port ::: " + process.env.PORT || 8003) console.log("\u26A1[server]: Server is running at Port ::: " + process.env.PORT || 8003)

View File

@ -1,14 +0,0 @@
NODE_ENV=
BACKEND_URL=http://localhost
FRONTEND_URL=http://localhost:3000
PROXY_PORT=8080
PORT=8080
DB_DIALECT=
DB_HOST=
DB_USER=
DB_PASS=
DB_NAME=
JWT_SECRET=
JWT_REFRESH_SECRET=

View File

@ -34,6 +34,7 @@ type IndexQuery = {
startDate: string; startDate: string;
endDate: string; endDate: string;
pageNumber: string; pageNumber: string;
userQueues: [];
}; };
type ReportOnQueue = { type ReportOnQueue = {
@ -52,12 +53,11 @@ export const reportUserByDateStartDateEnd = async (req: Request, res: Response):
throw new AppError("ERR_NO_PERMISSION", 403); throw new AppError("ERR_NO_PERMISSION", 403);
} }
const { userId, startDate, endDate, pageNumber } = req.query as IndexQuery const { userId, startDate, endDate, pageNumber, userQueues } = req.query as IndexQuery
console.log("userId, startDate, endDate, pageNumber: ", userId, startDate, endDate, pageNumber); console.log("userId, startDate, endDate, pageNumber: ", userId, startDate, endDate, pageNumber);
const { tickets, count, hasMore } = await ShowTicketReport({ userId, startDate, endDate, pageNumber }); const { tickets, count, hasMore } = await ShowTicketReport({ userId, startDate, endDate, pageNumber });
// console.log('kkkkkkkkkkkkkkkkkk tickets: ', JSON.stringify(tickets, null, 6)) // console.log('kkkkkkkkkkkkkkkkkk tickets: ', JSON.stringify(tickets, null, 6))
return res.status(200).json({ tickets, count, hasMore }); return res.status(200).json({ tickets, count, hasMore });

View File

@ -18,9 +18,20 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
const settings = await ListSettingsService(); const settings = await ListSettingsService();
const config = await SettingTicket.findAll(); // const config = await SettingTicket.findAll();
return res.status(200).json({ settings, config }); return res.status(200).json({ settings, });
};
export const ticketSettings = async (
req: Request,
res: Response
): Promise<Response> => {
const { number } = req.params;
const config = await SettingTicket.findAll({ where: { number } });
return res.status(200).json({ config });
}; };
export const updateTicketSettings = async ( export const updateTicketSettings = async (
@ -28,6 +39,7 @@ export const updateTicketSettings = async (
res: Response res: Response
): Promise<Response> => { ): Promise<Response> => {
const { const {
number,
outBusinessHours, outBusinessHours,
ticketExpiration, ticketExpiration,
weekend, weekend,
@ -36,45 +48,53 @@ export const updateTicketSettings = async (
holiday holiday
} = req.body; } = req.body;
if (!number) throw new AppError("No number selected", 400);
if (outBusinessHours && Object.keys(outBusinessHours).length > 0) { if (outBusinessHours && Object.keys(outBusinessHours).length > 0) {
await updateSettingTicket({ await updateSettingTicket({
...outBusinessHours, ...outBusinessHours,
key: "outBusinessHours" key: "outBusinessHours",
number
}); });
} }
if (ticketExpiration && Object.keys(ticketExpiration).length > 0) { if (ticketExpiration && Object.keys(ticketExpiration).length > 0) {
await updateSettingTicket({ await updateSettingTicket({
...ticketExpiration, ...ticketExpiration,
key: "ticketExpiration" key: "ticketExpiration",
number
}); });
} }
if (weekend && Object.keys(weekend).length > 0) { if (weekend && Object.keys(weekend).length > 0) {
await updateSettingTicket({ await updateSettingTicket({
...weekend, ...weekend,
key: "weekend" key: "weekend",
number
}); });
} }
if (saturday && Object.keys(saturday).length > 0) { if (saturday && Object.keys(saturday).length > 0) {
await updateSettingTicket({ await updateSettingTicket({
...saturday, ...saturday,
key: "saturday" key: "saturday",
number
}); });
} }
if (sunday && Object.keys(sunday).length > 0) { if (sunday && Object.keys(sunday).length > 0) {
await updateSettingTicket({ await updateSettingTicket({
...sunday, ...sunday,
key: "sunday" key: "sunday",
number
}); });
} }
if (holiday && Object.keys(holiday).length > 0) { if (holiday && Object.keys(holiday).length > 0) {
await updateSettingTicket({ await updateSettingTicket({
...holiday, ...holiday,
key: "holiday" key: "holiday",
number
}); });
} }

View File

@ -253,8 +253,6 @@ export const update = async (
req.body.userId = null; req.body.userId = null;
} }
console.log("REQ.BODY: ", JSON.stringify(req.body, null, 6));
let ticketData: TicketData = req.body; let ticketData: TicketData = req.body;
if (getSettingValue("oneContactChatWithManyWhats")?.value == "enabled") { if (getSettingValue("oneContactChatWithManyWhats")?.value == "enabled") {

View File

@ -134,21 +134,33 @@ export const all = async (req: Request, res: Response): Promise<Response> => {
}; };
export const store = async (req: Request, res: Response): Promise<Response> => { export const store = async (req: Request, res: Response): Promise<Response> => {
const { email, password, name, profile, queueIds } = req.body; const { email, password, name, profile, positionCompany, queueIds } = req.body;
console.log("===========> req.url: ", req.url);
if ( if (
req.url === "/user" &&
getSettingValue("userCreation")?.value == "disabled" &&
req.user.profile == "admin"
) {
throw new AppError("ERR_NO_PERMISSION", 403);
} else if (
req.url === "/signup" && req.url === "/signup" &&
(await CheckSettingsHelper("userCreation")) === "disabled" getSettingValue("userCreation")?.value == "disabled"
) { ) {
throw new AppError("ERR_USER_CREATION_DISABLED", 403); throw new AppError("ERR_USER_CREATION_DISABLED", 403);
} else if (req.url !== "/signup" && req.user.profile !== "master") { } else if (
req.user.profile !== "master"
) {
throw new AppError("ERR_NO_PERMISSION", 403); throw new AppError("ERR_NO_PERMISSION", 403);
} }
const user = await CreateUserService({ const user = await CreateUserService({
email, email,
password, password,
name, name,
positionCompany,
profile, profile,
queueIds queueIds
}); });

View File

@ -39,6 +39,9 @@ import receiveWhatsAppMediaOfficialAPI from "../helpers/ReceiveWhatsAppMediaOffi
import whatsappOfficialAPI from "../helpers/WhatsappOfficialAPI"; import whatsappOfficialAPI from "../helpers/WhatsappOfficialAPI";
import whatsappOfficialNumberInfo from "../helpers/WhatsappOfficialNumberInfo"; import whatsappOfficialNumberInfo from "../helpers/WhatsappOfficialNumberInfo";
import { getSettingValue } from "../helpers/WhaticketSettings"; import { getSettingValue } from "../helpers/WhaticketSettings";
import ListWhatsAppsNumber from "../services/WhatsappService/ListWhatsAppsNumber";
import SettingTicket from "../models/SettingTicket";
import { Op } from "sequelize";
interface WhatsappData { interface WhatsappData {
name: string; name: string;
@ -483,6 +486,38 @@ export const remove = async (
}); });
} }
let whats = await ListWhatsAppsNumber(whatsappId);
// Remove tickets business hours config
if (whats?.whatsapps?.length == 1) {
const configIds = await SettingTicket.findAll({
where: { number: whats?.whatsapps[0]?.number },
raw: true,
attributes: ["id"]
});
const whatsappTicketConfig = await SettingTicket.findOne({
where: { number: whats.whatsapps[0].number }
});
if (whatsappTicketConfig) {
try {
await SettingTicket.destroy({
where: {
id: {
[Op.in]: configIds.map(config => config.id)
}
}
});
} catch (error) {
console.log(
"Error on delete SettingTicket by number: ",
whats?.whatsapps[0]?.number
);
}
}
}
await DeleteWhatsAppService(whatsappId); await DeleteWhatsAppService(whatsappId);
removeDir( removeDir(

View File

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

View File

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

View File

@ -0,0 +1,26 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
/*
Add altering commands here.
Return a promise to correctly handle asynchronicity.
Example:
return queryInterface.bulkInsert('People', [{
name: 'John Doe',
isBetaMember: false
}], {});
*/
},
down: (queryInterface, Sequelize) => {
/*
Add reverting commands here.
Return a promise to correctly handle asynchronicity.
Example:
return queryInterface.bulkDelete('People', null, {});
*/
}
};

View File

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

View File

@ -12,6 +12,7 @@ import {
import ptBR from "date-fns/locale/pt-BR"; import ptBR from "date-fns/locale/pt-BR";
import { splitDateTime } from "./SplitDateTime"; import { splitDateTime } from "./SplitDateTime";
import Whatsapp from "../models/Whatsapp";
const fsPromises = require("fs/promises"); const fsPromises = require("fs/promises");
const fs = require("fs"); const fs = require("fs");
@ -24,37 +25,43 @@ const AutoCloseTickets = async () => {
// if (!botInfo.userIdBot) return // if (!botInfo.userIdBot) return
const ticketExpiration = await SettingTicket.findOne({ const whatsapps = await Whatsapp.findAll({ group: ["number"] });
where: { key: "ticketExpiration" }
});
if (ticketExpiration && ticketExpiration.value == "enabled") { for (const whatsapp of whatsapps) {
const startTime = splitDateTime( // console.log("-------> whatsapp: ", JSON.stringify(whatsapps, null, 6));
new Date(
_format(new Date(ticketExpiration.startTime), "yyyy-MM-dd HH:mm:ss", {
locale: ptBR
})
)
);
const seconds = timeStringToSeconds(startTime.fullTime); const ticketExpiration = await SettingTicket.findOne({
where: { key: "ticketExpiration", number: whatsapp.number }
// console.log("Ticket seconds: ", seconds);
let tickets: any = await ListTicketTimeLife({
timeseconds: seconds,
status: "open"
}); });
// console.log("tickets: ", tickets); if (ticketExpiration && ticketExpiration.value == "enabled") {
const startTime = splitDateTime(
new Date(
_format(
new Date(ticketExpiration.startTime),
"yyyy-MM-dd HH:mm:ss",
{
locale: ptBR
}
)
)
);
for (let i = 0; i < tickets.length; i++) { const seconds = timeStringToSeconds(startTime.fullTime);
await UpdateTicketService({ let tickets: any = await ListTicketTimeLife({
ticketData: { status: "closed", statusChatEnd: "FINALIZADO" }, timeseconds: seconds,
ticketId: tickets[i].ticket_id, status: "open",
msg: ticketExpiration.message number: whatsapp.number
}); });
for (let i = 0; i < tickets.length; i++) {
await UpdateTicketService({
ticketData: { status: "closed", statusChatEnd: "FINALIZADO" },
ticketId: tickets[i].ticket_id,
msg: ticketExpiration.message
});
}
} }
} }
} catch (error) { } catch (error) {

View File

@ -11,6 +11,7 @@ import { convertBytes } from "./ConvertBytes";
import { deleteScheduleByTicketIdCache } from "./SchedulingNotifyCache"; import { deleteScheduleByTicketIdCache } from "./SchedulingNotifyCache";
import SchedulingNotify from "../models/SchedulingNotify"; import SchedulingNotify from "../models/SchedulingNotify";
import Ticket from "../models/Ticket"; import Ticket from "../models/Ticket";
import User from "../models/User";
import { Sequelize, Op } from "sequelize"; import { Sequelize, Op } from "sequelize";
@ -65,12 +66,21 @@ const monitor = async () => {
if (_ticket) continue if (_ticket) continue
if (ticket.dataValues.status == 'closed') { if (ticket.dataValues.status == 'closed') {
await ticket.update({ status: 'pending' }) await ticket.update({ status: 'open' })
}
const userId: number = ticket.getDataValue('userId');
let userN = '';
if(userId){
const useer = await User.findByPk(userId);
if(useer){
const userName: string = useer.getDataValue('name');
userN = `*${userName}:* \n`;
}
} }
await new Promise(f => setTimeout(f, 3000)); await new Promise(f => setTimeout(f, 3000));
await SendWhatsAppMessage({ await SendWhatsAppMessage({
body: schedulingNotifies[i].message, ticket body: userN+schedulingNotifies[i].message, ticket
}); });
@ -80,33 +90,33 @@ const monitor = async () => {
} }
exec("df -h /", (error: any, stdout: any, stderr: any) => { // exec("df -h /", (error: any, stdout: any, stderr: any) => {
if (error) { // if (error) {
console.log(`exec error: ${error.message}`); // console.log(`exec error: ${error.message}`);
return; // return;
} // }
if (stderr) { // if (stderr) {
console.log(`exec stderr: ${stderr}`); // console.log(`exec stderr: ${stderr}`);
return; // return;
} // }
stdout = stdout.split(/\r?\n/) // stdout = stdout.split(/\r?\n/)
stdout = stdout[1].trim().split(/\s+/) // stdout = stdout[1].trim().split(/\s+/)
// DISK SPACE MONITORING // // DISK SPACE MONITORING
const io = getIO(); // const io = getIO();
io.emit("diskSpaceMonit", { // io.emit("diskSpaceMonit", {
action: "update", // action: "update",
diskSpace: { // diskSpace: {
size: stdout[1], // size: stdout[1],
used: stdout[2], // used: stdout[2],
available: stdout[3], // available: stdout[3],
use: stdout[4] // use: stdout[4]
} // }
}); // });
}); // });

View File

@ -4,6 +4,7 @@ import User from "../models/User";
interface SerializedUser { interface SerializedUser {
id: number; id: number;
name: string; name: string;
positionCompany: string;
email: string; email: string;
profile: string; profile: string;
queues: Queue[]; queues: Queue[];
@ -13,6 +14,7 @@ export const SerializeUser = (user: User): SerializedUser => {
return { return {
id: user.id, id: user.id,
name: user.name, name: user.name,
positionCompany: user.positionCompany,
email: user.email, email: user.email,
profile: user.profile, profile: user.profile,
queues: user.queues queues: user.queues

View File

@ -13,11 +13,11 @@ import {
} from "date-fns"; } from "date-fns";
import ptBR from "date-fns/locale/pt-BR"; import ptBR from "date-fns/locale/pt-BR";
const isHoliday = async () => { const isHoliday = async (number: string | number) => {
let obj = { set: false, msg: "" }; let obj = { set: false, msg: "" };
const holiday = await SettingTicket.findOne({ const holiday = await SettingTicket.findOne({
where: { key: "holiday" } where: { key: "holiday", number }
}); });
if ( if (
@ -50,11 +50,11 @@ const isHoliday = async () => {
return obj; return obj;
}; };
const isWeekend = async () => { const isWeekend = async (number: string | number) => {
let obj = { set: false, msg: "" }; let obj = { set: false, msg: "" };
const weekend = await SettingTicket.findOne({ const weekend = await SettingTicket.findOne({
where: { key: "weekend" } where: { key: "weekend", number }
}); });
if ( if (
@ -62,7 +62,6 @@ const isWeekend = async () => {
weekend.value == "enabled" && weekend.value == "enabled" &&
weekend.message?.trim()?.length > 0 weekend.message?.trim()?.length > 0
) { ) {
// Specify your desired timezone // Specify your desired timezone
const brazilTimeZone = "America/Sao_Paulo"; const brazilTimeZone = "America/Sao_Paulo";
@ -100,8 +99,7 @@ const isWeekend = async () => {
obj.set = true; obj.set = true;
obj.msg = weekend.message; obj.msg = weekend.message;
} }
} } else {
else{
// obj.set = true; // obj.set = true;
// obj.msg = weekend.message; // obj.msg = weekend.message;
} }
@ -109,11 +107,11 @@ const isWeekend = async () => {
} }
}; };
async function isOutBusinessTime() { async function isOutBusinessTime(number: string | number) {
let obj = { set: false, msg: "" }; let obj = { set: false, msg: "" };
const outBusinessHours = await SettingTicket.findOne({ const outBusinessHours = await SettingTicket.findOne({
where: { key: "outBusinessHours" } where: { key: "outBusinessHours", number }
}); });
let isWithinRange = false; let isWithinRange = false;

View File

@ -30,6 +30,9 @@ class SettingTicket extends Model<SettingTicket> {
@Column @Column
key: string; key: string;
@Column
number: string;
@CreatedAt @CreatedAt
createdAt: Date; createdAt: Date;

View File

@ -42,6 +42,9 @@ class User extends Model<User> {
@Column @Column
tokenVersion: number; tokenVersion: number;
@Column
positionCompany: string;
@Default("admin") @Default("admin")
@Column @Column
profile: string; profile: string;
@ -51,7 +54,6 @@ class User extends Model<User> {
@UpdatedAt @UpdatedAt
updatedAt: Date; updatedAt: Date;
@HasMany(() => Ticket) @HasMany(() => Ticket)
tickets: Ticket[]; tickets: Ticket[];

View File

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

View File

@ -7,6 +7,7 @@ interface Request {
endTime: string; endTime: string;
value: string; value: string;
message: string; message: string;
number: string;
} }
const updateSettingTicket = async ({ const updateSettingTicket = async ({
@ -14,16 +15,30 @@ const updateSettingTicket = async ({
startTime, startTime,
endTime, endTime,
value, value,
message message,
number
}: Request): Promise<SettingTicket | undefined> => { }: Request): Promise<SettingTicket | undefined> => {
try { try {
const businessHours = await SettingTicket.findOne({ where: { key } }); let businessHours = await SettingTicket.findOne({ where: { key, number } });
if (!businessHours) { if (!businessHours) {
throw new AppError("ERR_NO_SETTING_FOUND", 404); // throw new AppError("ERR_NO_SETTING_FOUND", 404);
businessHours = await SettingTicket.create({
key,
startTime,
endTime,
value,
message,
number
});
return businessHours;
} }
await businessHours.update({ startTime, endTime, message, value }); await businessHours.update(
{ startTime, endTime, message, value, number },
{ where: { key, number } }
);
return businessHours; return businessHours;
} catch (error: any) { } catch (error: any) {

View File

@ -1,47 +1,60 @@
import { Sequelize } from "sequelize";
import { Sequelize, } from "sequelize";
const dbConfig = require("../../config/database"); const dbConfig = require("../../config/database");
const sequelize = new Sequelize(dbConfig); const sequelize = new Sequelize(dbConfig);
const { QueryTypes } = require('sequelize'); const { QueryTypes } = require("sequelize");
import { splitDateTime } from "../../helpers/SplitDateTime"; import { splitDateTime } from "../../helpers/SplitDateTime";
import format from 'date-fns/format'; import format from "date-fns/format";
import ptBR from 'date-fns/locale/pt-BR'; import ptBR from "date-fns/locale/pt-BR";
interface Request { interface Request {
timeseconds: string | number; timeseconds: string | number;
status: string; status: string;
userId?: string | number; number?: string;
userId?: string | number;
} }
const ListTicketTimeLife = async ({timeseconds, status, userId }: Request): Promise<any[]> => { const ListTicketTimeLife = async ({
timeseconds,
status,
number,
userId
}: Request): Promise<any[]> => {
let tickets = [];
let tickets = [] let currentDate = format(new Date(), "yyyy-MM-dd HH:mm:ss", { locale: ptBR });
let currentDate = format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ptBR }) // console.log('------------------> currentDate: ', currentDate)
// console.log('------------------> currentDate: ', currentDate) if (userId) {
// CONSULTANDO FILAS PELO ID DO USUARIO
tickets = await sequelize.query(
`select user.id as user_id, user.name as user_name, t.id as ticket_id from Tickets as t inner join Users as user on
t.userId = user.id and user.name = 'botqueue' and t.status='${status}' and (TIMESTAMPDIFF(SECOND, t.updatedAt, '${currentDate}')) >= ${timeseconds};`,
{ type: QueryTypes.SELECT }
);
} else if (number) {
// CONSULTANDO TICKETS PELO WHATSAPP
tickets = await sequelize.query(
`SELECT t.id AS ticket_id
FROM Tickets t
JOIN Whatsapps w ON t.whatsappId = w.id
AND w.number = ${number}
WHERE t.status = 'open'
AND TIMESTAMPDIFF(SECOND, t.updatedAt, '${currentDate}') >= ${timeseconds};`,
{ type: QueryTypes.SELECT }
);
} else {
// CONSULTANDO FILAS PELO USUARIO
tickets = await sequelize.query(
`select id as ticket_id from Tickets where status='${status}' and
(TIMESTAMPDIFF(SECOND, updatedAt, '${currentDate}')) >= ${timeseconds};`,
{ type: QueryTypes.SELECT }
);
}
if (userId) { return tickets;
// CONSULTANDO FILAS PELO ID DO USUARIO
tickets = await sequelize.query(`select user.id as user_id, user.name as user_name, t.id as ticket_id from Tickets as t inner join Users as user on
t.userId = user.id and user.name = 'botqueue' and t.status='${status}' and (TIMESTAMPDIFF(SECOND, t.updatedAt, '${currentDate}')) >= ${timeseconds};`, { type: QueryTypes.SELECT });
} else {
// CONSULTANDO FILAS PELO USUARIO
tickets = await sequelize.query(`select id as ticket_id from Tickets where status='${status}' and
(TIMESTAMPDIFF(SECOND, updatedAt, '${currentDate}')) >= ${timeseconds};`, { type: QueryTypes.SELECT });
}
return tickets;
}; };
export default ListTicketTimeLife; export default ListTicketTimeLife;

View File

@ -10,6 +10,7 @@ import Queue from "../../models/Queue";
interface SerializedUser { interface SerializedUser {
id: number; id: number;
name: string; name: string;
positionCompany: string;
email: string; email: string;
profile: string; profile: string;
queues: Queue[]; queues: Queue[];

View File

@ -8,6 +8,7 @@ interface Request {
email: string; email: string;
password: string; password: string;
name: string; name: string;
positionCompany?: string;
queueIds?: number[]; queueIds?: number[];
profile?: string; profile?: string;
} }
@ -15,6 +16,7 @@ interface Request {
interface Response { interface Response {
email: string; email: string;
name: string; name: string;
positionCompany: string;
id: number; id: number;
profile: string; profile: string;
} }
@ -23,6 +25,7 @@ const CreateUserService = async ({
email, email,
password, password,
name, name,
positionCompany,
queueIds = [], queueIds = [],
profile = "master" profile = "master"
}: Request): Promise<Response> => { }: Request): Promise<Response> => {
@ -70,6 +73,7 @@ const CreateUserService = async ({
email, email,
password, password,
name, name,
positionCompany,
profile profile
}, },
{ include: ["queues"] } { include: ["queues"] }

View File

@ -40,7 +40,7 @@ const ListUser = async ({ profile, userId, raw, userIds, profiles }: Request): P
const users = await User.findAll({ const users = await User.findAll({
where: where_clause, where: where_clause,
raw, raw,
attributes: ["id", "name", "email"], attributes: ["id", "name", "email", "positionCompany"],
include: [ include: [
{ model: Queue, as: "queues", attributes: ["id", "name", "color"] } { model: Queue, as: "queues", attributes: ["id", "name", "color"] }

View File

@ -63,7 +63,7 @@ const ListUsersService = async ({
const { count, rows: users } = await User.findAndCountAll({ const { count, rows: users } = await User.findAndCountAll({
where: whereCondition, where: whereCondition,
attributes: ["name", "id", "email", "profile", "createdAt"], attributes: ["name", "id", "email","positionCompany", "profile", "createdAt"],
limit, limit,
offset, offset,
order: [["createdAt", "DESC"]], order: [["createdAt", "DESC"]],

View File

@ -4,7 +4,7 @@ import Queue from "../../models/Queue";
const ShowUserService = async (id: string | number): Promise<User> => { const ShowUserService = async (id: string | number): Promise<User> => {
const user = await User.findByPk(id, { const user = await User.findByPk(id, {
attributes: ["name", "id", "email", "profile", "tokenVersion"], attributes: ["name", "id", "email", "profile", "positionCompany", "tokenVersion"],
include: [ include: [
{ model: Queue, as: "queues", attributes: ["id", "name", "color"] } { model: Queue, as: "queues", attributes: ["id", "name", "color"] }
], ],

View File

@ -8,6 +8,7 @@ interface UserData {
email?: string; email?: string;
password?: string; password?: string;
name?: string; name?: string;
positionCompany?: string;
profile?: string; profile?: string;
queueIds?: number[]; queueIds?: number[];
} }
@ -60,7 +61,7 @@ const UpdateUserService = async ({
}); });
const { email, password, profile, name, queueIds = [] } = userData; const { email, password, profile, name, positionCompany, queueIds = [] } = userData;
try { try {
await schema.validate({ email, password, profile, name }); await schema.validate({ email, password, profile, name });
@ -73,6 +74,7 @@ const UpdateUserService = async ({
email, email,
password, password,
profile, profile,
positionCompany,
name name
}); });

View File

@ -87,6 +87,7 @@ import { Op } from "sequelize";
import SettingTicket from "../../models/SettingTicket"; import SettingTicket from "../../models/SettingTicket";
import mostRepeatedPhrase from "../../helpers/MostRepeatedPhrase"; import mostRepeatedPhrase from "../../helpers/MostRepeatedPhrase";
import ListWhatsAppsNumber from "../WhatsappService/ListWhatsAppsNumber";
var lst: any[] = getWhatsappIds(); var lst: any[] = getWhatsappIds();
@ -166,7 +167,6 @@ const verifyMediaMessage = async (
phoneNumberId: msg?.phoneNumberId phoneNumberId: msg?.phoneNumberId
}; };
if (!ticket?.phoneNumberId) { if (!ticket?.phoneNumberId) {
if (!media.filename) { if (!media.filename) {
const ext = media.mimetype.split("/")[1].split(";")[0]; const ext = media.mimetype.split("/")[1].split(";")[0];
@ -263,10 +263,6 @@ const verifyMediaMessage = async (
// return newMessage; // return newMessage;
// }; // };
const verifyMessage = async ( const verifyMessage = async (
msg: any, msg: any,
ticket: Ticket, ticket: Ticket,
@ -347,7 +343,9 @@ const verifyQueue = async (
} }
// //
const outService = await outOfService(); let whatsapp: any = await whatsappInfo(ticket?.whatsappId);
const outService = await outOfService(whatsapp?.number);
if (outService.length > 0) { if (outService.length > 0) {
const { type, msg: msgOutService } = outService[0]; const { type, msg: msgOutService } = outService[0];
@ -555,17 +553,21 @@ const handleMessage = async (
// let groupContact: Contact | undefined; // let groupContact: Contact | undefined;
if (msg.fromMe) { if (msg.fromMe) {
const ticketExpiration = await SettingTicket.findOne({ const whatsapp = await whatsappInfo(wbot.id);
where: { key: "ticketExpiration" }
});
if ( if (whatsapp?.number) {
ticketExpiration && const ticketExpiration = await SettingTicket.findOne({
ticketExpiration.value == "enabled" && where: { key: "ticketExpiration", number: whatsapp.number }
ticketExpiration?.message.trim() == msg.body.trim() });
) {
console.log("*********** TICKET EXPIRATION"); if (
return; 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 // messages sent automatically by wbot have a special character in front of it
@ -694,22 +696,23 @@ const handleMessage = async (
ticketHasQueue = true; ticketHasQueue = true;
} }
if (ticketHasQueue && ticket.status != "open") { if (ticketHasQueue && ticket.status != "open") {
const outService = await outOfService(); let whatsapp: any = await whatsappInfo(ticket?.whatsappId);
if (outService.length > 0) { const outService = await outOfService(whatsapp.number);
const { type, msg: msgOutService } = outService[0];
if (msg.fromMe && msgOutService == msg.body) { if (outService.length > 0) {
console.log(`${type} message ignored`); const { type, msg: msgOutService } = outService[0];
return;
}
botSendMessage(ticket, msgOutService); if (msg.fromMe && msgOutService == msg.body) {
return; console.log(`${type} message ignored`);
} return;
} }
botSendMessage(ticket, msgOutService);
return;
}
}
} catch (err) { } catch (err) {
Sentry.captureException(err); Sentry.captureException(err);
console.log("Error handling whatsapp message: Err: ", err); console.log("Error handling whatsapp message: Err: ", err);
@ -778,9 +781,9 @@ const wbotMessageListener = (wbot: Session): void => {
}); });
}; };
const outOfService = async () => { const outOfService = async (number: string) => {
// MESSAGE TO HOLIDAY // MESSAGE TO HOLIDAY
const holiday: any = await isHoliday(); const holiday: any = await isHoliday(number);
let objs: any = []; let objs: any = [];
@ -789,14 +792,14 @@ const outOfService = async () => {
} }
// MESSAGES TO SATURDAY OR SUNDAY // MESSAGES TO SATURDAY OR SUNDAY
const weekend: any = await isWeekend(); const weekend: any = await isWeekend(number);
if (weekend && weekend.set) { if (weekend && weekend.set) {
objs.push({ type: "weekend", msg: weekend.msg }); objs.push({ type: "weekend", msg: weekend.msg });
} }
// MESSAGE TO BUSINESS TIME // MESSAGE TO BUSINESS TIME
const businessTime = await isOutBusinessTime(); const businessTime = await isOutBusinessTime(number);
if (businessTime && businessTime.set) { if (businessTime && businessTime.set) {
objs.push({ type: "businessTime", msg: businessTime.msg }); objs.push({ type: "businessTime", msg: businessTime.msg });
@ -816,3 +819,6 @@ export {
isValidMsg, isValidMsg,
mediaTypeWhatsappOfficial mediaTypeWhatsappOfficial
}; };
async function whatsappInfo(whatsappId: string | number) {
return await Whatsapp.findByPk(whatsappId);
}

View File

@ -1 +0,0 @@
REACT_APP_BACKEND_URL = http://localhost:8080/

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, } from 'react' import React, { useState, useEffect, useContext } from 'react'
// import * as Yup from 'yup' // import * as Yup from 'yup'
import { Formik, Form, Field, } from 'formik' import { Formik, Form, Field, } from 'formik'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
@ -12,7 +12,7 @@ import DateFnsUtils from '@date-io/date-fns'
import ptBrLocale from "date-fns/locale/pt-BR" import ptBrLocale from "date-fns/locale/pt-BR"
import { WhatsAppsContext } from "../../context/WhatsApp/WhatsAppsContext"
import { import {
MuiPickersUtilsProvider, MuiPickersUtilsProvider,
@ -28,12 +28,16 @@ import {
TextField, TextField,
Switch, Switch,
FormControlLabel, FormControlLabel,
Divider,
} from '@material-ui/core' } from '@material-ui/core'
import api from '../../services/api' import api from '../../services/api'
import { i18n } from '../../translate/i18n' import { i18n } from '../../translate/i18n'
import toastError from '../../errors/toastError' import toastError from '../../errors/toastError'
import Select from "@material-ui/core/Select"
import MenuItem from "@material-ui/core/MenuItem"
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
display: 'flex', display: 'flex',
@ -87,13 +91,35 @@ const ConfigModal = ({ open, onClose, change }) => {
enableWeekendMessage: false enableWeekendMessage: false
} }
const { whatsApps } = useContext(WhatsAppsContext)
const [selectedNumber, setSelectedNumber] = useState('')
const [config, setConfig] = useState(initialState) const [config, setConfig] = useState(initialState)
useEffect(() => {
console.log('selectedNumber: ', selectedNumber)
if (selectedNumber?.trim().length === 0) {
setConfig(initialState)
}
}, [selectedNumber])
useEffect(() => { useEffect(() => {
const fetchSession = async () => { const fetchSession = async () => {
try { try {
const { data } = await api.get('/settings') // const { data } = await api.get('/settings')
if (!selectedNumber) return
const { data } = await api.get(`/settings/ticket/${selectedNumber}`)
if (data?.config && data.config.length === 0) {
setConfig(initialState)
return
}
const outBusinessHours = data.config.find((c) => c.key === "outBusinessHours") const outBusinessHours = data.config.find((c) => c.key === "outBusinessHours")
const ticketExpiration = data.config.find((c) => c.key === "ticketExpiration") const ticketExpiration = data.config.find((c) => c.key === "ticketExpiration")
@ -127,11 +153,12 @@ const ConfigModal = ({ open, onClose, change }) => {
} }
} }
fetchSession() fetchSession()
}, [change]) }, [change, selectedNumber])
const handleSaveConfig = async (values) => { const handleSaveConfig = async (values) => {
values = { values = {
number: selectedNumber,
outBusinessHours: { outBusinessHours: {
startTime: values.startTimeBus, startTime: values.startTimeBus,
endTime: values.endTimeBus, endTime: values.endTimeBus,
@ -147,7 +174,7 @@ const ConfigModal = ({ open, onClose, change }) => {
message: values.weekendMessage, message: values.weekendMessage,
value: values.enableWeekendMessage ? 'enabled' : 'disabled' value: values.enableWeekendMessage ? 'enabled' : 'disabled'
}, },
saturday:{ saturday: {
value: values.checkboxSaturdayValue ? 'enabled' : 'disabled' value: values.checkboxSaturdayValue ? 'enabled' : 'disabled'
}, },
sunday: { sunday: {
@ -211,6 +238,39 @@ const ConfigModal = ({ open, onClose, change }) => {
<DialogContent dividers> <DialogContent dividers>
<div>
<Select
value={selectedNumber}
onChange={(e) => setSelectedNumber(e.target.value)}
label={i18n.t("transferTicketModal.fieldQueuePlaceholder")}
required
>
<MenuItem style={{ background: "white", }} value={''}>&nbsp;</MenuItem>
{whatsApps.reduce((acc, curr) => {
const existingObject = acc.find(item => item.number === curr.number)
if (!existingObject) {
acc.push(curr)
}
return acc
}, []).map((whatsapp) => (
<MenuItem
key={whatsapp.id}
value={whatsapp.number}
>
{whatsapp.number}
</MenuItem>
))}
</Select>
</div>
<Divider />
<br />
<div className={classes.multFieldLine}> <div className={classes.multFieldLine}>
<Field <Field
component={TimePicker} component={TimePicker}

View File

@ -227,9 +227,30 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => {
<DialogTitle id="form-dialog-title"> <DialogTitle id="form-dialog-title">
{i18n.t("transferTicketModal.title")} {i18n.t("transferTicketModal.title")}
</DialogTitle> </DialogTitle>
<DialogContent dividers> <DialogContent dividers >
<FormControl variant="outlined" className={classes.maxWidth} style={{marginBottom: '8px'}}>
{/* <InputLabel>{i18n.t("transferTicketModal.fieldQueueLabel")}</InputLabel> */}
<InputLabel>{'Usuário'}</InputLabel>
<Select
value={selectedUser}
onChange={e => setSelectedUser(e.target.value)}
label={'Transfeir para fila'}
>
<MenuItem style={{ background: "white", }} value={''}>&nbsp;</MenuItem>
{users.map((user) => (
<MenuItem
key={user.id}
value={user.id}
>{user.name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl variant="outlined" className={classes.maxWidth}> <FormControl variant="outlined" className={classes.maxWidth}>
<InputLabel>{i18n.t("transferTicketModal.fieldQueueLabel")}</InputLabel> <InputLabel>{i18n.t("transferTicketModal.fieldQueuePlaceholder")}</InputLabel>
<Select <Select
value={selectedQueue} value={selectedQueue}
@ -253,24 +274,6 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => {
</Select> </Select>
<br />
<Select
value={selectedUser}
onChange={e => setSelectedUser(e.target.value)}
label={'Transfeir para usuario'}
>
<MenuItem style={{ background: "white", }} value={''}>&nbsp;</MenuItem>
{users.map((user) => (
<MenuItem
key={user.id}
value={user.id}
>{user.name}
</MenuItem>
))}
</Select>
</FormControl> </FormControl>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>

View File

@ -84,6 +84,8 @@ const UserModal = ({ open, onClose, userId }) => {
name: "", name: "",
email: "", email: "",
password: "", password: "",
positionCompany: "",
position: "",
profile: "user", profile: "user",
}; };
@ -220,10 +222,9 @@ const UserModal = ({ open, onClose, userId }) => {
fullWidth fullWidth
/> />
</div> </div>
<div className={classes.multFieldLine}>
<Field <Field
as={TextField} as={TextField}
label={i18n.t("userModal.form.email")} label='Login'
name="email" name="email"
error={touched.email && Boolean(errors.email)} error={touched.email && Boolean(errors.email)}
helperText={touched.email && errors.email} helperText={touched.email && errors.email}
@ -231,6 +232,17 @@ const UserModal = ({ open, onClose, userId }) => {
margin="dense" margin="dense"
fullWidth fullWidth
/> />
<div className={classes.multFieldLine}>
<Field
as={TextField}
label="Cargo"
name="positionCompany"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
fullWidth
/>
<FormControl <FormControl
variant="outlined" variant="outlined"
className={classes.formControl} className={classes.formControl}
@ -262,6 +274,7 @@ const UserModal = ({ open, onClose, userId }) => {
/> />
</FormControl> </FormControl>
</div> </div>
<Can <Can
role={loggedInUser.profile} role={loggedInUser.profile}
perform="user-modal:editQueues" perform="user-modal:editQueues"

View File

@ -10,7 +10,7 @@ import { Badge } from '@material-ui/core'
import DashboardOutlinedIcon from '@material-ui/icons/DashboardOutlined' import DashboardOutlinedIcon from '@material-ui/icons/DashboardOutlined'
import ReportOutlinedIcon from '@material-ui/icons/ReportOutlined' import ReportOutlinedIcon from '@material-ui/icons/ReportOutlined'
import CampaignIcon from '@material-ui/icons/Send'; import CampaignIcon from '@material-ui/icons/Send'
import SendOutlined from '@material-ui/icons/SendOutlined' import SendOutlined from '@material-ui/icons/SendOutlined'
@ -29,6 +29,10 @@ import { i18n } from '../translate/i18n'
import { WhatsAppsContext } from '../context/WhatsApp/WhatsAppsContext' import { WhatsAppsContext } from '../context/WhatsApp/WhatsAppsContext'
import { AuthContext } from '../context/Auth/AuthContext' import { AuthContext } from '../context/Auth/AuthContext'
import { Can } from '../components/Can' import { Can } from '../components/Can'
import openSocket from 'socket.io-client'
import api from '../services/api'
function ListItemLink(props) { function ListItemLink(props) {
const { icon, primary, to, className } = props const { icon, primary, to, className } = props
@ -56,6 +60,8 @@ const MainListItems = (props) => {
const { whatsApps } = useContext(WhatsAppsContext) const { whatsApps } = useContext(WhatsAppsContext)
const { user } = useContext(AuthContext) const { user } = useContext(AuthContext)
const [connectionWarning, setConnectionWarning] = useState(false) const [connectionWarning, setConnectionWarning] = useState(false)
const [settings, setSettings] = useState([])
useEffect(() => { useEffect(() => {
const delayDebounceFn = setTimeout(() => { const delayDebounceFn = setTimeout(() => {
@ -79,6 +85,55 @@ const MainListItems = (props) => {
return () => clearTimeout(delayDebounceFn) return () => clearTimeout(delayDebounceFn)
}, [whatsApps]) }, [whatsApps])
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
const fetchSession = async () => {
try {
try {
const { data } = await api.get('/settings')
setSettings(data.settings)
} catch (err) {
// toastError(err)
}
} catch (err) {
// toastError(err)
}
}
fetchSession()
}, 500)
return () => clearTimeout(delayDebounceFn)
}, [])
const getSettingValue = (key) => {
return settings?.find((s) => s?.key === key)?.value
// const { value } = settings.find((s) => s?.key === key)
// return value
}
useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL)
socket.on('settings', (data) => {
if (data.action === 'update') {
setSettings((prevState) => {
const aux = [...prevState]
const settingIndex = aux.findIndex((s) => s.key === data.setting.key)
aux[settingIndex].value = data.setting.value
return aux
})
}
})
return () => {
socket.disconnect()
}
}, [])
return ( return (
//Solicitado pelo Adriano: Click no LinkItem e fechar o menu! //Solicitado pelo Adriano: Click no LinkItem e fechar o menu!
<div onClick={() => setDrawerOpen(false)}> <div onClick={() => setDrawerOpen(false)}>
@ -112,11 +167,7 @@ const MainListItems = (props) => {
<> <>
<Divider /> <Divider />
<ListSubheader inset>{i18n.t("mainDrawer.listItems.administration")}</ListSubheader> <ListSubheader inset>{i18n.t("mainDrawer.listItems.administration")}</ListSubheader>
<ListItemLink
to="/users"
primary={i18n.t("mainDrawer.listItems.users")}
icon={<PeopleAltOutlinedIcon />}
/>
<ListItemLink <ListItemLink
to="/" to="/"
primary="Dashboard" primary="Dashboard"
@ -140,7 +191,11 @@ const MainListItems = (props) => {
perform="drawer-admin-items:view" perform="drawer-admin-items:view"
yes={() => ( yes={() => (
<> <>
<ListItemLink
to="/users"
primary={i18n.t("mainDrawer.listItems.users")}
icon={<PeopleAltOutlinedIcon />}
/>
<ListItemLink <ListItemLink
to="/queues" to="/queues"
primary={i18n.t('mainDrawer.listItems.queues')} primary={i18n.t('mainDrawer.listItems.queues')}
@ -169,11 +224,16 @@ const MainListItems = (props) => {
icon={<ReportOutlinedIcon />} icon={<ReportOutlinedIcon />}
/> */} /> */}
<ListItemLink {
to="/campaign" (getSettingValue('hasCampaign') === 'enabled' || user.profile === 'master') && (
primary="Campanha" <ListItemLink
icon={<CampaignIcon />} to="/campaign"
/> primary="Campanha"
icon={<CampaignIcon />}
/>
)
}
<Can <Can
role={user.profile} role={user.profile}

View File

@ -103,7 +103,7 @@ const Queues = () => {
const [settings, setSettings] = useState([]) const [settings, setSettings] = useState([])
useEffect(() => { useEffect(() => {
;(async () => { ; (async () => {
setLoading(true) setLoading(true)
try { try {
const { data } = await api.get('/queue') const { data } = await api.get('/queue')
@ -203,8 +203,7 @@ const Queues = () => {
<ConfirmationModal <ConfirmationModal
title={ title={
selectedQueue && selectedQueue &&
`${i18n.t('queues.confirmationModal.deleteTitle')} ${ `${i18n.t('queues.confirmationModal.deleteTitle')} ${selectedQueue.name
selectedQueue.name
}?` }?`
} }
open={confirmModalOpen} open={confirmModalOpen}
@ -301,28 +300,36 @@ const Queues = () => {
settings.length > 0 && settings.length > 0 &&
getSettingValue('editQueue') && getSettingValue('editQueue') &&
getSettingValue('editQueue') === 'enabled') | getSettingValue('editQueue') === 'enabled') |
(user.profile === 'master') ? ( (user.profile === 'master') ? (
<IconButton
size="small" <>
onClick={() => handleEditQueue(queue)} <IconButton
> size="small"
<Edit /> onClick={() => handleEditQueue(queue)}
</IconButton> >
<Edit />
</IconButton>
<IconButton
size="small"
onClick={() => {
setSelectedQueue(queue)
setConfirmModalOpen(true)
}}
>
<DeleteOutline />
</IconButton>
</>
) : ( ) : (
<div></div> <div></div>
)} )}
</div> </div>
// <IconButton
// size="small"
// onClick={() => handleEditQueue(queue)}
// >
// <Edit />
// </IconButton>
)} )}
/> />
<Can {/* <Can
role={user.profile} role={user.profile}
perform="show-icon-delete-queue" perform="show-icon-delete-queue"
yes={() => ( yes={() => (
@ -336,7 +343,7 @@ const Queues = () => {
<DeleteOutline /> <DeleteOutline />
</IconButton> </IconButton>
)} )}
/> /> */}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@ -221,6 +221,19 @@ let columnsData = [
{ title: `${i18n.t("reports.listColumns.column1_8")}`, field: 'updatedAt' }, { title: `${i18n.t("reports.listColumns.column1_8")}`, field: 'updatedAt' },
{ title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' }] { title: `${i18n.t("reports.listColumns.column1_9")}`, field: 'statusChatEnd' }]
let columnsDataSuper = [
{ 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.column0_3")}`, field: 'contact.name' },
{ title: `${i18n.t("reports.listColumns.column1_5")}`, field: 'queue.name' },
{ title: 'Status', field: 'status' },
{ 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' }
]
// function convertAndFormatDate(dateString) { // function convertAndFormatDate(dateString) {
@ -245,7 +258,6 @@ let columnsData = [
const Report = () => { const Report = () => {
const { user: userA } = useContext(AuthContext) const { user: userA } = useContext(AuthContext)
//-------- //--------
const [searchParam] = useState("") const [searchParam] = useState("")
@ -316,14 +328,21 @@ const Report = () => {
setLoading(true) setLoading(true)
const fetchQueries = async () => { const fetchQueries = async () => {
try { try {
if (reportOption === '1') { if (reportOption === '1') {
// const { data } = await api.get("/reports/", { params: { userId: userId ? userId : 0, startDate: convertAndFormatDate(startDate), endDate: convertAndFormatDate(endDate), pageNumber: pageNumberTickets }, }) // const { data } = await api.get("/reports/", { params: { userId: userId ? userId : 0, startDate: convertAndFormatDate(startDate), endDate: convertAndFormatDate(endDate), pageNumber: pageNumberTickets }, })
const { data } = await api.get("/reports/", { params: { userId, startDate, endDate, pageNumber: pageNumberTickets }, }) const { data } = await api.get("/reports/", { params: { userId, startDate, endDate, pageNumber: pageNumberTickets }, userQueues: userA.queues})
let ticketsQueue = data.tickets;
let userQueues = userA.queues;
let filterQueuesTickets = [];
if(userQueues.length > 1){
filterQueuesTickets = ticketsQueue.filter(ticket => userQueues.some(queue => queue?.name === ticket?.queue?.name));
}else if(userQueues.length > 0) {
filterQueuesTickets = ticketsQueue.filter(ticket => ticket?.queue?.name === userQueues[0]?.name);
}
data.tickets = filterQueuesTickets;
dispatchQ({ type: "LOAD_QUERY", payload: data.tickets }) dispatchQ({ type: "LOAD_QUERY", payload: data.tickets })
setHasMore(data.hasMore) setHasMore(data.hasMore)
@ -684,7 +703,7 @@ const Report = () => {
<> <>
<MTable data={query} <MTable data={query}
columns={columnsData} columns={userA.profile !== 'supervisor' ?columnsData:columnsDataSuper}
hasChild={true} hasChild={true}
removeClickRow={false} removeClickRow={false}

View File

@ -285,6 +285,33 @@ const Settings = () => {
</Container> </Container>
</div> </div>
<div className={classes.root}>
<Container className={classes.container} maxWidth="sm">
<Paper className={classes.paper}>
<Typography variant="body1">
Modulo campanha
</Typography>
<Select
margin="dense"
variant="outlined"
native
id="hasCampaign-setting"
name="hasCampaign"
value={
settings &&
settings.length > 0 &&
getSettingValue('hasCampaign')
}
className={classes.settingOption}
onChange={handleChangeSetting}
>
<option value="enabled">Ativado</option>
<option value="disabled">Desativado</option>
</Select>
</Paper>
</Container>
</div>
</div> </div>
)} )}

View File

@ -1,82 +1,82 @@
import React, { useState, useEffect, useReducer, useContext} from "react"; import React, { useState, useEffect, useReducer, useContext } from "react"
import { toast } from "react-toastify"; import { toast } from "react-toastify"
import openSocket from "socket.io-client"; import openSocket from "socket.io-client"
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles"
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper"
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button"
import Table from "@material-ui/core/Table"; import Table from "@material-ui/core/Table"
import TableBody from "@material-ui/core/TableBody"; import TableBody from "@material-ui/core/TableBody"
import TableCell from "@material-ui/core/TableCell"; import TableCell from "@material-ui/core/TableCell"
import TableHead from "@material-ui/core/TableHead"; import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"; import TableRow from "@material-ui/core/TableRow"
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton"
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search"
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField"
import InputAdornment from "@material-ui/core/InputAdornment"; import InputAdornment from "@material-ui/core/InputAdornment"
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"; import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"
import EditIcon from "@material-ui/icons/Edit"; import EditIcon from "@material-ui/icons/Edit"
import MainContainer from "../../components/MainContainer"; import MainContainer from "../../components/MainContainer"
import MainHeader from "../../components/MainHeader"; import MainHeader from "../../components/MainHeader"
import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper"; import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper"
import Title from "../../components/Title"; import Title from "../../components/Title"
import api from "../../services/api"; import api from "../../services/api"
import { i18n } from "../../translate/i18n"; import { i18n } from "../../translate/i18n"
import TableRowSkeleton from "../../components/TableRowSkeleton"; import TableRowSkeleton from "../../components/TableRowSkeleton"
import UserModal from "../../components/UserModal"; import UserModal from "../../components/UserModal"
import ConfirmationModal from "../../components/ConfirmationModal"; import ConfirmationModal from "../../components/ConfirmationModal"
import toastError from "../../errors/toastError"; import toastError from "../../errors/toastError"
//-------- //--------
import { AuthContext } from "../../context/Auth/AuthContext"; import { AuthContext } from "../../context/Auth/AuthContext"
import { Can } from "../../components/Can"; import { Can } from "../../components/Can"
const reducer = (state, action) => { const reducer = (state, action) => {
if (action.type === "LOAD_USERS") { if (action.type === "LOAD_USERS") {
const users = action.payload; const users = action.payload
const newUsers = []; const newUsers = []
users.forEach((user) => { users.forEach((user) => {
const userIndex = state.findIndex((u) => u.id === user.id); const userIndex = state.findIndex((u) => u.id === user.id)
if (userIndex !== -1) { if (userIndex !== -1) {
state[userIndex] = user; state[userIndex] = user
} else { } else {
newUsers.push(user); newUsers.push(user)
} }
}); })
return [...state, ...newUsers]; return [...state, ...newUsers]
} }
if (action.type === "UPDATE_USERS") { if (action.type === "UPDATE_USERS") {
const user = action.payload; const user = action.payload
const userIndex = state.findIndex((u) => u.id === user.id); const userIndex = state.findIndex((u) => u.id === user.id)
if (userIndex !== -1) { if (userIndex !== -1) {
state[userIndex] = user; state[userIndex] = user
return [...state]; return [...state]
} else { } else {
return [user, ...state]; return [user, ...state]
} }
} }
if (action.type === "DELETE_USER") { if (action.type === "DELETE_USER") {
const userId = action.payload; const userId = action.payload
const userIndex = state.findIndex((u) => u.id === userId); const userIndex = state.findIndex((u) => u.id === userId)
if (userIndex !== -1) { if (userIndex !== -1) {
state.splice(userIndex, 1); state.splice(userIndex, 1)
} }
return [...state]; return [...state]
} }
if (action.type === "RESET") { if (action.type === "RESET") {
return []; return []
} }
}; }
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
mainPaper: { mainPaper: {
@ -85,167 +85,229 @@ const useStyles = makeStyles((theme) => ({
overflowY: "scroll", overflowY: "scroll",
...theme.scrollbarStyles, ...theme.scrollbarStyles,
}, },
})); }))
const Users = () => { const Users = () => {
const classes = useStyles(); const classes = useStyles()
//-------- //--------
const { user: userA } = useContext(AuthContext); const { user: userA } = useContext(AuthContext)
const [loading, setLoading] = useState(false)
const [pageNumber, setPageNumber] = useState(1)
const [hasMore, setHasMore] = useState(false)
const [selectedUser, setSelectedUser] = useState(null)
const [deletingUser, setDeletingUser] = useState(null)
const [userModalOpen, setUserModalOpen] = useState(false)
const [confirmModalOpen, setConfirmModalOpen] = useState(false)
const [searchParam, setSearchParam] = useState("")
const [users, dispatch] = useReducer(reducer, [])
const [settings, setSettings] = useState([])
const [loading, setLoading] = useState(false);
const [pageNumber, setPageNumber] = useState(1);
const [hasMore, setHasMore] = useState(false);
const [selectedUser, setSelectedUser] = useState(null);
const [deletingUser, setDeletingUser] = useState(null);
const [userModalOpen, setUserModalOpen] = useState(false);
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
const [searchParam, setSearchParam] = useState("");
const [users, dispatch] = useReducer(reducer, []);
useEffect(() => { useEffect(() => {
dispatch({ type: "RESET" }); dispatch({ type: "RESET" })
setPageNumber(1); setPageNumber(1)
}, [searchParam]); }, [searchParam])
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true)
const delayDebounceFn = setTimeout(() => { const delayDebounceFn = setTimeout(() => {
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
const { data } = await api.get("/users/", { const { data } = await api.get("/users/", {
params: { searchParam, pageNumber }, params: { searchParam, pageNumber },
}); })
dispatch({ type: "LOAD_USERS", payload: data.users }); dispatch({ type: "LOAD_USERS", payload: data.users })
setHasMore(data.hasMore); setHasMore(data.hasMore)
setLoading(false); setLoading(false)
} catch (err) { } catch (err) {
toastError(err); toastError(err)
} }
}; }
fetchUsers(); fetchUsers()
}, 500); }, 500)
return () => clearTimeout(delayDebounceFn); return () => clearTimeout(delayDebounceFn)
}, [searchParam, pageNumber]); }, [searchParam, pageNumber])
useEffect(() => { useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL); const delayDebounceFn = setTimeout(() => {
const fetchSession = async () => {
try {
try {
const { data } = await api.get('/settings')
setSettings(data.settings)
} catch (err) {
toastError(err)
}
} catch (err) {
toastError(err)
}
}
fetchSession()
}, 500)
return () => clearTimeout(delayDebounceFn)
}, [])
const getSettingValue = (key) => {
return settings?.find((s) => s?.key === key)?.value
// const { value } = settings.find((s) => s?.key === key)
// return value
}
useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL)
socket.on("user", (data) => { socket.on("user", (data) => {
if (data.action === "update" || data.action === "create") { if (data.action === "update" || data.action === "create") {
dispatch({ type: "UPDATE_USERS", payload: data.user }); dispatch({ type: "UPDATE_USERS", payload: data.user })
} }
if (data.action === "delete") { if (data.action === "delete") {
dispatch({ type: "DELETE_USER", payload: +data.userId }); dispatch({ type: "DELETE_USER", payload: +data.userId })
} }
}); })
socket.on('settings', (data) => {
if (data.action === 'update') {
setSettings((prevState) => {
const aux = [...prevState]
const settingIndex = aux.findIndex((s) => s.key === data.setting.key)
aux[settingIndex].value = data.setting.value
return aux
})
}
})
return () => { return () => {
socket.disconnect(); socket.disconnect()
}; }
}, []); }, [])
const handleOpenUserModal = () => { const handleOpenUserModal = () => {
setSelectedUser(null); setSelectedUser(null)
setUserModalOpen(true); setUserModalOpen(true)
}; }
const handleCloseUserModal = () => { const handleCloseUserModal = () => {
setSelectedUser(null); setSelectedUser(null)
setUserModalOpen(false); setUserModalOpen(false)
}; }
const handleSearch = (event) => { const handleSearch = (event) => {
setSearchParam(event.target.value.toLowerCase()); setSearchParam(event.target.value.toLowerCase())
}; }
const handleEditUser = (user) => { const handleEditUser = (user) => {
setSelectedUser(user); setSelectedUser(user)
setUserModalOpen(true); setUserModalOpen(true)
}; }
const handleDeleteUser = async (userId) => { const handleDeleteUser = async (userId) => {
try { try {
await api.delete(`/users/${userId}`); await api.delete(`/users/${userId}`)
toast.success(i18n.t("users.toasts.deleted")); toast.success(i18n.t("users.toasts.deleted"))
} catch (err) { } catch (err) {
toastError(err); toastError(err)
} }
setDeletingUser(null); setDeletingUser(null)
setSearchParam(""); setSearchParam("")
setPageNumber(1); setPageNumber(1)
}; }
const loadMore = () => { const loadMore = () => {
setPageNumber((prevState) => prevState + 1); setPageNumber((prevState) => prevState + 1)
}; }
const handleScroll = (e) => { const handleScroll = (e) => {
if (!hasMore || loading) return; if (!hasMore || loading) return
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; const { scrollTop, scrollHeight, clientHeight } = e.currentTarget
if (scrollHeight - (scrollTop + 100) < clientHeight) { if (scrollHeight - (scrollTop + 100) < clientHeight) {
loadMore(); loadMore()
} }
}; }
console.log('userA.profile: ', userA.profile)
return ( return (
<Can <Can
role={userA.profile} role={userA.profile}
perform="user-view:show" perform="user-view:show"
yes={() => ( yes={() => (
<MainContainer> <MainContainer>
<ConfirmationModal <ConfirmationModal
title={ title={
deletingUser && deletingUser &&
`${i18n.t("users.confirmationModal.deleteTitle")} ${ `${i18n.t("users.confirmationModal.deleteTitle")} ${deletingUser.name
deletingUser.name }?`
}?` }
} open={confirmModalOpen}
open={confirmModalOpen} onClose={setConfirmModalOpen}
onClose={setConfirmModalOpen} onConfirm={() => handleDeleteUser(deletingUser.id)}
onConfirm={() => handleDeleteUser(deletingUser.id)} >
> {i18n.t("users.confirmationModal.deleteMessage")}
{i18n.t("users.confirmationModal.deleteMessage")} </ConfirmationModal>
</ConfirmationModal> <UserModal
<UserModal open={userModalOpen}
open={userModalOpen} onClose={handleCloseUserModal}
onClose={handleCloseUserModal} aria-labelledby="form-dialog-title"
aria-labelledby="form-dialog-title" userId={selectedUser && selectedUser.id}
userId={selectedUser && selectedUser.id} />
/> <MainHeader>
<MainHeader> <Title>{i18n.t("users.title")}</Title>
<Title>{i18n.t("users.title")}</Title> <MainHeaderButtonsWrapper>
<MainHeaderButtonsWrapper> <TextField
<TextField placeholder={i18n.t("contacts.searchPlaceholder")}
placeholder={i18n.t("contacts.searchPlaceholder")} type="search"
type="search" value={searchParam}
value={searchParam} onChange={handleSearch}
onChange={handleSearch} InputProps={{
InputProps={{ startAdornment: (
startAdornment: ( <InputAdornment position="start">
<InputAdornment position="start"> <SearchIcon style={{ color: "gray" }} />
<SearchIcon style={{ color: "gray" }} /> </InputAdornment>
</InputAdornment> ),
), }}
}} />
/>
<Can
role={userA.profile} {
perform="btn-add-user" (getSettingValue('userCreation') === 'enabled' || userA.profile === 'master') && (
yes={() => ( <Button
<Button variant="contained"
variant="contained" color="primary"
color="primary" onClick={handleOpenUserModal}
onClick={handleOpenUserModal} >
> {i18n.t("users.buttons.add")}
{i18n.t("users.buttons.add")} </Button>
</Button> )
)} }
/>
{/* <Can
role={userA.profile}
perform="btn-add-user"
yes={() => (
<Button
variant="contained"
color="primary"
onClick={handleOpenUserModal}
>
{i18n.t("users.buttons.add")}
</Button>
)}
/> */}
@ -267,7 +329,9 @@ const Users = () => {
<TableCell align="center"> <TableCell align="center">
{i18n.t("users.table.profile")} {i18n.t("users.table.profile")}
</TableCell> </TableCell>
<TableCell align="center">
Cargo
</TableCell>
<TableCell align="center"> <TableCell align="center">
{i18n.t("users.table.actions")} {i18n.t("users.table.actions")}
</TableCell> </TableCell>
@ -281,6 +345,7 @@ const Users = () => {
<TableCell align="center">{user.name}</TableCell> <TableCell align="center">{user.name}</TableCell>
<TableCell align="center">{user.email}</TableCell> <TableCell align="center">{user.email}</TableCell>
<TableCell align="center">{user.profile}</TableCell> <TableCell align="center">{user.profile}</TableCell>
<TableCell align="center">{user.positionCompany}</TableCell>
<TableCell align="center"> <TableCell align="center">
@ -292,37 +357,37 @@ const Users = () => {
</IconButton> </IconButton>
<Can <Can
role={userA.profile} role={userA.profile}
perform="icon-remove-user" perform="icon-remove-user"
yes={() => ( yes={() => (
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { onClick={(e) => {
setConfirmModalOpen(true); setConfirmModalOpen(true)
setDeletingUser(user); setDeletingUser(user)
}} }}
> >
<DeleteOutlineIcon /> <DeleteOutlineIcon />
</IconButton> </IconButton>
)} )}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
{loading && <TableRowSkeleton columns={4} />} {loading && <TableRowSkeleton columns={4} />}
</> </>
</TableBody> </TableBody>
</Table> </Table>
</Paper> </Paper>
</MainContainer> </MainContainer>
)} )}
/> />
); )
}; }
export default Users; export default Users

View File

@ -16,6 +16,7 @@ const rules = {
admin: { admin: {
static: [ static: [
'show-icon-add-queue',
'show-icon-edit-whatsapp', 'show-icon-edit-whatsapp',
'show-icon-edit-queue', 'show-icon-edit-queue',
'menu-users:view', 'menu-users:view',
@ -31,6 +32,7 @@ const rules = {
'queues-view:show', 'queues-view:show',
'user-view:show', 'user-view:show',
'ticket-report:show', 'ticket-report:show',
'btn-add-user'
], ],
}, },