Compare commits

..

80 Commits

Author SHA1 Message Date
adriano d6ec5972a5 feat: redundant role removal and service import 2024-04-03 15:04:51 -03:00
adriano 20a45b6210 Merge branch 'el_lojas_melhorias' into integracao_wa_oficial_sos_el 2024-04-03 15:03:28 -03:00
adriano a6d800f17a Merge branch 'el_lojas_melhorias' of github.com:AdrianoRobson/projeto-hit into el_lojas_melhorias 2024-04-03 14:11:01 -03:00
adriano a5657d0a2f feat: Enable reception of multiple vCards sent via WhatsApp 2024-04-03 14:10:53 -03:00
willian-pessoa 024c6920af Merge branch 'el_lojas_melhorias' of https://github.com/AdrianoRobson/projeto-hit into el_lojas_melhorias 2024-04-02 17:21:53 -03:00
willian-pessoa d827e72b7c refactor: grafico pizza, melhorias responsividade 2024-04-02 17:11:54 -03:00
adriano a33ce21f44 feat: Update to support queueId or contact_from as parameter to create ticket remote 2024-04-02 15:33:34 -03:00
adriano 7df322d1ab feat: Update controller to use queueId assigned to a WhatsApp number for message sending in remote ticket creation 2024-04-02 11:59:44 -03:00
adriano 88a71e5758 fix: Saturday message not matching with database enabled/disabled 2024-04-01 17:51:45 -03:00
adriano 4b9dca2401 fix: Bug when send remote message 2024-04-01 17:06:11 -03:00
adriano 90e9e311c3 fix: Correct text of start date in frontend 2024-04-01 09:17:42 -03:00
adriano ce7385602f Feet: Get time from Frontend in Piechart 2024-04-01 09:09:59 -03:00
adriano e6ea004edf fix: Piechart bug startDate endDate 2024-04-01 09:07:49 -03:00
adriano 8a35b985b9 git commit -m "feat: Pull information from closed tickets in backend to feed pie chart in frontend and add option to change service hours on Saturday" 2024-04-01 09:00:15 -03:00
adriano a96d6f26c7 chore: update Piechart tickets name 2024-03-28 14:28:34 -03:00
adriano 76929c41ec feat: PieChart data ploted from backend 2024-03-28 14:12:30 -03:00
adriano 5e81f02116 Merge branch 'el_lojas_melhorias' of github.com:AdrianoRobson/projeto-hit into el_lojas_melhorias 2024-03-28 12:21:17 -03:00
adriano c27770ef02 fix: Improve ticket query in backend to resolve high delay issue 2024-03-28 12:20:55 -03:00
willian-pessoa b05cd7d1d6 Merge branch 'el_lojas_melhorias' of https://github.com/AdrianoRobson/projeto-hit into el_lojas_melhorias 2024-03-27 17:52:34 -03:00
willian-pessoa 1a7077feaf feat: grafico de pizza 2024-03-27 17:52:20 -03:00
adriano dc5a6945d2 feat: Modify backend and frontend to display waiting time for customer service in report and update report controller queries accordingly 2024-03-27 12:02:11 -03:00
adriano 9f040ce953 feat: Improve display of queue and WhatsApp number-based interaction report 2024-03-25 16:00:24 -03:00
adriano 8447628fbf feat: Create routes and controllers for generating reports by WhatsApp numbers and queues in the backend, and update frontend for report visualization 2024-03-21 18:33:00 -03:00
adriano d298f75efc Merge branch 'el_lojas_melhorias' of github.com:AdrianoRobson/projeto-hit into el_lojas_melhorias 2024-03-21 09:21:01 -03:00
adriano 84896ec09c git commit -m "fix: Correct marking of bot messages from Omnihit application to be ignored upon return. Updated the frontend to chaining the notify data " 2024-03-21 09:16:31 -03:00
gustavo-gsp 9b6e48f54e feat: add notification for tickets in waiting status
Details:
- Implemented notification functionality for tickets that are in waiting status.
2024-03-20 14:33:36 -03:00
gustavo-gsp 82863f9d0e Merge branch 'el_lojas_melhorias' of github.com:AdrianoRobson/projeto-hit into el_lojas_melhorias 2024-03-20 14:17:25 -03:00
gustavo-gsp 18d0420949 error correction in notifications for already opened tickets 2024-03-20 14:08:35 -03:00
adriano 1894fd35ab feat: modify reports page
Details:
- Added a new hidden column called "messages" to the tickets table.
- Implemented search functionality based on ticket messages.
2024-03-20 11:33:30 -03:00
adriano 52e63706c1 feat: back to bot ura when the user is waiting in a queue 2024-03-18 18:05:59 -03:00
adriano e80ed8d092 Merge branch 'master' of github.com:AdrianoRobson/projeto-hit 2024-03-18 15:21:42 -03:00
adriano 5572ca1223 Merge branch 'feat-iam-integration' into feat_lojas 2024-03-18 15:19:53 -03:00
adriano db4bf2bf84 feat: Add routes and controllers to integrate with E-Trust Horacius for user and application management 2024-03-18 15:18:18 -03:00
gustavo-gsp 64d2cf8323
Merge pull request #22 from gustavo-gsp/feat_lojas
Feat lojas
2024-03-18 15:09:42 -03:00
gustavo-gsp 7533a382cc
Update server.js 2024-03-18 15:07:47 -03:00
Adriano d7019ffb1a feat: add notification and sound alert for new tickets in user's service queue
Details:
- Implemented functionality to notify users with a sound alert when new tickets arrive in their service queues.

fix: include responsible person's name when generating ticket already opened error in all languages

Details:
- Fixed the error generation process to include the name of the responsible person for the ticket when the system detects an attempt to create an already opened ticket.
2024-03-18 14:51:13 -03:00
Adriano e864e0b97f Merge branch 'master' of github.com:AdrianoRobson/projeto-hit 2024-03-15 16:36:41 -03:00
adriano 0cc027252d feat(ura): Added option to return to the previous submenu and main menu
Fixed URAs context to prevent users from receiving information from a menu that is not currently in the context.
2024-03-15 11:59:32 -03:00
adriano 75b14becc3 feat: Modify ticket querying process to prioritize querying by ticket IDs before retrieving messages 2024-03-14 12:10:26 -03:00
adriano 5bcfc1bdcc feat: Implement querying of synthetic and analytical reports on tickets 2024-03-13 18:16:56 -03:00
adriano 5366f9943b chore: removed test code 2024-03-13 11:25:00 -03:00
adriano d47a36d8b2 feat: Allow identifying messages sent by the agent and the IVR 2024-03-13 11:19:40 -03:00
Henrriky 37c384a712 chore: update md files 2024-03-12 17:19:16 -03:00
Henrriky 895ce83b5b feat: add reset password service 2024-03-12 17:18:41 -03:00
Henrriky d608538c9e feat: Add API routes for integrating Horacius users with Omnihit 2024-03-12 17:18:22 -03:00
Henrriky 2f56673962 refactor: update name of functions link and unlink user right 2024-03-12 17:16:46 -03:00
Henrriky fca4dd7036 feat: add unlink user right to change profile property of user to default 2024-03-12 17:16:07 -03:00
Henrriky 2d22a4b9f0 feat: add link user right to modify profile property of user 2024-03-12 17:15:24 -03:00
Henrriky 097737a3b8 feat: add check user right service 2024-03-12 17:11:26 -03:00
Henrriky 26e90c6ea9 feat: add middleware to verify token (API KEY) send by horacius system 2024-03-12 17:09:56 -03:00
Henrriky 3478b7c5b2 feat: add iam controllers (createUser, checkUser, deleteUser, updateUser, resetPassword, linkUserAndUserRight, unlinkUserAndUserRight, checkUserRight) 2024-03-12 17:06:39 -03:00
Henrriky 4c7e49fb9a feat: add base structure to EL-IAM integration 2024-03-12 12:46:12 -03:00
adriano 279c4697dd fix: save contact by remote whatsapp session 2024-03-07 19:38:22 -03:00
adriano e020a5f75d fix: Update frontend to indicate lack of WhatsApp connection and adjust backend to return 422 status for WhatsApp number validation issues 2024-03-07 18:03:46 -03:00
adriano c5c5ddb5a4 fix: bug id and body undefined from webhook 2024-03-07 15:25:22 -03:00
adriano ec6e84f567 feat: Add function in controller to remove whatsapp cache in Redis 2024-03-07 11:53:50 -03:00
adriano 9d4b80986d feat: Add function in controller to remove user and update cache in Redis 2024-03-07 11:47:42 -03:00
adriano 8e24f37219 fix: bug ignored wrong number connected in the application 2024-03-07 09:43:04 -03:00
adriano 2fc732eec1 feat: Implement Redis solution for querying WhatsApp number in webhook controller 2024-03-06 18:14:12 -03:00
adriano 943d121ab1 fix: Correct bug in editing official WhatsApp and implement solution to validate number using remote WhatsApp API 2024-03-06 11:00:03 -03:00
Adriano e5dff2a6d5 Merge branch 'master' of github.com:AdrianoRobson/projeto-hit 2024-03-04 16:46:25 -03:00
adriano 6c5b89fd28 feat: Add new function to clear user cache and include phone number field for official WhatsApp 2024-03-04 15:53:50 -03:00
adriano 8ca5b4503a fix: Correct search queries in the report and fix bugs in WhatsApp template sending 2024-03-01 17:47:18 -03:00
adriano c4eda756eb fix: Prevent loading all tickets when status is 'closed' 2024-02-28 11:18:48 -03:00
adriano 47ed80e1a9 feat: Update report to allow querying by creation date, update date, and service queue 2024-02-28 10:48:36 -03:00
adriano c9905eefb7 feat: Update to display total number of pending and waiting tickets 2024-02-26 16:58:39 -03:00
adriano 237fa8124e feat: Update application to receive vecard from the client 2024-02-26 11:18:01 -03:00
adriano 1d3a6178ba feat: Improve code for transferring users to another agent 2024-02-23 14:36:52 -03:00
adriano 05dd0e60c0 chore: Remove frontend comment 2024-02-22 15:30:43 -03:00
adriano 7fc0f136ff fix: Fix bug in ticket transfer from selected user's queue by agent 2024-02-22 15:20:26 -03:00
adriano d25d296498 feat: Update response message for clarity 2024-02-21 18:10:30 -03:00
Adriano 60b69db0c5 Merge branch 'master' of github.com:AdrianoRobson/projeto-hit 2024-02-21 17:52:31 -03:00
adriano 9ff95ff4bd feat: Add route for creating tickets from external endpoints and update authentication middleware 2024-02-21 17:47:23 -03:00
Adriano 939a91e573 Merge branch 'master' of github.com:AdrianoRobson/projeto-hit 2024-02-21 11:25:32 -03:00
adriano 5abab2ef05 feat: Optimize user search via Redis for backend access 2024-02-21 11:23:51 -03:00
Adriano 9e62f5d5e8 Merge branch 'master' of github.com:AdrianoRobson/projeto-hit 2024-02-21 09:21:15 -03:00
Adriano 7f52466c50 chore: change frontend port 2024-02-21 09:21:02 -03:00
adriano 9370931737 fix: infinite transfer message send to client 2024-02-21 09:17:12 -03:00
adriano db140a328c feat: Enable direct transfer of tickets to agent via WhatsApp user input 2024-02-20 12:33:36 -03:00
adriano 18660b6947 feat: Update frontend to display static application name 2024-02-19 15:05:19 -03:00
84 changed files with 4530 additions and 1022 deletions

3
.gitignore vendored
View File

@ -40,7 +40,10 @@ WWebJS
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
.env.save
nano.save
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*

View File

@ -183,11 +183,17 @@ socketIo.on('connect_error', async function (err) {
}) })
// //
const wwebVersion = '2.2402.5';
//NOVA OPÇÃO MD //NOVA OPÇÃO MD
client = new Client({ client = new Client({
authStrategy: new LocalAuth({ clientId: 'omnihit_sesssion' }), authStrategy: new LocalAuth({ clientId: 'omnihit_sesssion' }),
puppeteer: { args: ['--no-sandbox', '--disable-setuid-sandbox'], executablePath: process.env.CHROME_BIN || '/usr/bin/google-chrome-stable' }, puppeteer: { args: ['--no-sandbox', '--disable-setuid-sandbox'], executablePath: process.env.CHROME_BIN || '/usr/bin/google-chrome-stable' },
webVersionCache: {
type: 'remote',
remotePath: `https://raw.githubusercontent.com/wppconnect-team/wa-version/main/html/${wwebVersion}.html`,
},
}) })
client.initialize() client.initialize()

View File

@ -20,12 +20,18 @@ import {
} from "../helpers/ContactsCache"; } from "../helpers/ContactsCache";
import { off } from "process"; import { off } from "process";
import GetContactService from "../services/ContactServices/GetContactService"
type IndexQuery = { type IndexQuery = {
searchParam: string; searchParam: string;
pageNumber: string; pageNumber: string;
}; };
type IndexGetContactQuery = {
name: string;
number: string;
};
interface ExtraInfo { interface ExtraInfo {
name: string; name: string;
value: string; value: string;
@ -84,6 +90,20 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
return res.json({ contacts, count, hasMore }); return res.json({ contacts, count, hasMore });
}; };
export const getContact = async (
req: Request,
res: Response
): Promise<Response> => {
const { name, number } = req.body as IndexGetContactQuery;
const contact = await GetContactService({
name,
number
});
return res.status(200).json(contact);
};
export const store = async (req: Request, res: Response): Promise<Response> => { export const store = async (req: Request, res: Response): Promise<Response> => {
const newContact: ContactData = req.body; const newContact: ContactData = req.body;
newContact.number = newContact.number.replace("-", "").replace(" ", ""); newContact.number = newContact.number.replace("-", "").replace(" ", "");
@ -106,21 +126,12 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
const validNumber = await CheckIsValidContact(newContact.number); const validNumber = await CheckIsValidContact(newContact.number);
// const validNumber: any = await CheckContactNumber(newContact.number)
if (!validNumber) { if (!validNumber) {
throw new AppError("ERR_WAPP_CHECK_CONTACT"); throw new AppError("ERR_WAPP_CHECK_CONTACT");
} }
const profilePicUrl = await GetProfilePicUrl(validNumber); const profilePicUrl = await GetProfilePicUrl(validNumber);
console.log("xxxxxxxxxxx profilePicUrl: ", profilePicUrl);
// console.log(`newContact.name: ${newContact.name}\n
// newContact.number: ${newContact.number}\n
// newContact.email: ${newContact.email}\n
// newContact.extraInfo: ${newContact.extraInfo}`)
let name = newContact.name; let name = newContact.name;
let number = validNumber; let number = validNumber;
let email = newContact.email; let email = newContact.email;

View File

@ -0,0 +1,494 @@
import { Request, Response } from "express";
import { getIO } from "../libs/socket";
import { Op } from "sequelize";
import CreateUserService from "../services/UserServices/CreateUserService";
import UpdateUserService from "../services/UserServices/UpdateUserService";
import DeleteUserService from "../services/UserServices/DeleteUserService";
import { del, get, set } from "../helpers/RedisClient";
import {
startWhoIsOnlineMonitor,
stopWhoIsOnlineMonitor
} from "../helpers/WhoIsOnlineMonitor";
import User from "../models/User";
export const createUser = async (
req: Request,
res: Response
): Promise<Response> => {
const { user_id, user_first_name, user_tax_id, user_email, user_title }: any =
req.body;
const invalid = invalidProperties(req.body, [
"user_id",
"user_tax_id",
"user_first_name"
]);
if (invalid) {
return res.status(400).json(response("1", `${invalid}`, "0", "createUser"));
}
const auxUser = await User.findOne({ where: { secondaryId: user_id } });
if (auxUser) {
return res
.status(400)
.json(
response("1", `The user ${user_id} already exist`, "0", "createUser")
);
}
const user = await CreateUserService({
email: user_tax_id || user_email,
password: "12345",
name: user_first_name,
positionCompany: user_title,
profile: "user",
ignoreThrow: true
});
if (user?.error) {
return res
.status(user?.status)
.json(response("0", `${user?.msg}`, "0", "createUser"));
}
if (!user?.error) {
const _user = await User.findByPk(user.id);
_user?.update({ secondaryId: user_id });
const { id, name } = user;
await set(`user:${id}`, { id, name });
const io = getIO();
io.emit("user", {
action: "create",
user
});
await startWhoIsOnlineMonitor();
}
return res
.status(200)
.json(response("1", `User ${user_id} created`, "1", "createUser"));
};
export const deleteUser = async (
req: Request,
res: Response
): Promise<Response> => {
const { user_id }: any = req.body;
const invalid = invalidProperties(req.body, ["user_id"]);
if (invalid) {
return res.status(400).json(response("1", `${invalid}`, "0", "deleteUser"));
}
const _user = await User.findOne({ where: { secondaryId: user_id } });
if (_user) {
const user = await DeleteUserService(_user.id, true);
if (user?.error) {
return res
.status(user?.status)
.json(response("0", `${user?.msg}`, "0", "deleteUser"));
}
if (!user?.error) {
del(`user:${_user.id}`);
const io = getIO();
io.emit("user", {
action: "delete",
userId: _user.id
});
await stopWhoIsOnlineMonitor();
io.emit("onlineStatus", {
action: "delete",
userOnlineTime: _user.id
});
await startWhoIsOnlineMonitor();
return res
.status(200)
.json(response("1", `User ${user_id} deleted`, "1", "deleteUser"));
}
}
return res
.status(500)
.json(response("0", "Internal server error", "0", "deleteUser"));
};
export const listAllUsers = async (
req: Request,
res: Response
): Promise<Response> => {
const _users: any = await User.findAll({
where: {
secondaryId: {
[Op.ne]: ""
}
},
attributes: ["secondaryId", "name"]
});
if (_users) {
const user_list = _users.map((user: any) => {
const { secondaryId, name } = user;
return { user_id: secondaryId, full_name: name };
});
return res
.status(200)
.json(response("1", "Success", user_list, "listAllUsers"));
}
return res
.status(500)
.json(response("0", "Internal server error", [], "listAllUsers"));
};
export const checkUser = async (
req: Request,
res: Response
): Promise<Response> => {
const { user_id }: any = req.body;
const invalid = invalidProperties(req.body, ["user_id"]);
if (invalid) {
return res.status(400).json(response("1", `${invalid}`, "0", "checkUser"));
}
const _user = await User.findOne({ where: { secondaryId: user_id } });
if (_user) {
return res
.status(200)
.json(response("1", `User ${user_id} exist`, "1", "checkUser"));
}
return res
.status(404)
.json(response("1", `User ${user_id} not exist`, "0", "checkUser"));
};
export const updateUser = async (
req: Request,
res: Response
): Promise<Response> => {
const { user_id, user_first_name, user_tax_id, user_email, user_title }: any =
req.body;
const invalid = invalidProperties(req.body, ["user_id"]);
if (invalid) {
return res.status(400).json(response("1", `${invalid}`, "0", "checkUser"));
}
const _user: any = await User.findOne({ where: { secondaryId: user_id } });
if (!_user)
return res
.status(404)
.json(response("1", `User ${user_id} not exist`, "0", "updateUser"));
const userData = {
email: user_tax_id || user_email,
name: user_first_name,
positionCompany: user_title
};
let user: any = await UpdateUserService({
userData,
userId: _user.id,
ignoreThrow: true
});
if (user?.error) {
return res
.status(user?.status)
.json(response("0", `${user?.msg}`, "0", "updateUser"));
}
if (user) {
const { id, name } = user;
await set(`user:${id}`, { id, name });
}
const io = getIO();
io.emit("user", {
action: "update",
user
});
return res
.status(200)
.json(response("1", `User ${user_id} updated`, "1", "updateUser"));
};
export const resetPassword = async (
req: Request,
res: Response
): Promise<Response> => {
const { user_id, user_password }: any = req.body;
const invalid = invalidProperties(req.body, ["user_id", "user_password"]);
if (invalid) {
return res
.status(400)
.json(response("1", `${invalid}`, "0", "resetPassword"));
}
const _user = await User.findOne({ where: { secondaryId: user_id } });
if (!_user) {
return res
.status(404)
.json(response("1", `User ${user_id} not exist`, "0", "resetPassword"));
}
const userData = {
password: user_password,
email: _user.email
};
let user: any = await UpdateUserService({
userData,
userId: _user.id,
ignoreThrow: true
});
if (user?.error) {
return res
.status(user?.status)
.json(response("0", `${user?.msg}`, "0", "resetPassword"));
}
await logoutUser(_user.id);
return res
.status(200)
.json(
response("1", `User ${user_id} password updated`, "1", "resetPassword")
);
};
export const linkUserAndUserRight = async (
req: Request,
res: Response
): Promise<Response> => {
const { user_id, user_right_id, user_right_title }: any = req.body;
const invalid = invalidProperties(req.body, ["user_id", "user_right_id"]);
if (invalid) {
return res
.status(400)
.json(response("1", `${invalid}`, "0", "linkUserAndUserRight"));
}
if (
(user_right_id &&
!["admin", "user", "supervisor"].includes(
user_right_id?.trim().toLocaleLowerCase()
)) ||
(user_right_title &&
!["admin", "user", "supervisor"].includes(
user_right_title?.trim().toLocaleLowerCase()
))
) {
return res
.status(400)
.json(
response(
"1",
`The user profile ${
user_right_title || user_right_id
} provided by the property user_right_title or user_right_id does not match the following profiles: admin, user, supervisor`,
"0",
"linkUserAndUserRight"
)
);
}
const _user: any = await User.findOne({ where: { secondaryId: user_id } });
if (!_user)
return res
.status(404)
.json(
response("1", `User ${user_id} not exist`, "0", "linkUserAndUserRight")
);
const userData = {
profile: user_right_title || user_right_id,
email: _user.email
};
let user: any = await UpdateUserService({
userData,
userId: _user.id,
ignoreThrow: true
});
if (user?.error) {
return res
.status(user?.status)
.json(response("0", `${user?.msg}`, "0", "linkUserAndUserRight"));
}
await logoutUser(_user.id);
return res
.status(200)
.json(
response(
"1",
`User ${user_id} associated with ${
user_right_title || user_right_id
} profile`,
"1",
"linkUserAndUserRight"
)
);
};
export const checkUserRight = async (
req: Request,
res: Response
): Promise<Response> => {
const { user_id, user_right_id, user_right_title }: any = req.body;
const invalid = invalidProperties(req.body, ["user_id", "user_right_id"]);
if (invalid) {
return res
.status(400)
.json(response("1", `${invalid}`, "0", "checkUserRight"));
}
if (
(user_right_id &&
!["admin", "user", "supervisor"].includes(
user_right_id?.trim().toLocaleLowerCase()
)) ||
(user_right_title &&
!["admin", "user", "supervisor"].includes(
user_right_title?.trim().toLocaleLowerCase()
))
) {
return res
.status(400)
.json(
response(
"1",
`The user profile ${
user_right_title || user_right_id
} provided by the property user_right_title or user_right_id does not match the following profiles: admin, user, supervisor`,
"0",
"checkUserRight"
)
);
}
const _user: any = await User.findOne({
where: {
secondaryId: user_id
}
});
if (!_user)
return res
.status(404)
.json(response("1", `User ${user_id} not exist`, "0", "checkUserRight"));
if (
(user_right_id && _user.profile != user_right_id) ||
(user_right_title && _user.profile != user_right_title)
) {
return res
.status(403)
.json(
response(
"1",
`User ${user_id} does not have this profile`,
"0",
"checkUserRight"
)
);
}
return res
.status(200)
.json(
response(
"1",
`User ${user_id} has ${user_right_title || user_right_id} profile`,
"1",
"checkUserRight"
)
);
};
async function logoutUser(userId: any) {
await stopWhoIsOnlineMonitor();
let onlineTime = {
userId: `${userId}`,
status: "logout..."
};
const io = getIO();
io.emit("onlineStatus", {
action: "logout",
userOnlineTime: onlineTime
});
await startWhoIsOnlineMonitor();
}
function response(code: string, msg: string, obj: any, type: string) {
let payload = { return_code: code, return_msg: msg };
switch (type) {
case "createUser":
return { ...payload, user_created: obj };
case "deleteUser":
return { ...payload, user_removed: obj };
case "listAllUsers":
return { ...payload, user_list: obj };
case "checkUser":
return { ...payload, user_exists: obj };
case "updateUser":
return { ...payload, user_updated: obj };
case "resetPassword":
return { ...payload, password_set: obj };
case "linkUserAndUserRight":
return { ...payload, user_right_linked: obj };
case "checkUserRight":
return { ...payload, user_right_exists: obj };
default:
return payload;
}
}
function invalidProperties(body: any, pros: any[]) {
for (const field of pros) {
console.log("body[field]: ", body[field], " field: ", field);
if (!body[field]) {
return `${field} is required`;
}
}
return false;
}

View File

@ -21,6 +21,7 @@ import sendWhatsAppMessageOfficialAPI from "../helpers/sendWhatsAppMessageOffici
import Whatsapp from "../models/Whatsapp"; import Whatsapp from "../models/Whatsapp";
import checkLastClientMsg24hs from "../helpers/CheckLastClientMsg24hs"; import checkLastClientMsg24hs from "../helpers/CheckLastClientMsg24hs";
import AppError from "../errors/AppError"; import AppError from "../errors/AppError";
import { get } from "../helpers/RedisClient";
type IndexQuery = { type IndexQuery = {
pageNumber: string; pageNumber: string;
@ -99,7 +100,8 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
} }
const name = params.find((p: any) => p?.template_name); const name = params.find((p: any) => p?.template_name);
const { language }: any = params.find((p: any) => p?.language); const { language }: any =
params?.find((p: any) => p?.language) || "pt_BR";
const { template_name } = name; const { template_name } = name;
@ -112,9 +114,10 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
} }
}; };
console.log("TEMPLATE: ", template);
sendWhatsAppMessageOfficialAPI(ticket, body, null, template); sendWhatsAppMessageOfficialAPI(ticket, body, null, template);
console.log("TEMPLATE: ", template);
return res.send(); return res.send();
} }
} catch (error: any) { } catch (error: any) {

View File

@ -5,11 +5,12 @@ import DeleteQueueService from "../services/QueueService/DeleteQueueService";
import ListQueuesService from "../services/QueueService/ListQueuesService"; import ListQueuesService from "../services/QueueService/ListQueuesService";
import ShowQueueService from "../services/QueueService/ShowQueueService"; import ShowQueueService from "../services/QueueService/ShowQueueService";
import UpdateQueueService from "../services/QueueService/UpdateQueueService"; import UpdateQueueService from "../services/QueueService/UpdateQueueService";
import Queue from "../models/Queue" import Queue from "../models/Queue";
import AppError from "../errors/AppError" import AppError from "../errors/AppError";
import { get, set } from "../helpers/RedisClient"; import { del, get, set } from "../helpers/RedisClient";
import { Op } from "sequelize";
import ListWhatsAppsService from "../services/WhatsappService/ListWhatsAppsService";
import Whatsapp from "../models/Whatsapp";
export const index = async (req: Request, res: Response): Promise<Response> => { export const index = async (req: Request, res: Response): Promise<Response> => {
const queues = await ListQueuesService(); const queues = await ListQueuesService();
@ -17,6 +18,58 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
return res.status(200).json(queues); return res.status(200).json(queues);
}; };
export const listQueues = async (
req: Request,
res: Response
): Promise<Response> => {
const whatsapps = await Whatsapp.findAll({
where: {
name: { [Op.ne]: "botqueue" },
number: { [Op.ne]: "" },
phoneNumberId: false
},
attributes: ["number"],
include: [
{
model: Queue,
as: "queues",
attributes: ["id", "name"]
}
]
});
const whats = whatsapps
?.filter((w: any) => w?.queues?.length > 0)
?.map((w: any) => {
const { number, queues } = w;
return {
number,
queues: queues?.map((q: any) => {
const { id, name } = q;
return { id, name };
})
};
});
let _queues: any = [];
for (const w of whats) {
const { queues } = w;
for (const q of queues) {
const { id: queueId, name } = q;
const auxQueue = _queues.findIndex((q: any) => q.queueId == queueId);
if (auxQueue == -1) {
_queues.push({ queueId, name });
}
}
}
return res.status(200).json(_queues);
};
export const store = async (req: Request, res: Response): Promise<Response> => { export const store = async (req: Request, res: Response): Promise<Response> => {
const { name, color, greetingMessage } = req.body; const { name, color, greetingMessage } = req.body;
@ -125,7 +178,7 @@ export const customization = async (
await set("ura", ura); await set("ura", ura);
const _ura = await get("ura"); const _ura = await get({ key: "ura", parse: true });
console.log("_URA: ", _ura); console.log("_URA: ", _ura);
return res.status(200).json({ new_queues }); return res.status(200).json({ new_queues });
@ -164,6 +217,8 @@ export const remove = async (
await DeleteQueueService(queueId); await DeleteQueueService(queueId);
await del(`queue:${queueId}`);
const io = getIO(); const io = getIO();
io.emit("queue", { io.emit("queue", {
action: "delete", action: "delete",

View File

@ -4,35 +4,30 @@ import { Request, Response } from "express";
import AppError from "../errors/AppError"; import AppError from "../errors/AppError";
import ShowTicketReport from "../services/TicketServices/ShowTicketReport"; import ShowTicketReport from "../services/TicketServices/ShowTicketReport";
import ShowMessageReport from "../services/MessageServices/ShowMessageReport"; import ShowMessageReport from "../services/MessageServices/ShowMessageReport";
import onlineUserService from "../services/UserServices/CreateOrUpdateOnlineUserService"; import onlineUserService from "../services/UserServices/CreateOrUpdateOnlineUserService";
import User from "../models/User"; import User from "../models/User";
import Queue from "../models/Queue"; import Queue from "../models/Queue";
import UserOnlineTime from "../models/UserOnlineTime"; import UserOnlineTime from "../models/UserOnlineTime";
import { Op, Sequelize, literal } from "sequelize"; import { Op, Sequelize, literal } from "sequelize";
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";
import { splitDateTime } from "../helpers/SplitDateTime"; import { splitDateTime } from "../helpers/SplitDateTime";
import ListUserOnlineOffline from "../services/UserServices/ListUsersOnlineOfflineService"; import ListUserOnlineOffline from "../services/UserServices/ListUsersOnlineOfflineService";
import ListUserParamiterService from "../services/UserServices/ListUserParamiterService"; import ListUserParamiterService from "../services/UserServices/ListUserParamiterService";
import ShowUserServiceReport from "../services/UserServices/ShowUserServiceReport"; import ShowUserServiceReport from "../services/UserServices/ShowUserServiceReport";
import CountTicketsByUserQueue from "../services/UserServices/CountTicketsByUserQueue"; import CountTicketsByUserQueue from "../services/UserServices/CountTicketsByUserQueue";
import ShowQueuesByUser from "../services/UserServices/ShowQueuesByUser"; import ShowQueuesByUser from "../services/UserServices/ShowQueuesByUser";
// import { filter } from "bluebird";
import { getIO } from "../libs/socket"; import { getIO } from "../libs/socket";
import { Json } from "sequelize/types/lib/utils"; import { Json } from "sequelize/types/lib/utils";
import ReportByNumberQueueService from "../services/ReportServices/ReportByNumberQueueService";
import CountStatusChatEndService from "../services/StatusChatEndService/CountStatusChatEndService";
type IndexQuery = { type IndexQuery = {
userId: string; userId: string;
startDate: string; startDate: string;
endDate: string; endDate: string;
createdOrUpdated: string;
queueId: string;
pageNumber: string; pageNumber: string;
userQueues: []; userQueues: [];
}; };
@ -40,11 +35,12 @@ type IndexQuery = {
type ReportOnQueue = { type ReportOnQueue = {
userId: string; userId: string;
identifier: string; identifier: string;
} };
export const reportUserByDateStartDateEnd = async (req: Request, res: Response): Promise<Response> => {
export const reportUserByDateStartDateEnd = async (
req: Request,
res: Response
): Promise<Response> => {
if ( if (
req.user.profile !== "master" && req.user.profile !== "master" &&
req.user.profile !== "admin" && req.user.profile !== "admin" &&
@ -53,23 +49,42 @@ 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, userQueues } = req.query as IndexQuery const {
userId,
startDate,
endDate,
pageNumber,
userQueues,
createdOrUpdated,
queueId
} = req.query as IndexQuery;
console.log("userId, startDate, endDate, pageNumber: ", userId, startDate, endDate, pageNumber); const { tickets, count, hasMore } = await ShowTicketReport({
userId,
startDate,
endDate,
pageNumber,
createdOrUpdated,
queueId
});
const { tickets, count, hasMore } = await ShowTicketReport({ userId, startDate, endDate, pageNumber }); const queues = await Queue.findAll({ attributes: ["id", "name"] });
// 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, queues });
}; };
export const reportUserService = async (
export const reportUserService = async (req: Request, res: Response): Promise<Response> => { req: Request,
res: Response
if (req.user.profile !== "master" && req.user.profile !== "admin" && req.user.profile !=="supervisor") { ): Promise<Response> => {
if (
req.user.profile !== "master" &&
req.user.profile !== "admin" &&
req.user.profile !== "supervisor"
) {
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 } = req.query as IndexQuery;
// let usersProfile = await ListUserParamiterService({ profile: 'user' }) // let usersProfile = await ListUserParamiterService({ profile: 'user' })
let usersProfile = await ListUserParamiterService({ let usersProfile = await ListUserParamiterService({
@ -77,55 +92,89 @@ export const reportUserService = async (req: Request, res: Response): Promise<Re
raw: true raw: true
}); });
const sumUserOlineTime = await ShowUserServiceReport({ startDate, endDate, userId }); const sumUserOlineTime = await ShowUserServiceReport({
const closedByUser = await ShowUserServiceReport({ startDate, endDate, ticketStatus: 'closed', userId }); startDate,
const openByUser = await ShowUserServiceReport({ startDate, endDate, ticketStatus: 'open', userId }); endDate,
userId
});
const closedByUser = await ShowUserServiceReport({
startDate,
endDate,
ticketStatus: "closed",
userId
});
const openByUser = await ShowUserServiceReport({
startDate,
endDate,
ticketStatus: "open",
userId
});
let dateTime = splitDateTime(new Date(format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ptBR }))) let dateTime = splitDateTime(
const onlineUsers = await ListUserOnlineOffline({ date: dateTime.fullDate }) new Date(format(new Date(), "yyyy-MM-dd HH:mm:ss", { locale: ptBR }))
);
const onlineUsers = await ListUserOnlineOffline({ date: dateTime.fullDate });
const openByUserOnQueue = await CountTicketsByUserQueue({ startDate: startDate, endDate: endDate, status: 'open', clientChatStart: true }) const openByUserOnQueue = await CountTicketsByUserQueue({
const openByUserOutQueue = await CountTicketsByUserQueue({ startDate: startDate, endDate: endDate, status: 'open', clientChatStart: false }) startDate: startDate,
endDate: endDate,
status: "open",
clientChatStart: true
});
const openByUserOutQueue = await CountTicketsByUserQueue({
startDate: startDate,
endDate: endDate,
status: "open",
clientChatStart: false
});
const closedByUserOnQueue = await CountTicketsByUserQueue({ startDate: startDate, endDate: endDate, status: 'closed', clientChatStart: true }) const closedByUserOnQueue = await CountTicketsByUserQueue({
const closedUserOutQueue = await CountTicketsByUserQueue({ startDate: startDate, endDate: endDate, status: 'closed', clientChatStart: false }) startDate: startDate,
endDate: endDate,
status: "closed",
clientChatStart: true
});
const closedUserOutQueue = await CountTicketsByUserQueue({
startDate: startDate,
endDate: endDate,
status: "closed",
clientChatStart: false
});
// let openQueueInOut = openByUserOnQueue.concat(openByUserOutQueue) // let openQueueInOut = openByUserOnQueue.concat(openByUserOutQueue)
// let closedQueueInOut = closedByUserOnQueue.concat(closedUserOutQueue) // let closedQueueInOut = closedByUserOnQueue.concat(closedUserOutQueue)
const queuesByUser = await ShowQueuesByUser({ profile: "user" });
const queuesByUser = await ShowQueuesByUser({ profile: 'user' }) let openCloseOnQueue = openByUserOnQueue.concat(closedByUserOnQueue);
let openCloseOutQueue = openByUserOutQueue.concat(closedUserOutQueue);
let openCloseOnQueue = openByUserOnQueue.concat(closedByUserOnQueue)
let openCloseOutQueue = openByUserOutQueue.concat(closedUserOutQueue)
// console.log('onlineUsers: ',JSON.parse(JSON.stringify(onlineUsers))) // console.log('onlineUsers: ',JSON.parse(JSON.stringify(onlineUsers)))
// console.log('sumUserOlineTime: ', JSON.parse(JSON.stringify(sumUserOlineTime))) // console.log('sumUserOlineTime: ', JSON.parse(JSON.stringify(sumUserOlineTime)))
for (let i = 0; i < queuesByUser.length; i++) { for (let i = 0; i < queuesByUser.length; i++) {
queuesByUser[i].countOpen = 0;
queuesByUser[i].countOpen = 0 queuesByUser[i].countClosed = 0;
queuesByUser[i].countClosed = 0
for (let x = 0; x < openCloseOnQueue.length; x++) { for (let x = 0; x < openCloseOnQueue.length; x++) {
if ((queuesByUser[i].userId == openCloseOnQueue[x].userId) && if (
(queuesByUser[i].queueId == openCloseOnQueue[x].queueId && openCloseOnQueue[x].status == 'open')) { queuesByUser[i].userId == openCloseOnQueue[x].userId &&
queuesByUser[i].countOpen = openCloseOnQueue[x].totAttendance queuesByUser[i].queueId == openCloseOnQueue[x].queueId &&
openCloseOnQueue[x].status == "open"
) {
queuesByUser[i].countOpen = openCloseOnQueue[x].totAttendance;
} else if (
queuesByUser[i].userId == openCloseOnQueue[x].userId &&
queuesByUser[i].queueId == openCloseOnQueue[x].queueId &&
openCloseOnQueue[x].status == "closed"
) {
queuesByUser[i].countClosed = openCloseOnQueue[x].totAttendance;
} }
else if ((queuesByUser[i].userId == openCloseOnQueue[x].userId) &&
(queuesByUser[i].queueId == openCloseOnQueue[x].queueId && openCloseOnQueue[x].status == 'closed')) {
queuesByUser[i].countClosed = openCloseOnQueue[x].totAttendance
}
} }
} }
usersProfile.map((user: any) => { usersProfile.map((user: any) => {
let index = sumUserOlineTime.findIndex((e: any) => e.userId == user.id);
let index = sumUserOlineTime.findIndex((e: any) => e.userId == user.id)
if (index != -1) { if (index != -1) {
user.sumOnlineTime = sumUserOlineTime[index]; user.sumOnlineTime = sumUserOlineTime[index];
@ -133,68 +182,67 @@ export const reportUserService = async (req: Request, res: Response): Promise<Re
// console.log('user.sumOlineTime: 'user.sumOnlineTime) // console.log('user.sumOlineTime: 'user.sumOnlineTime)
} }
index = closedByUser.findIndex((e: any) => e.userId == user.id) index = closedByUser.findIndex((e: any) => e.userId == user.id);
if (index != -1) { if (index != -1) {
user.sumClosed = closedByUser[index]; user.sumClosed = closedByUser[index];
} }
index = openByUser.findIndex((e: any) => e.userId == user.id) index = openByUser.findIndex((e: any) => e.userId == user.id);
if (index != -1) { if (index != -1) {
user.sumOpen = openByUser[index] user.sumOpen = openByUser[index];
} }
// OPEN, CLOSED TICKETS STARTED BY USERS // OPEN, CLOSED TICKETS STARTED BY USERS
let openClosedOutQueue = {} let openClosedOutQueue = {};
let open = openCloseOutQueue.filter((e) => e.userId == user.id && e.status == 'open') let open = openCloseOutQueue.filter(
let closed = openCloseOutQueue.filter((e) => e.userId == user.id && e.status == 'closed') e => e.userId == user.id && e.status == "open"
);
let closed = openCloseOutQueue.filter(
e => e.userId == user.id && e.status == "closed"
);
openClosedOutQueue = { openClosedOutQueue = {
...openClosedOutQueue, ...openClosedOutQueue,
userId: user.id, userId: user.id,
countOpen: open && open.length > 0 ? open[0].totAttendance : 0, countOpen: open && open.length > 0 ? open[0].totAttendance : 0,
countClosed: closed && closed.length > 0 ? closed[0].totAttendance : 0 countClosed: closed && closed.length > 0 ? closed[0].totAttendance : 0
} };
user.openClosedOutQueue = openClosedOutQueue
user.openClosedOutQueue = openClosedOutQueue;
// OPEN, CLOSED TICKETS STARTED BY CLIENTS // OPEN, CLOSED TICKETS STARTED BY CLIENTS
let openClosedInQueue = queuesByUser.filter((e) => e.userId == user.id) let openClosedInQueue = queuesByUser.filter(e => e.userId == user.id);
if (openClosedInQueue && openClosedInQueue.length > 0) { if (openClosedInQueue && openClosedInQueue.length > 0) {
user.openClosedInQueue = openClosedInQueue user.openClosedInQueue = openClosedInQueue;
} }
index = onlineUsers.findIndex((e: any) => e.userId == user.id);
index = onlineUsers.findIndex((e: any) => e.userId == user.id)
if (index != -1) { if (index != -1) {
user.statusOnline = onlineUsers[index] user.statusOnline = onlineUsers[index];
} }
if (startDate.length > 0 && startDate.split('-').length == 3) { if (startDate.length > 0 && startDate.split("-").length == 3) {
let date = startDate.split('-') let date = startDate.split("-");
user.startDate = `${date[2]}/${date[1]}/${date[0]}` user.startDate = `${date[2]}/${date[1]}/${date[0]}`;
} }
if (endDate.length > 0 && endDate.split('-').length == 3) { if (endDate.length > 0 && endDate.split("-").length == 3) {
let date = endDate.split('-') let date = endDate.split("-");
user.endDate = `${date[2]}/${date[1]}/${date[0]}` user.endDate = `${date[2]}/${date[1]}/${date[0]}`;
} }
});
}) return res.status(200).json({ usersProfile: usersProfile });
return res.status(200).json({usersProfile: usersProfile});
}; };
export const reportMessagesUserByDateStartDateEnd = async (
req: Request,
export const reportMessagesUserByDateStartDateEnd = async (req: Request, res: Response): Promise<Response> => { res: Response
): Promise<Response> => {
if ( if (
req.user.profile !== "master" && req.user.profile !== "master" &&
req.user.profile !== "admin" && req.user.profile !== "admin" &&
@ -203,35 +251,30 @@ export const reportMessagesUserByDateStartDateEnd = async (req: Request, res: Re
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 } = req.query as IndexQuery;
let data_query_messages = await ShowMessageReport(userId, startDate, endDate); let data_query_messages = await ShowMessageReport(userId, startDate, endDate);
for (var i = 0; i < data_query_messages.length; i++) { for (var i = 0; i < data_query_messages.length; i++) {
if (data_query_messages[i].fromMe) { if (data_query_messages[i].fromMe) {
data_query_messages[i].fromMe = 'Atendente' data_query_messages[i].fromMe = "Atendente";
} } else {
else { data_query_messages[i].fromMe = "Cliente";
data_query_messages[i].fromMe = 'Cliente'
} }
data_query_messages[i].id = (i + 1) data_query_messages[i].id = i + 1;
console.log('data_query_messages: ', data_query_messages[i])
} }
return res.status(200).json(data_query_messages); return res.status(200).json(data_query_messages);
}; };
export const reportOnQueue = async (
req: Request,
export const reportOnQueue = async (req: Request, res: Response): Promise<Response> => { res: Response
): Promise<Response> => {
// console.log(req.body) // console.log(req.body)
const { adminId, identifier, queueStatus, file } = req.body const { adminId, identifier, queueStatus, file } = req.body;
const io = getIO(); const io = getIO();
io.emit("queryOnQueueStatus", { io.emit("queryOnQueueStatus", {
@ -244,10 +287,80 @@ export const reportOnQueue = async (req: Request, res: Response): Promise<Respon
} }
}); });
return res.status(200).json({ message: 'ok' }) return res.status(200).json({ message: "ok" });
}; };
export const reportService = async (
req: Request,
res: Response
): Promise<Response> => {
if (
req.user.profile !== "master" &&
req.user.profile !== "admin" &&
req.user.profile !== "supervisor"
) {
throw new AppError("ERR_NO_PERMISSION", 403);
}
const { startDate, endDate, queueId } = req.query as IndexQuery;
console.log(
`startDate: ${startDate} | endDate: ${endDate} | queueId: ${queueId}`
);
const reportService = await ReportByNumberQueueService({
startDate,
endDate
});
return res.status(200).json({ reportService });
};
export const reportServiceByQueue = async (
req: Request,
res: Response
): Promise<Response> => {
if (
req.user.profile !== "master" &&
req.user.profile !== "admin" &&
req.user.profile !== "supervisor"
) {
throw new AppError("ERR_NO_PERMISSION", 403);
}
const { startDate, endDate, queueId } = req.query as IndexQuery;
const reportService = await ReportByNumberQueueService({
startDate,
endDate,
queue: true
});
return res.status(200).json({ reportService });
};
export const reportTicksCountByStatusChatEnds = async (
req: Request,
res: Response
): Promise<Response> => {
if (
req.user.profile !== "master" &&
req.user.profile !== "admin" &&
req.user.profile !== "supervisor"
) {
throw new AppError("ERR_NO_PERMISSION", 403);
}
const { startDate, endDate } = req.query as IndexQuery;
const dateToday = splitDateTime(
new Date(format(new Date(), "yyyy-MM-dd HH:mm:ss", { locale: ptBR }))
);
const reportStatusChatEnd = await CountStatusChatEndService(
startDate || dateToday.fullDate,
endDate || dateToday.fullDate
);
return res.status(200).json({ reportStatusChatEnd });
};

View File

@ -20,7 +20,7 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
// const config = await SettingTicket.findAll(); // const config = await SettingTicket.findAll();
return res.status(200).json({ settings, }); return res.status(200).json({ settings });
}; };
export const ticketSettings = async ( export const ticketSettings = async (
@ -40,6 +40,7 @@ export const updateTicketSettings = async (
): Promise<Response> => { ): Promise<Response> => {
const { const {
number, number,
saturdayBusinessTime,
outBusinessHours, outBusinessHours,
ticketExpiration, ticketExpiration,
weekend, weekend,
@ -58,6 +59,14 @@ export const updateTicketSettings = async (
}); });
} }
if (saturdayBusinessTime && Object.keys(saturdayBusinessTime).length > 0) {
await updateSettingTicket({
...saturdayBusinessTime,
key: "saturdayBusinessTime",
number
});
}
if (ticketExpiration && Object.keys(ticketExpiration).length > 0) { if (ticketExpiration && Object.keys(ticketExpiration).length > 0) {
await updateSettingTicket({ await updateSettingTicket({
...ticketExpiration, ...ticketExpiration,

View File

@ -22,7 +22,7 @@ import format from "date-fns/format";
import ListTicketsServiceCache from "../services/TicketServices/ListTicketServiceCache"; import ListTicketsServiceCache from "../services/TicketServices/ListTicketServiceCache";
import { searchTicketCache, loadTicketsCache } from "../helpers/TicketCache"; import { searchTicketCache, loadTicketsCache } from "../helpers/TicketCache";
import { Op } from "sequelize"; import { Op, where } from "sequelize";
type IndexQuery = { type IndexQuery = {
searchParam: string; searchParam: string;
@ -68,6 +68,15 @@ import ListWhatsAppsForQueueService from "../services/WhatsappService/ListWhatsA
import ListWhatsAppsNumber from "../services/WhatsappService/ListWhatsAppsNumber"; import ListWhatsAppsNumber from "../services/WhatsappService/ListWhatsAppsNumber";
import Whatsapp from "../models/Whatsapp"; import Whatsapp from "../models/Whatsapp";
import AppError from "../errors/AppError"; import AppError from "../errors/AppError";
import CreateOrUpdateContactService from "../services/ContactServices/CreateOrUpdateContactService";
import FindOrCreateTicketService from "../services/TicketServices/FindOrCreateTicketService";
import CheckIsValidContact from "../services/WbotServices/CheckIsValidContact";
import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl";
import CreateContactService from "../services/ContactServices/CreateContactService";
import { botSendMessage } from "../services/WbotServices/wbotMessageListener";
import WhatsappQueue from "../models/WhatsappQueue";
import { get } from "../helpers/RedisClient";
import CountStatusChatEndService from "../services/StatusChatEndService/CountStatusChatEndService";
export const index = async (req: Request, res: Response): Promise<Response> => { export const index = async (req: Request, res: Response): Promise<Response> => {
const { const {
@ -86,7 +95,7 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
let queueIds: number[] = []; let queueIds: number[] = [];
if (queueIdsStringified) { if (queueIdsStringified && queueIdsStringified.trim().length > 0) {
queueIds = JSON.parse(queueIdsStringified); queueIds = JSON.parse(queueIdsStringified);
} }
@ -106,6 +115,158 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
return res.status(200).json({ tickets, count, hasMore }); return res.status(200).json({ tickets, count, hasMore });
}; };
export const remoteTicketCreation = async (
req: Request,
res: Response
): Promise<Response> => {
let { queueId, contact_from, contact_to, msg, contact_name }: any = req.body;
let whatsappId: any;
if (!queueId && !contact_from) {
return res
.status(400)
.json({ error: `Property 'queueId' or 'contact_from' is required.` });
}
const validate = ["contact_to", "msg"];
const validateOnlyNumber = ["queueId", "contact_to", "contact_from"];
for (let prop of validate) {
if (!req.body[prop])
return res
.status(400)
.json({ error: `Property '${prop}' is undefined.` });
if (validateOnlyNumber.includes(prop)) {
if (!/^\d+$/.test(req.body[prop])) {
return res
.status(400)
.json({ error: `The property '${prop}' must be a number` });
}
}
}
if (queueId) {
const whatsapps = await ListWhatsAppsForQueueService(queueId, "CONNECTED");
if (!whatsapps || whatsapps?.length == 0) {
return res.status(500).json({
msg: `queueId ${queueId} does not have a WhatsApp number associated with it or the number's session is disconnected.`
});
}
const { id } = whatsapps[0];
whatsappId = id;
} else if (contact_from) {
const whatsapp = await Whatsapp.findOne({
where: { number: contact_from, status: "CONNECTED" }
});
if (!whatsapp) {
return res.status(404).json({
msg: `Whatsapp number ${contact_from} not found or disconnected!`
});
}
const { id } = whatsapp;
const { queues } = await ShowWhatsAppService(id);
if (!queues || queues.length == 0) {
return res.status(500).json({
msg: `The WhatsApp number ${contact_from} is not associated with any queue! `
});
}
queueId = queues[0].id;
whatsappId = id;
}
// const validNumber = await CheckIsValidContact(contact_to, true);
const validNumber = contact_to;
if (validNumber) {
let contact = await Contact.findOne({ where: { number: validNumber } });
if (!contact) {
// const profilePicUrl = await GetProfilePicUrl(validNumber);
contact = await CreateContactService({
name: contact_name ? contact_name : contact_to,
number: validNumber
// profilePicUrl
});
const io = getIO();
io.emit("contact", {
action: "create",
contact
});
}
const { id: contactId } = contact;
const botInfo = await BotIsOnQueue("botqueue");
let ticket = await Ticket.findOne({
where: {
[Op.or]: [
{ contactId, status: "queueChoice" },
{ contactId, status: "open", userId: botInfo.userIdBot }
]
}
});
if (getSettingValue("whatsaAppCloudApi")?.value == "enabled") {
if (ticket) {
await UpdateTicketService({
ticketData: { status: "closed" },
ticketId: ticket.id
});
ticket = null;
}
} else {
if (ticket) {
await UpdateTicketService({
ticketData: { status: "closed" },
ticketId: ticket.id
});
}
}
if (!ticket) {
ticket = await FindOrCreateTicketService(
contact,
whatsappId,
0,
undefined,
queueId
);
botSendMessage(ticket, `${msg}`);
}
const io = getIO();
io.to(ticket.status).emit("ticket", {
action: "update",
ticket
});
console.log(
`REMOTE TICKET CREATION FROM ENDPOINT | STATUS: 200 | MSG: success`
);
return res.status(200).json({ msg: "success" });
}
console.log(
`REMOTE TICKET CREATION FROM ENDPOINT | STATUS: 500 | MSG: The number ${contact_to} does not exist on WhatsApp`
);
return res
.status(500)
.json({ msg: `The number ${contact_to} does not exist on WhatsApp` });
};
export const store = async (req: Request, res: Response): Promise<Response> => { export const store = async (req: Request, res: Response): Promise<Response> => {
const { contactId, status, userId, msg, queueId, whatsappId }: TicketData = const { contactId, status, userId, msg, queueId, whatsappId }: TicketData =
req.body; req.body;
@ -211,7 +372,8 @@ export const update = async (
ticketData: { ticketData: {
status: status, status: status,
userId: userId, userId: userId,
statusChatEnd: statusChatEndName.name statusChatEnd: statusChatEndName.name,
statusChatEndId: statusChatEndName.id
}, },
ticketId ticketId
}); });
@ -269,7 +431,6 @@ export const update = async (
for (const w of whatsappsByqueue) { for (const w of whatsappsByqueue) {
let whats = await ListWhatsAppsNumber(w.id); let whats = await ListWhatsAppsNumber(w.id);
console.log("-------> WHATS: ", JSON.stringify(whats, null, 6));
const ticket = await Ticket.findOne({ const ticket = await Ticket.findOne({
where: { where: {
[Op.and]: [ [Op.and]: [
@ -426,10 +587,3 @@ export const remove = async (
return res.status(200).json({ message: "ticket deleted" }); return res.status(200).json({ message: "ticket deleted" });
}; };
// export async function setMessageAsRead(ticket: Ticket) {
// const wbot_url = await getWbot(ticket.whatsappId);
// console.log('wbot_url: ', wbot_url, ' | ticket.contact.number: ', ticket.contact.number);
// await endPointQuery(`${wbot_url}/api/sendSeen`, { number: `${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us` });
// }

View File

@ -12,7 +12,7 @@ import DeleteUserService from "../services/UserServices/DeleteUserService";
import ListUser from "../services/UserServices/ListUserParamiterService"; import ListUser from "../services/UserServices/ListUserParamiterService";
import User from "../models/User"; import User from "../models/User";
import { get, set } from "../helpers/RedisClient"; import { del, get, set } from "../helpers/RedisClient";
import { import {
startWhoIsOnlineMonitor, startWhoIsOnlineMonitor,
@ -27,6 +27,7 @@ import { splitDateTime } from "../helpers/SplitDateTime";
import ListUserByWhatsappQueuesService from "../services/UserServices/ListUserByWhatsappQueuesService"; import ListUserByWhatsappQueuesService from "../services/UserServices/ListUserByWhatsappQueuesService";
import { json } from "sequelize"; import { json } from "sequelize";
import { getSettingValue } from "../helpers/WhaticketSettings"; import { getSettingValue } from "../helpers/WhaticketSettings";
import { setBotInfo } from "../helpers/SetBotInfo";
type IndexQuery = { type IndexQuery = {
searchParam: string; searchParam: string;
@ -99,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> => {
const { userId, profile } = req.query as IndexQuery; let { userId, profile }: any = req.query as IndexQuery;
console.log( console.log(
"userId: ", "userId: ",
@ -111,6 +112,8 @@ export const all = async (req: Request, res: Response): Promise<Response> => {
); );
if (getSettingValue("queueTransferByWhatsappScope")?.value == "enabled") { if (getSettingValue("queueTransferByWhatsappScope")?.value == "enabled") {
if (!userId) return res.json({ users: [], queues: [] });
const obj = await ListUserByWhatsappQueuesService( const obj = await ListUserByWhatsappQueuesService(
userId, userId,
'"admin", "user", "supervisor"' '"admin", "user", "supervisor"'
@ -164,6 +167,11 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
queueIds queueIds
}); });
if (user) {
const { id, name } = user;
await set(`user:${id}`, { id, name });
}
const io = getIO(); const io = getIO();
io.emit("user", { io.emit("user", {
action: "create", action: "create",
@ -267,34 +275,11 @@ export const update = async (
let user: any = await UpdateUserService({ userData, userId }); let user: any = await UpdateUserService({ userData, userId });
if (user?.name?.trim() == "botqueue") { await setBotInfo(user);
let botInfo;
if ( if (user) {
user?.queues?.length > 0 && const { id, name } = user;
user.queues[0]?.name?.trim() == "botqueue" await set(`user:${id}`, { id, name });
) {
botInfo = JSON.stringify({
userId: user.id,
queueId: user.queues[0].id,
botIsOnQueue: true
});
botInfo = JSON.parse(botInfo);
await set("botInfo", botInfo);
} else if (
user?.queues?.length == 0 ||
user.queues[0]?.name?.trim() != "botqueue"
) {
botInfo = JSON.stringify({
userId: user.id,
queueId: 0,
botIsOnQueue: false
});
botInfo = JSON.parse(botInfo);
await set("botInfo", botInfo);
}
} }
const io = getIO(); const io = getIO();
@ -320,6 +305,8 @@ export const remove = async (
await DeleteUserService(userId); await DeleteUserService(userId);
del(`user:${userId}`);
const io = getIO(); const io = getIO();
io.emit("user", { io.emit("user", {
action: "delete", action: "delete",

View File

@ -42,6 +42,7 @@ import { getSettingValue } from "../helpers/WhaticketSettings";
import ListWhatsAppsNumber from "../services/WhatsappService/ListWhatsAppsNumber"; import ListWhatsAppsNumber from "../services/WhatsappService/ListWhatsAppsNumber";
import SettingTicket from "../models/SettingTicket"; import SettingTicket from "../models/SettingTicket";
import { Op } from "sequelize"; import { Op } from "sequelize";
import { del, get, set } from "../helpers/RedisClient";
interface WhatsappData { interface WhatsappData {
name: string; name: string;
@ -54,6 +55,7 @@ interface WhatsappData {
isDefault?: boolean; isDefault?: boolean;
isOfficial?: boolean; isOfficial?: boolean;
phoneNumberId?: string; phoneNumberId?: string;
number?: string;
wabaId?: string; wabaId?: string;
} }
@ -228,6 +230,25 @@ export const weebhook = async (
req.body.entry[0].changes[0].value.metadata.display_phone_number; req.body.entry[0].changes[0].value.metadata.display_phone_number;
let type = message.type; let type = message.type;
const contact_to_exist = await get({
key: "whatsapp:*",
value: `${contact_to}`
});
if (contact_to_exist == null) {
console.log(
"WHATSAPP OFFICIAL",
" | CONCTACT_FROM: ",
contact_from,
" | CONTACT_TO: ",
contact_to
);
console.log(
"NUMBER IGNORED. WHATSAPP NUMBER FROM ANOTHER OMNIHIT APPLICATION!"
);
return res.status(403).json({ error: "Unauthorized" });
}
let wbot = {}; let wbot = {};
let msg = {}; let msg = {};
let contacts = req.body.entry[0].changes[0].value.contacts[0]; let contacts = req.body.entry[0].changes[0].value.contacts[0];
@ -247,6 +268,10 @@ export const weebhook = async (
}); });
if (type == "text") { if (type == "text") {
if (!message?.text?.body) {
return res.status(400).json({ error: "body not found" });
}
type = "chat"; type = "chat";
msg = { msg = {
...msg, ...msg,
@ -254,6 +279,10 @@ export const weebhook = async (
type type
}; };
} else { } else {
if (!message[message?.type]?.id) {
return res.status(400).json({ error: "id not found" });
}
const mediaId = message[message.type].id; const mediaId = message[message.type].id;
const mimetype = message[message.type].mime_type; const mimetype = message[message.type].mime_type;
@ -322,7 +351,8 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
urlApi, urlApi,
phoneNumberId, phoneNumberId,
wabaId, wabaId,
isOfficial isOfficial,
number
}: WhatsappData = req.body; }: WhatsappData = req.body;
if (req.user.profile !== "master") { if (req.user.profile !== "master") {
@ -333,7 +363,8 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
urlApi, urlApi,
isOfficial, isOfficial,
phoneNumberId, phoneNumberId,
wabaId wabaId,
number
}); });
if (invalid) { if (invalid) {
@ -346,6 +377,7 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
} else if (!isOfficial) { } else if (!isOfficial) {
phoneNumberId = ""; phoneNumberId = "";
wabaId = ""; wabaId = "";
number = "";
} }
let invalidPhoneName = validatePhoneName(name); let invalidPhoneName = validatePhoneName(name);
@ -365,7 +397,8 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
queueIds, queueIds,
phoneNumberId, phoneNumberId,
wabaId, wabaId,
isOfficial isOfficial,
number
}); });
console.log("whatsapp.id: ", whatsapp.id); console.log("whatsapp.id: ", whatsapp.id);
@ -377,6 +410,16 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
number: getNumberFromName(name), number: getNumberFromName(name),
client_url: `${process.env.BACKEND_URL_RAW}:${process.env.PORT}` client_url: `${process.env.BACKEND_URL_RAW}:${process.env.PORT}`
}); });
} else {
await set(
`whatsapp:${whatsapp.id}`,
JSON.stringify({
number: whatsapp?.number,
id: whatsapp?.id,
greetingMessage: whatsapp?.greetingMessage,
phoneNumberId: whatsapp?.phoneNumberId
})
);
} }
const io = getIO(); const io = getIO();
@ -416,13 +459,14 @@ export const update = async (
return res.status(200).json({ message: invalidPhoneName }); return res.status(200).json({ message: invalidPhoneName });
} }
const { urlApi, isOfficial, phoneNumberId, wabaId } = whatsappData; const { urlApi, isOfficial, phoneNumberId, number, wabaId } = whatsappData;
const invalid = checkWhatsAppData({ const invalid = checkWhatsAppData({
urlApi, urlApi,
isOfficial, isOfficial,
phoneNumberId, phoneNumberId,
wabaId wabaId,
number
}); });
if (invalid) { if (invalid) {
@ -435,6 +479,7 @@ export const update = async (
} else if (!isOfficial) { } else if (!isOfficial) {
whatsappData.phoneNumberId = ""; whatsappData.phoneNumberId = "";
whatsappData.wabaId = ""; whatsappData.wabaId = "";
whatsappData.number = "";
} }
const { whatsapp, oldDefaultWhatsapp } = await UpdateWhatsAppService({ const { whatsapp, oldDefaultWhatsapp } = await UpdateWhatsAppService({
@ -449,6 +494,16 @@ export const update = async (
number: getNumberFromName(whatsapp.name), number: getNumberFromName(whatsapp.name),
client_url: `${process.env.BACKEND_URL_RAW}:${process.env.PORT}` client_url: `${process.env.BACKEND_URL_RAW}:${process.env.PORT}`
}); });
} else {
await set(
`whatsapp:${whatsapp.id}`,
JSON.stringify({
number: whatsapp?.number,
id: whatsapp?.id,
greetingMessage: whatsapp?.greetingMessage,
phoneNumberId: whatsapp?.phoneNumberId
})
);
} }
const io = getIO(); const io = getIO();
@ -486,6 +541,8 @@ export const remove = async (
}); });
} }
await del(`whatsapp:${whatsappId}`);
let whats = await ListWhatsAppsNumber(whatsappId); let whats = await ListWhatsAppsNumber(whatsappId);
// Remove tickets business hours config // Remove tickets business hours config
@ -549,18 +606,22 @@ interface WhatsappDataValidate {
isOfficial?: boolean; isOfficial?: boolean;
phoneNumberId?: string; phoneNumberId?: string;
wabaId?: string; wabaId?: string;
number?: string;
} }
const checkWhatsAppData = ({ const checkWhatsAppData = ({
urlApi, urlApi,
isOfficial, isOfficial,
phoneNumberId, phoneNumberId,
wabaId wabaId,
number
}: WhatsappDataValidate) => { }: WhatsappDataValidate) => {
if (isOfficial && (!phoneNumberId || phoneNumberId.trim() == "")) { if (isOfficial && (!phoneNumberId || phoneNumberId.trim() == "")) {
return { message: "Phone number Id is required!" }; return { message: "Phone number Id is required!" };
} else if (isOfficial && (!wabaId || wabaId.trim() == "")) { } else if (isOfficial && (!wabaId || wabaId.trim() == "")) {
return { message: "WABA ID is required!" }; return { message: "WABA ID is required!" };
} else if (isOfficial && (!number || number.trim() == "")) {
return { message: "Phone number is required!" };
} else if (!isOfficial && (!urlApi || urlApi.trim() == "")) { } else if (!isOfficial && (!urlApi || urlApi.trim() == "")) {
return { message: "urlApi is required!" }; return { message: "urlApi is required!" };
} }

View File

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

View File

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

View File

@ -0,0 +1,17 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Tickets", "statusChatEndId", {
type: DataTypes.INTEGER,
references: { model: "StatusChatEnds", key: "id" },
onUpdate: "CASCADE",
onDelete: "SET NULL",
allowNull: true
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Tickets", "statusChatEndId");
}
};

View File

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

View File

@ -1,5 +1,5 @@
class AppError { class AppError {
public readonly message: string; public message: string;
public readonly statusCode: number; public readonly statusCode: number;

View File

@ -5,8 +5,7 @@ import ListUsersService from "../services/UserServices/ListUsersService";
import { get } from "./RedisClient"; import { get } from "./RedisClient";
const _botIsOnQueue = async (botName: string) => { const _botIsOnQueue = async (botName: string) => {
const botInfo = await get({ key: "botInfo", parse: true });
const botInfo = await get("botInfo");
if ( if (
botInfo && botInfo &&

View File

@ -1,6 +1,7 @@
import { Op } from "sequelize"; import { Op } from "sequelize";
import AppError from "../errors/AppError"; import AppError from "../errors/AppError";
import Ticket from "../models/Ticket"; import Ticket from "../models/Ticket";
import User from "../models/User";
import ListWhatsAppsNumber from "../services/WhatsappService/ListWhatsAppsNumber"; import ListWhatsAppsNumber from "../services/WhatsappService/ListWhatsAppsNumber";
import { getSettingValue } from "./WhaticketSettings"; import { getSettingValue } from "./WhaticketSettings";
import ListWhatsAppsForQueueService from "../services/WhatsappService/ListWhatsAppsForQueueService"; import ListWhatsAppsForQueueService from "../services/WhatsappService/ListWhatsAppsForQueueService";
@ -15,9 +16,9 @@ const CheckContactOpenTickets = async (
if (getSettingValue("oneContactChatWithManyWhats")?.value == "enabled") { if (getSettingValue("oneContactChatWithManyWhats")?.value == "enabled") {
let whats = await ListWhatsAppsNumber(whatsappId); let whats = await ListWhatsAppsNumber(whatsappId);
console.log("contactId: ", contactId, " | whatsappId: ", whatsappId); // console.log("contactId: ", contactId, " | whatsappId: ", whatsappId);
console.log("WHATS: ", JSON.stringify(whats, null, 6)); // console.log("WHATS: ", JSON.stringify(whats, null, 6));
ticket = await Ticket.findOne({ ticket = await Ticket.findOne({
where: { where: {
@ -38,8 +39,13 @@ const CheckContactOpenTickets = async (
if (ticket) { if (ticket) {
if (handle) return true; if (handle) return true;
const userName = await User.findOne({
where:{ id: ticket.userId }
});
const error = new AppError("ERR_OTHER_OPEN_TICKET");
error.message = `Erro: já existe um ticket criado com esse contato. Responsável: ${userName? userName.name.toUpperCase() : 'Aguardando'}`;
throw new AppError("ERR_OTHER_OPEN_TICKET"); throw error;
} }

View File

@ -13,15 +13,14 @@ import { WhatsIndex } from "./LoadBalanceWhatsSameQueue";
interface Request { interface Request {
userId?: string | number; userId?: string | number;
queueId?: string | number; queueId?: string | number;
ignoreNoWhatsappFound?: boolean
} }
//const GetDefaultWhatsApp = async (userId?: string | number): Promise<Whatsapp> => {
const GetDefaultWhatsApp = async ({ const GetDefaultWhatsApp = async ({
userId, userId,
queueId queueId,
ignoreNoWhatsappFound = false
}: Request): Promise<any> => { }: Request): Promise<any> => {
// test del
let defaultWhatsapp = await Whatsapp.findOne({ let defaultWhatsapp = await Whatsapp.findOne({
where: { isDefault: true } where: { isDefault: true }
}); });
@ -56,17 +55,16 @@ const GetDefaultWhatsApp = async ({
} }
} else { } else {
defaultWhatsapp = await Whatsapp.findOne({ defaultWhatsapp = await Whatsapp.findOne({
where: { status: "CONNECTED" } where: { status: "CONNECTED", isOfficial: false }
}); });
} }
} }
if (!defaultWhatsapp) { if (!defaultWhatsapp && !ignoreNoWhatsappFound) {
throw new AppError("ERR_NO_DEF_WAPP_FOUND"); throw new AppError("ERR_NO_DEF_WAPP_FOUND");
} }
return defaultWhatsapp; return defaultWhatsapp;
//
}; };
export default GetDefaultWhatsApp; export default GetDefaultWhatsApp;

View File

@ -15,9 +15,9 @@ async function omnihitDashboardSession(data: any) {
} }
); );
} catch (error) { } catch (error) {
console.log( // console.log(
`Post request error to ${process.env.URL_DASHBOARD_SESSIONS}/api/v1/omnihit/monitor` // `Post request error to ${process.env.URL_DASHBOARD_SESSIONS}/api/v1/omnihit/monitor`
); // );
} }
} }

View File

@ -6,27 +6,101 @@ type WhatsappData = {
contactId: string | number; contactId: string | number;
identifier: string; identifier: string;
value?: string; value?: string;
history?: string;
}; };
export async function set(key: string, value: string, expire: boolean = false) { type getData = {
await redis.set(key, JSON.stringify(value)); key: string;
value?: string;
parse?: boolean;
};
if (expire) await redis.expire(key, 300); export async function set(key: string, value: string | object) {
if (typeof value == "object") await redis.set(key, JSON.stringify(value));
else {
await redis.set(key, value);
}
} }
export async function get(key: string) { export async function getSimple(key: string) {
const value: any = await redis.get(key); const value: any = await redis.get(key);
return JSON.parse(value); return value;
} }
export async function get({ key, value, parse }: getData) {
if (key.includes("*")) {
const keys = await redis.keys(key);
if (keys.length > 0) {
for (const key of keys) {
const val = await redis.get(key);
if (val.includes(value)) {
if (parse) return JSON.parse(val);
return val;
}
}
}
return null;
} else {
const value: any = await redis.get(key);
if (parse) return JSON.parse(value);
return value;
}
}
export async function del(key: string) { export async function del(key: string) {
await redis.del(key); await redis.del(key);
} }
export async function clearAllKeys(...keys: string[]) {
for (const key of keys) {
// Retrieve all keys matching the pattern '*'
const del_keys = await redis.keys(key);
// If there are keys, delete them
if (del_keys.length > 0) {
console.log("del_keys: ", del_keys);
await redis.del(...del_keys);
}
}
}
export async function findByContain(
key: string,
keyName: string,
substring: string
) {
// const keys = await redis.keys("*" + substring + "*");
// const keys = await redis.keys("user:*");
const keys = await redis.keys(key);
const results: any[] = [];
for (const key of keys) {
const value = await redis.get(key);
if (value) {
const obj = JSON.parse(value);
if (
substring
?.trim()
?.toLowerCase()
.includes(obj[keyName]?.trim()?.toLowerCase())
) {
results.push(obj);
}
}
}
return results;
}
export async function createObject({ export async function createObject({
whatsappId, whatsappId,
contactId, contactId,
identifier, identifier,
value value,
history = ""
}: WhatsappData) { }: WhatsappData) {
const key = `whatsappId:${whatsappId}:contactId:${contactId}:identifier:${identifier}`; const key = `whatsappId:${whatsappId}:contactId:${contactId}:identifier:${identifier}`;
const result = await redis.hmset( const result = await redis.hmset(
@ -38,7 +112,9 @@ export async function createObject({
"identifier", "identifier",
identifier, identifier,
"value", "value",
value value,
"history",
history
); );
await redis.expire(key, 300); await redis.expire(key, 300);
@ -66,7 +142,8 @@ export async function findObject(
"whatsappId", "whatsappId",
"contactId", "contactId",
"identifier", "identifier",
"value" "value",
"history"
); );
return result; return result;
} }

View File

@ -0,0 +1,33 @@
import { get, set } from "../helpers/RedisClient";
export async function setBotInfo(user: any) {
if (user?.name?.trim() == "botqueue") {
let botInfo;
if (
user?.queues?.length > 0 &&
user.queues[0]?.name?.trim() == "botqueue"
) {
botInfo = JSON.stringify({
userId: user.id,
queueId: user.queues[0].id,
botIsOnQueue: true
});
botInfo = JSON.parse(botInfo);
await set("botInfo", botInfo);
} else if (
user?.queues?.length == 0 ||
user.queues[0]?.name?.trim() != "botqueue"
) {
botInfo = JSON.stringify({
userId: user.id,
queueId: 0,
botIsOnQueue: false
});
botInfo = JSON.parse(botInfo);
await set("botInfo", botInfo);
}
}
}

View File

@ -62,21 +62,8 @@ const isWeekend = async (number: string | number) => {
weekend.value == "enabled" && weekend.value == "enabled" &&
weekend.message?.trim()?.length > 0 weekend.message?.trim()?.length > 0
) { ) {
// Specify your desired timezone
const brazilTimeZone = "America/Sao_Paulo";
const currentDateUtc = new Date();
// Convert UTC date to Brazil time zone
const currentDate = utcToZonedTime(currentDateUtc, brazilTimeZone);
// Format the date using the desired format
const formattedDate = _format(currentDate, "yyyy-MM-dd HH:mm:ssXXX");
const parsedDate = parseISO(formattedDate);
// Convert parsed date to Brazil time zone // Convert parsed date to Brazil time zone
const localDate = utcToZonedTime(parsedDate, brazilTimeZone); const localDate = localDateConvert();
// Check if it's Saturday or Sunday // Check if it's Saturday or Sunday
if (isSaturday(localDate)) { if (isSaturday(localDate)) {
@ -173,8 +160,104 @@ async function isOutBusinessTime(number: string | number) {
return obj; return obj;
} }
export { async function isOutBusinessTimeSaturday(number: string | number) {
isWeekend, let obj = { set: false, msg: "" };
isHoliday,
isOutBusinessTime // Convert parsed date to Brazil time zone
}; const localDate = localDateConvert();
// Check if it's Saturday or Sunday
if (!isSaturday(localDate)) {
return obj;
}
const outBusinessHoursSaturday = await SettingTicket.findOne({
where: { key: "saturdayBusinessTime", number }
});
let isWithinRange = false;
if (
outBusinessHoursSaturday &&
outBusinessHoursSaturday.value == "enabled" &&
outBusinessHoursSaturday?.message?.trim()?.length > 0
) {
const ticketDateTimeUpdate = splitDateTime(
new Date(
_format(new Date(), "yyyy-MM-dd HH:mm:ss", {
locale: ptBR
})
)
);
const startTime = splitDateTime(
new Date(
_format(
new Date(outBusinessHoursSaturday.startTime),
"yyyy-MM-dd HH:mm:ss",
{
locale: ptBR
}
)
)
);
const endTime = splitDateTime(
new Date(
_format(
new Date(outBusinessHoursSaturday.endTime),
"yyyy-MM-dd HH:mm:ss",
{
locale: ptBR
}
)
)
);
const format = "HH:mm:ss";
const parsedStartTime = parse(
ticketDateTimeUpdate.fullTime,
format,
new Date()
);
const parsedEndTime = parse(startTime.fullTime, format, new Date());
const parsedTimeToCheck = parse(endTime.fullTime, format, new Date());
const timeInterval = { start: parsedStartTime, end: parsedEndTime };
// If the time range spans across different days, handle the date part
if (parsedEndTime < parsedStartTime) {
const nextDay = new Date(parsedStartTime);
nextDay.setDate(nextDay.getDate() + 1);
timeInterval.end = nextDay;
}
isWithinRange = isWithinInterval(parsedTimeToCheck, timeInterval);
if (!isWithinRange) {
obj.set = true;
obj.msg = outBusinessHoursSaturday.message;
}
}
return obj;
}
function localDateConvert() {
const brazilTimeZone = "America/Sao_Paulo";
const currentDateUtc = new Date();
// Convert UTC date to Brazil time zone
const currentDate = utcToZonedTime(currentDateUtc, brazilTimeZone);
// Format the date using the desired format
const formattedDate = _format(currentDate, "yyyy-MM-dd HH:mm:ssXXX");
const parsedDate = parseISO(formattedDate);
// Convert parsed date to Brazil time zone
const localDate = utcToZonedTime(parsedDate, brazilTimeZone);
return localDate;
}
export { isWeekend, isHoliday, isOutBusinessTime, isOutBusinessTimeSaturday };

View File

@ -21,6 +21,14 @@ const isAuth = (req: Request, res: Response, next: NextFunction): void => {
const [, token] = authHeader.split(" "); const [, token] = authHeader.split(" ");
if (
(req.originalUrl == "/queue/remote/list" ||
req.originalUrl == "/tickets/remote/create") &&
token === process.env.TOKEN_REMOTE_TICKET_CREATION
) {
return next();
}
try { try {
const decoded = verify(token, authConfig.secret); const decoded = verify(token, authConfig.secret);

View File

@ -0,0 +1,23 @@
import { Request, Response, NextFunction } from "express";
import AppError from "../errors/AppError";
const verifyAPIKey = (req: Request, res: Response, next: NextFunction): void => {
const authHeader = req.headers.authorization;
if (!authHeader) {
throw new AppError("ERR_SESSION_EXPIRED", 401);
}
const [, token] = authHeader.split(" ");
const apiKeyIsValid = token === process.env.TOKEN_IAM_HORACIUS_EL
if (!apiKeyIsValid) {
throw new AppError(
"Invalid token",
401
);
}
return next();
};
export default verifyAPIKey;

View File

@ -31,6 +31,10 @@ class Message extends Model<Message> {
@Column @Column
fromMe: boolean; fromMe: boolean;
@Default(false)
@Column
fromAgent: boolean;
@Column(DataType.TEXT) @Column(DataType.TEXT)
body: string; body: string;

View File

@ -10,6 +10,7 @@ import {
} from "sequelize-typescript"; } from "sequelize-typescript";
import SchedulingNotify from "./SchedulingNotify"; import SchedulingNotify from "./SchedulingNotify";
import Ticket from "./Ticket"
@Table @Table
class StatusChatEnd extends Model<StatusChatEnd> { class StatusChatEnd extends Model<StatusChatEnd> {
@ -29,6 +30,9 @@ import {
@HasMany(() => SchedulingNotify) @HasMany(() => SchedulingNotify)
SchedulingNotifies: SchedulingNotify[]; SchedulingNotifies: SchedulingNotify[];
@HasMany(() => Ticket)
tickets: Ticket[];
} }
export default StatusChatEnd; export default StatusChatEnd;

View File

@ -21,6 +21,7 @@ import User from "./User";
import Whatsapp from "./Whatsapp"; import Whatsapp from "./Whatsapp";
import SchedulingNotify from "./SchedulingNotify"; import SchedulingNotify from "./SchedulingNotify";
import StatusChatEnd from "./StatusChatEnd"
@Table @Table
class Ticket extends Model<Ticket> { class Ticket extends Model<Ticket> {
@ -42,6 +43,10 @@ class Ticket extends Model<Ticket> {
@Column @Column
isGroup: boolean; isGroup: boolean;
@ForeignKey(() => StatusChatEnd)
@Column
statusChatEndId: number;
@Column @Column
statusChatEnd: string; statusChatEnd: string;

View File

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

View File

@ -16,6 +16,8 @@ contactRoutes.get("/contacts/:contactId", isAuth, ContactController.show);
contactRoutes.post("/contacts", isAuth, ContactController.store); contactRoutes.post("/contacts", isAuth, ContactController.store);
contactRoutes.post("/contact", isAuth, ContactController.getContact);
contactRoutes.put("/contacts/:contactId", isAuth, ContactController.update); contactRoutes.put("/contacts/:contactId", isAuth, ContactController.update);
contactRoutes.delete("/contacts/:contactId", isAuth, ContactController.remove); contactRoutes.delete("/contacts/:contactId", isAuth, ContactController.remove);

View File

@ -0,0 +1,56 @@
import { Router } from "express";
import * as IAMControllerEL from "../controllers/IAMControllerEL";
import verifyAPIKey from "../middleware/verifyAPIKey";
const iamRoutesEL = Router();
iamRoutesEL.post(
"/iam/horacius/createUser",
verifyAPIKey,
IAMControllerEL.createUser
);
iamRoutesEL.put(
"/iam/horacius/updateUser",
verifyAPIKey,
IAMControllerEL.updateUser
);
iamRoutesEL.delete(
"/iam/horacius/deleteUser",
verifyAPIKey,
IAMControllerEL.deleteUser
);
iamRoutesEL.get(
"/iam/horacius/listAllUsers",
verifyAPIKey,
IAMControllerEL.listAllUsers
);
iamRoutesEL.get(
"/iam/horacius/checkUser",
verifyAPIKey,
IAMControllerEL.checkUser
);
iamRoutesEL.patch(
"/iam/horacius/linkUserAndUserRight",
verifyAPIKey,
IAMControllerEL.linkUserAndUserRight
);
iamRoutesEL.post(
"/iam/horacius/linkUserAndUserRight",
verifyAPIKey,
IAMControllerEL.checkUserRight
);
iamRoutesEL.patch(
"/iam/horacius/resetPassword",
verifyAPIKey,
IAMControllerEL.resetPassword
);
export default iamRoutesEL;

View File

@ -15,10 +15,12 @@ import schedulingNotifiyRoutes from "./SchedulingNotifyRoutes";
import statusChatEndRoutes from "./statusChatEndRoutes"; import statusChatEndRoutes from "./statusChatEndRoutes";
import hitRoutes from "./hitRoutes"; import hitRoutes from "./hitRoutes";
import wbotMonitorRoutes from "./wbotMonitorRoutes"; import wbotMonitorRoutes from "./wbotMonitorRoutes";
import iamRoutesEL from "./iamRoutesEL";
const routes = Router(); const routes = Router();
routes.use(iamRoutesEL);
routes.use(userRoutes); routes.use(userRoutes);
routes.use("/auth", authRoutes); routes.use("/auth", authRoutes);
routes.use(settingRoutes); routes.use(settingRoutes);

View File

@ -11,6 +11,8 @@ queueRoutes.post("/queue", isAuth, QueueController.store);
queueRoutes.post("/queue/customization", QueueController.customization); queueRoutes.post("/queue/customization", QueueController.customization);
queueRoutes.get("/queue/remote/list", isAuth, QueueController.listQueues);
queueRoutes.get("/queue/:queueId", isAuth, QueueController.show); queueRoutes.get("/queue/:queueId", isAuth, QueueController.show);
queueRoutes.put("/queue/:queueId", isAuth, QueueController.update); queueRoutes.put("/queue/:queueId", isAuth, QueueController.update);

View File

@ -7,12 +7,42 @@ import * as ReportController from "../controllers/ReportController";
const reportRoutes = express.Router(); const reportRoutes = express.Router();
reportRoutes.get("/reports", isAuth, ReportController.reportUserByDateStartDateEnd); reportRoutes.get(
"/reports",
isAuth,
ReportController.reportUserByDateStartDateEnd
);
reportRoutes.post("/reports/onqueue", ReportController.reportOnQueue); reportRoutes.post("/reports/onqueue", ReportController.reportOnQueue);
reportRoutes.get("/reports/user/services", isAuth, ReportController.reportUserService); reportRoutes.get(
"/reports/user/services",
isAuth,
ReportController.reportUserService
);
reportRoutes.get("/reports/messages", isAuth, ReportController.reportMessagesUserByDateStartDateEnd); reportRoutes.get(
"/reports/services/numbers",
isAuth,
ReportController.reportService
);
reportRoutes.get(
"/reports/services/queues",
isAuth,
ReportController.reportServiceByQueue
);
reportRoutes.get(
"/reports/messages",
isAuth,
ReportController.reportMessagesUserByDateStartDateEnd
);
reportRoutes.get(
"/reports/count/statusChatEnd",
isAuth,
ReportController.reportTicksCountByStatusChatEnds
);
export default reportRoutes; export default reportRoutes;

View File

@ -5,7 +5,6 @@ import * as TicketController from "../controllers/TicketController";
const ticketRoutes = express.Router(); const ticketRoutes = express.Router();
// ticketRoutes.get("/tickets/cache", isAuth, TicketController.ticketsCache); // ticketRoutes.get("/tickets/cache", isAuth, TicketController.ticketsCache);
ticketRoutes.get("/tickets/count", isAuth, TicketController.count); ticketRoutes.get("/tickets/count", isAuth, TicketController.count);
@ -14,6 +13,11 @@ ticketRoutes.get("/tickets", isAuth, TicketController.index);
ticketRoutes.get("/tickets/:ticketId", isAuth, TicketController.show); ticketRoutes.get("/tickets/:ticketId", isAuth, TicketController.show);
ticketRoutes.post(
"/tickets/remote/create", isAuth,
TicketController.remoteTicketCreation
);
ticketRoutes.post("/tickets", isAuth, TicketController.store); ticketRoutes.post("/tickets", isAuth, TicketController.store);
ticketRoutes.put("/tickets/:ticketId", isAuth, TicketController.update); ticketRoutes.put("/tickets/:ticketId", isAuth, TicketController.update);

View File

@ -23,6 +23,11 @@ import fs from "fs";
import dir from "path"; import dir from "path";
import { getSettingValue } from "./helpers/WhaticketSettings"; import { getSettingValue } from "./helpers/WhaticketSettings";
import loadSettings from "./helpers/LoadSettings"; import loadSettings from "./helpers/LoadSettings";
import { clearAllKeys, get, set } from "./helpers/RedisClient";
import ShowUserService from "./services/UserServices/ShowUserService";
import { json } from "sequelize";
import { setBotInfo } from "./helpers/SetBotInfo";
import Queue from "./models/Queue";
const server = app.listen(process.env.PORT, () => { const server = app.listen(process.env.PORT, () => {
logger.info(`Server started on port: ${process.env.PORT}`); logger.info(`Server started on port: ${process.env.PORT}`);
@ -43,18 +48,54 @@ gracefulShutdown(server);
(async () => { (async () => {
console.log("os.tmpdir(): ", os.tmpdir()); console.log("os.tmpdir(): ", os.tmpdir());
await clearAllKeys("user:*", "whatsapp:*", "queue:*");
const users = await User.findAll();
for (const user of users) {
const { id, name } = user;
if (name == "botqueue") {
const userService = await ShowUserService(id);
await setBotInfo(userService);
}
await set(`user:${id}`, { id, name });
}
// const queues = await Queue.findAll();
// for (const queue of queues) {
// const { id, greetingMessage, name } = queue;
// await set(`queue:${id}`, { id, name, greetingMessage });
// }
loadSettings(); loadSettings();
let whatsapps: any = await Whatsapp.findAll({ let whatsapps: any = await Whatsapp.findAll({
attributes: ["id", "url", "phoneNumberId"] attributes: ["id", "url", "phoneNumberId", "number", "greetingMessage"]
}); });
if (whatsapps && whatsapps.length > 0) { if (whatsapps && whatsapps.length > 0) {
for (let i = 0; i < whatsapps.length; i++) { for (let i = 0; i < whatsapps.length; i++) {
try { try {
const { phoneNumberId } = whatsapps[i]; const { phoneNumberId, id, greetingMessage } = whatsapps[i];
if (phoneNumberId) continue; if (phoneNumberId) {
await set(
`whatsapp:${whatsapps[i].dataValues.id}`,
JSON.stringify({
number: whatsapps[i].dataValues.number,
id,
greetingMessage,
phoneNumberId
})
);
}
if (phoneNumberId) {
continue;
}
console.log( console.log(
`API URL: ${whatsapps[i].dataValues.url}/api/connection/status` `API URL: ${whatsapps[i].dataValues.url}/api/connection/status`

View File

@ -0,0 +1,38 @@
import AppError from "../../errors/AppError";
import Contact from "../../models/Contact";
import CreateContactService from "./CreateContactService";
interface ExtraInfo {
name: string;
value: string;
}
interface Request {
name: string;
number: string;
email?: string;
profilePicUrl?: string;
extraInfo?: ExtraInfo[];
}
const GetContactService = async ({ name, number }: Request): Promise<Contact> => {
const numberExists = await Contact.findOne({
where: { number }
});
if (!numberExists) {
const contact = await CreateContactService({
name,
number,
})
if (contact == null)
throw new AppError("CONTACT_NOT_FIND")
else
return contact
}
return numberExists
};
export default GetContactService;

View File

@ -13,6 +13,7 @@ interface MessageData {
read?: boolean; read?: boolean;
mediaType?: string; mediaType?: string;
mediaUrl?: string; mediaUrl?: string;
fromAgent?: boolean;
} }
interface Request { interface Request {
messageData: MessageData; messageData: MessageData;

View File

@ -1,6 +1,7 @@
import * as Yup from "yup"; import * as Yup from "yup";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import Queue from "../../models/Queue"; import Queue from "../../models/Queue";
import { set } from "../../helpers/RedisClient";
interface QueueData { interface QueueData {
name: string; name: string;
@ -9,68 +10,67 @@ interface QueueData {
} }
const CreateQueueService = async (queueData: QueueData): Promise<Queue> => { const CreateQueueService = async (queueData: QueueData): Promise<Queue> => {
try { try {
const { color, name } = queueData; const { color, name } = queueData;
const queueSchema = Yup.object().shape({ const queueSchema = Yup.object().shape({
name: Yup.string() name: Yup.string()
.min(2, "ERR_QUEUE_INVALID_NAME") .min(2, "ERR_QUEUE_INVALID_NAME")
.required("ERR_QUEUE_INVALID_NAME") .required("ERR_QUEUE_INVALID_NAME")
.test( .test(
"Check-unique-name", "Check-unique-name",
"ERR_QUEUE_NAME_ALREADY_EXISTS", "ERR_QUEUE_NAME_ALREADY_EXISTS",
async value => { async value => {
if (value) { if (value) {
const queueWithSameName = await Queue.findOne({ const queueWithSameName = await Queue.findOne({
where: { name: value } where: { name: value }
}); });
return !queueWithSameName; return !queueWithSameName;
}
return false;
}
),
color: Yup.string()
.required("ERR_QUEUE_INVALID_COLOR")
.test("Check-color", "ERR_QUEUE_INVALID_COLOR", async value => {
if (value) {
const colorTestRegex = /^#[0-9a-f]{3,6}$/i;
return colorTestRegex.test(value);
} }
return false; return false;
} })
), .test(
color: Yup.string() "Check-color-exists",
.required("ERR_QUEUE_INVALID_COLOR") "ERR_QUEUE_COLOR_ALREADY_EXISTS",
.test("Check-color", "ERR_QUEUE_INVALID_COLOR", async value => { async value => {
if (value) { if (value) {
const colorTestRegex = /^#[0-9a-f]{3,6}$/i; const queueWithSameColor = await Queue.findOne({
return colorTestRegex.test(value); where: { color: value }
} });
return false; return !queueWithSameColor;
}) }
.test( return false;
"Check-color-exists",
"ERR_QUEUE_COLOR_ALREADY_EXISTS",
async value => {
if (value) {
const queueWithSameColor = await Queue.findOne({
where: { color: value }
});
return !queueWithSameColor;
} }
return false; )
} });
)
});
try { try {
await queueSchema.validate({ color, name }); await queueSchema.validate({ color, name });
} catch (err: any) { } catch (err: any) {
throw new AppError(err.message); throw new AppError(err.message);
} }
const queue = await Queue.create(queueData); const queue = await Queue.create(queueData);
return queue; // const { id, greetingMessage } = queue;
// await set(`queue:${id}`, { id, name, greetingMessage });
return queue;
} catch (error: any) { } catch (error: any) {
console.error('===> Error on CreateQueueService.ts file: \n', error) console.error("===> Error on CreateQueueService.ts file: \n", error);
throw new AppError(error.message); throw new AppError(error.message);
} }
}; };
export default CreateQueueService; export default CreateQueueService;

View File

@ -3,6 +3,7 @@ import * as Yup from "yup";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import Queue from "../../models/Queue"; import Queue from "../../models/Queue";
import ShowQueueService from "./ShowQueueService"; import ShowQueueService from "./ShowQueueService";
import { set } from "../../helpers/RedisClient"
interface QueueData { interface QueueData {
name?: string; name?: string;
@ -14,9 +15,7 @@ const UpdateQueueService = async (
queueId: number | string, queueId: number | string,
queueData: QueueData queueData: QueueData
): Promise<Queue> => { ): Promise<Queue> => {
try { try {
const { color, name } = queueData; const { color, name } = queueData;
const queueSchema = Yup.object().shape({ const queueSchema = Yup.object().shape({
@ -70,13 +69,14 @@ const UpdateQueueService = async (
await queue.update(queueData); await queue.update(queueData);
return queue; // const { id, greetingMessage } = queue;
// await set(`queue:${id}`, { id, name, greetingMessage });
return queue;
} catch (error: any) { } catch (error: any) {
console.error('===> Error on UpdateQueueService.ts file: \n', error) console.error("===> Error on UpdateQueueService.ts file: \n", error);
throw new AppError(error.message); throw new AppError(error.message);
} }
}; };
export default UpdateQueueService; export default UpdateQueueService;

View File

@ -0,0 +1,333 @@
import { Sequelize } from "sequelize";
const dbConfig = require("../../config/database");
const sequelize = new Sequelize(dbConfig);
const { QueryTypes } = require("sequelize");
import { splitDateTime } from "../../helpers/SplitDateTime";
import format from "date-fns/format";
import ptBR from "date-fns/locale/pt-BR";
import Whatsapp from "../../models/Whatsapp";
import { number } from "yup";
import ShowWhatsAppService from "../WhatsappService/ShowWhatsAppService";
interface Request {
startDate: string | number;
endDate: string;
queue?: boolean;
}
const ReportByNumberQueueService = async ({
startDate,
endDate,
queue = false
}: Request): Promise<any[]> => {
let reportServiceData: any[] = [];
const whatsapps = await Whatsapp.findAll();
if (!queue) {
for (const whatsapp of whatsapps) {
const { id, name, number } = whatsapp;
if (
!number ||
reportServiceData.findIndex((w: any) => w?.number == number) != -1
)
continue;
console.log("NUMBER: ", number);
// CHAT STARTED BY AGENT
const startedByAgent: any = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t
JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromAgent = 1
AND w.number = ${number};`,
{ type: QueryTypes.SELECT }
);
// CHAT STARTED BY CLIENT
const startedByClient: any = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t
JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromMe = 0
AND w.number = ${number};`,
{ type: QueryTypes.SELECT }
);
// CHAT CLOSED
const closedChat: any = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t
JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND t.status = 'closed'
AND w.number = ${number};`,
{ type: QueryTypes.SELECT }
);
// CHAT WAINTING TIME
const avgChatWaitingTime: any = await sequelize.query(
`
SELECT TIME_FORMAT(
SEC_TO_TIME(
TIMESTAMPDIFF(
SECOND,
(
SELECT createdAt
FROM Messages
WHERE ticketId = m.ticketId
AND fromMe = 0
ORDER BY createdAt ASC
LIMIT 1
),
(
SELECT createdAt
FROM Messages
WHERE ticketId = m.ticketId
AND fromAgent = 1
ORDER BY createdAt ASC
LIMIT 1
)
)
), '%H:%i:%s') AS WAITING_TIME
FROM Tickets t
JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromMe = 0
-- AND q.id = 2
AND w.number = ${number}
AND t.status IN ('open', 'closed')
HAVING WAITING_TIME IS NOT NULL
ORDER BY
WAITING_TIME;`,
{ type: QueryTypes.SELECT }
);
// CHAT PENDING
const pendingChat: any = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t
JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND t.status = 'pending'
AND w.number = ${number};`,
{ type: QueryTypes.SELECT }
);
reportServiceData.push({
id,
name,
number,
startedByAgent: startedByAgent[0]?.ticket_count,
startedByClient: startedByClient[0]?.ticket_count,
closedChat: closedChat[0]?.ticket_count,
avgChatWaitingTime: avg(avgChatWaitingTime),
pendingChat: pendingChat[0]?.ticket_count
});
}
} else {
for (const whatsapp of whatsapps) {
const { id, name, number } = whatsapp;
if (
!number ||
reportServiceData.findIndex((w: any) => w?.number == number) != -1
)
continue;
const data = await ShowWhatsAppService(id);
const queues: any = data.queues.map((q: any) => {
const { id, name, color } = q;
return { id, name, color };
});
console.log("NUMBER 2: ", number);
for (const q of queues) {
// CHAT STARTED BY AGENT
const startedByAgent: any = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t
JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromAgent = 1
AND q.id = ${q.id};`,
{ type: QueryTypes.SELECT }
);
// CHAT STARTED BY CLIENT
const startedByClient: any = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t
JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromMe = 0
AND q.id = ${q.id};`,
{ type: QueryTypes.SELECT }
);
// CHAT CLOSED
const closedChat: any = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t
JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND t.status = 'closed'
AND q.id = ${q.id};`,
{ type: QueryTypes.SELECT }
);
// CHAT WAINTING TIME
const avgChatWaitingTime: any = await sequelize.query(
`SELECT TIME_FORMAT(
SEC_TO_TIME(
TIMESTAMPDIFF(
SECOND,
(
SELECT createdAt
FROM Messages
WHERE ticketId = m.ticketId
AND fromMe = 0
ORDER BY createdAt ASC
LIMIT 1
),
(
SELECT createdAt
FROM Messages
WHERE ticketId = m.ticketId
AND fromAgent = 1
ORDER BY createdAt ASC
LIMIT 1
)
)
), '%H:%i:%s') AS WAITING_TIME
FROM Tickets t
JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromMe = 0
AND q.id = ${q.id}
AND t.status IN ('open', 'closed')
HAVING WAITING_TIME IS NOT NULL
ORDER BY
WAITING_TIME;`,
{ type: QueryTypes.SELECT }
);
// CHAT PENDING
const pendingChat: any = await sequelize.query(
`SELECT COUNT(DISTINCT t.id) AS ticket_count
FROM Tickets t
JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND t.status = 'pending'
AND q.id = ${q.id};`,
{ type: QueryTypes.SELECT }
);
reportServiceData.push({
id,
name,
number,
queueName: q.name,
queueColor: q.color,
startedByAgent: startedByAgent[0]?.ticket_count,
startedByClient: startedByClient[0]?.ticket_count,
closedChat: closedChat[0]?.ticket_count,
avgChatWaitingTime: avg(avgChatWaitingTime),
pendingChat: pendingChat[0]?.ticket_count
});
}
}
}
return reportServiceData;
};
export default ReportByNumberQueueService;
function avg(avgChatWaitingTime: any) {
let waitingAVG: any = avgChatWaitingTime
.filter((t: any) => t?.WAITING_TIME)
.map((t: any) => t.WAITING_TIME)
if (waitingAVG.length > 0) {
let midIndex = Math.floor((0 + waitingAVG.length) / 2)
if (waitingAVG.length % 2 == 1) {
waitingAVG = waitingAVG[midIndex]
} else {
waitingAVG = calculateAverageTime(
waitingAVG[midIndex - 1],
waitingAVG[midIndex]
)
}
} else {
waitingAVG = 0
}
return waitingAVG
}
function calculateAverageTime(time1: string, time2: string) {
// Function to parse time string to seconds
function timeStringToSeconds(timeString: string) {
const [hours, minutes, seconds] = timeString.split(":").map(Number);
return hours * 3600 + minutes * 60 + seconds;
}
// Function to convert seconds to time string
function secondsToTimeString(seconds: number) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
return `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`;
}
// Convert time strings to seconds
const time1Seconds = timeStringToSeconds(time1);
const time2Seconds = timeStringToSeconds(time2);
// Calculate average seconds
const averageSeconds = Math.round((time1Seconds + time2Seconds) / 2);
// Convert average seconds back to time string
const averageTime = secondsToTimeString(averageSeconds);
return averageTime;
}

View File

@ -0,0 +1,27 @@
import StatusChatEnd from "../../models/StatusChatEnd";
import AppError from "../../errors/AppError";
import { Sequelize } from "sequelize";
import { splitDateTime } from "../../helpers/SplitDateTime"
import ptBR from "date-fns/locale/pt-BR";
import { format } from "date-fns"
const dbConfig = require("../../config/database");
const sequelize = new Sequelize(dbConfig);
const { QueryTypes } = require("sequelize");
const CountStatusChatEndService = async (
startDate: string,
endDate: string
) => {
const countStatusChatEnd: any = 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'
group by s.id;`,
{ type: QueryTypes.SELECT }
);
return countStatusChatEnd;
};
export default CountStatusChatEndService;

View File

@ -14,14 +14,11 @@ const FindOrCreateTicketService = async (
whatsappId: number, whatsappId: number,
unreadMessages: number, unreadMessages: number,
groupContact?: Contact, groupContact?: Contact,
queueId?: number | string
): Promise<Ticket> => { ): Promise<Ticket> => {
try { try {
let ticket; let ticket;
// else if (getSettingValue("whatsaAppCloudApi")?.value == "enabled") {
// }
if (getSettingValue("oneContactChatWithManyWhats")?.value == "enabled") { if (getSettingValue("oneContactChatWithManyWhats")?.value == "enabled") {
let whats = await ListWhatsAppsNumber(whatsappId); let whats = await ListWhatsAppsNumber(whatsappId);
@ -45,7 +42,8 @@ const FindOrCreateTicketService = async (
}); });
} }
const { queues, greetingMessage, phoneNumberId } = await ShowWhatsAppService(whatsappId); const { queues, greetingMessage, phoneNumberId } =
await ShowWhatsAppService(whatsappId);
const botInfo = { isOnQueue: false }; const botInfo = { isOnQueue: false };
@ -97,7 +95,7 @@ const FindOrCreateTicketService = async (
if (!ticket) { if (!ticket) {
let status = "pending"; let status = "pending";
if (queues.length > 1 && !botInfo.isOnQueue) { if (queues.length > 1 && !botInfo.isOnQueue && !queueId) {
status = "queueChoice"; status = "queueChoice";
} }
@ -105,6 +103,7 @@ const FindOrCreateTicketService = async (
contactId: groupContact ? groupContact.id : contact.id, contactId: groupContact ? groupContact.id : contact.id,
status: status, status: status,
isGroup: !!groupContact, isGroup: !!groupContact,
queueId,
unreadMessages, unreadMessages,
whatsappId, whatsappId,
phoneNumberId phoneNumberId

View File

@ -29,7 +29,8 @@ const FindOrCreateTicketServiceBot = async (
} }
}); });
const { queues, greetingMessage } = await ShowWhatsAppService(whatsappId); const { queues, greetingMessage, phoneNumberId } =
await ShowWhatsAppService(whatsappId);
//Habilitar esse caso queira usar o bot //Habilitar esse caso queira usar o bot
@ -102,12 +103,13 @@ const FindOrCreateTicketServiceBot = async (
} }
ticket = await Ticket.create({ ticket = await Ticket.create({
contactId: groupContact ? groupContact.id : contact.id, contactId: groupContact ? groupContact.id : contact.id,
status: status, status: status,
userId: botInfo.userIdBot, userId: botInfo.userIdBot,
isGroup: !!groupContact, isGroup: !!groupContact,
unreadMessages, unreadMessages,
whatsappId whatsappId,
phoneNumberId
}); });
console.log('yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy') console.log('yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy')

View File

@ -61,11 +61,6 @@ const ListTicketsService = async ({
console.log('PAGE NUMBER TICKET: ', pageNumber) console.log('PAGE NUMBER TICKET: ', pageNumber)
//TEST DEL
// const url = await getWbot(46)
// console.log('---------> URL: ', url)
//
if (pageNumber.trim().length == 0) { if (pageNumber.trim().length == 0) {
pageNumber = '1' pageNumber = '1'
} }
@ -136,15 +131,14 @@ const ListTicketsService = async ({
whereCondition = { ...whereCondition, status }; whereCondition = { ...whereCondition, status };
if (unlimited === 'true' && status !== 'pending') { if (unlimited === "current" && status !== "pending") {
whereCondition = { whereCondition = {
...whereCondition, ...whereCondition,
createdAt: { createdAt: {
[Op.gte]: dateToday.fullDate + ' 00:00:00.000000', [Op.gte]: dateToday.fullDate + " 00:00:00.000000",
[Op.lte]: dateToday.fullDate + ' 23:59:59.999999' [Op.lte]: dateToday.fullDate + " 23:59:59.999999"
} }
} };
} }
} }
@ -196,8 +190,8 @@ const ListTicketsService = async ({
if ( if (
userProfile.dataValues.profile != "admin" && userProfile.dataValues.profile != "admin" &&
userProfile.dataValues.profile != "master" userProfile.dataValues.profile != "master" &&
// userProfile.dataValues.profile != "supervisor" userProfile.dataValues.profile != "supervisor"
) { ) {
whereCondition = { ...whereCondition, userId }; whereCondition = { ...whereCondition, userId };
} }
@ -224,9 +218,10 @@ const ListTicketsService = async ({
}; };
} }
const limit = unlimited === 'true' ? 100000 : 40; const limit = unlimited === "current" || unlimited === "all" ? 100000 : 40;
const offset = limit * (+pageNumber - 1); const offset = limit * (+pageNumber - 1);
console.log("kkkkkkkkk limit: ", limit);
const { count, rows: tickets } = await Ticket.findAndCountAll({ const { count, rows: tickets } = await Ticket.findAndCountAll({
where: whereCondition, where: whereCondition,

View File

@ -7,23 +7,28 @@ import Queue from "../../models/Queue";
import Message from "../../models/Message"; import Message from "../../models/Message";
import { userInfo } from "os"; import { userInfo } from "os";
import { Op, where } from "sequelize"; import { Op, QueryTypes, json, where } from "sequelize";
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
import moment from 'moment'; import moment from "moment";
const dbConfig = require("../../config/database");
const sequelize = new Sequelize(dbConfig);
import { startOfDay, endOfDay, parseISO, getDate} from "date-fns"; import { startOfDay, endOfDay, parseISO, getDate } from "date-fns";
import { string } from "yup/lib/locale"; import { string } from "yup/lib/locale";
import Whatsapp from "../../models/Whatsapp"; import Whatsapp from "../../models/Whatsapp";
import Query from "mysql2/typings/mysql/lib/protocol/sequences/Query";
import { te } from "date-fns/locale";
interface Request { interface Request {
userId: string | number; userId: string | number;
startDate: string; startDate: string;
endDate: string; endDate: string;
createdOrUpdated?: string;
queueId?: string;
pageNumber?: string; pageNumber?: string;
} }
interface Response { interface Response {
tickets: Ticket[]; tickets: Ticket[];
count: number; count: number;
@ -35,89 +40,182 @@ const ShowTicketReport = async ({
userId, userId,
startDate, startDate,
endDate, endDate,
pageNumber = "1" pageNumber = "1",
createdOrUpdated = "created",
queueId
}: Request): Promise<Response> => { }: Request): Promise<Response> => {
// let where_clause: any = {};
// let query = "";
let where_clause = {} // if (userId !== "0") {
// where_clause.userid = userId;
// query = `AND t.userId = ${userId}`;
// }
if(userId=='0'){ // if (queueId) {
// where_clause.queueId = queueId;
// query = `AND t.queueId = ${queueId}`;
// }
const createdAtOrUpdatedAt =
createdOrUpdated == "created" ? "createdAt" : "updatedAt";
let where_clause = {};
if (queueId) {
where_clause = { where_clause = {
updatedAt: { queueId: queueId,
[Op.gte]: startDate+' 00:00:00.000000', [createdAtOrUpdatedAt]: {
[Op.lte]: endDate +' 23:59:59.999999' [Op.gte]: startDate + " 00:00:00.000000",
}, [Op.lte]: endDate + " 23:59:59.999999"
} }
} };
else{ } else if (userId == "0") {
where_clause = {
[createdAtOrUpdatedAt]: {
[Op.gte]: startDate + " 00:00:00.000000",
[Op.lte]: endDate + " 23:59:59.999999"
}
};
} else if (userId != "0") {
where_clause = { where_clause = {
userid: userId, userid: userId,
updatedAt: { [createdAtOrUpdatedAt]: {
[Op.gte]: startDate+' 00:00:00.000000', [Op.gte]: startDate + " 00:00:00.000000",
[Op.lte]: endDate +' 23:59:59.999999' [Op.lte]: endDate + " 23:59:59.999999"
}, }
} };
} }
const limit = 40; const limit = 40;
const offset = limit * (+pageNumber - 1); const offset = limit * (+pageNumber - 1);
const {count, rows: tickets} = await Ticket.findAndCountAll({ let { count, rows: tickets }: any = await Ticket.findAndCountAll({
where: where_clause,
where: where_clause ,
limit, limit,
offset, offset,
//attributes: ['id', 'status', 'createdAt', 'updatedAt'], attributes: [
"id",
attributes: ['id', 'status', 'statusChatEnd', [Sequelize.fn("DATE_FORMAT",Sequelize.col("Ticket.createdAt"),"%d/%m/%Y %H:%i:%s"),"createdAt"], "status",
[Sequelize.fn("DATE_FORMAT",Sequelize.col("Ticket.updatedAt"),"%d/%m/%Y %H:%i:%s"),"updatedAt"]], "statusChatEnd",
[
Sequelize.fn(
"DATE_FORMAT",
Sequelize.col("Ticket.createdAt"),
"%d/%m/%Y %H:%i:%s"
),
"createdAt"
],
[
Sequelize.fn(
"DATE_FORMAT",
Sequelize.col("Ticket.updatedAt"),
"%d/%m/%Y %H:%i:%s"
),
"updatedAt"
]
],
include: [ include: [
{ {
model: Message, model: Message,
required:true, required: true,
separate: true, separate: true,
// attributes: ['body', 'read', 'mediaType','fromMe', 'mediaUrl','createdAt'], attributes: [
"body",
attributes: ['body', 'read', 'mediaType','fromMe', 'mediaUrl', [Sequelize.fn("DATE_FORMAT",Sequelize.col("createdAt"),"%d/%m/%Y %H:%i:%s"),"createdAt"]], "read",
"mediaType",
order: [ "fromMe",
['createdAt', 'ASC'] "mediaUrl",
] [
Sequelize.fn(
"DATE_FORMAT",
Sequelize.col("createdAt"),
"%d/%m/%Y %H:%i:%s"
),
"createdAt"
]
],
order: [["createdAt", "ASC"]]
}, },
{ {
model: Contact, model: Contact,
attributes: ['name', 'number'] attributes: ["name", "number"]
}, },
{ {
model: User, model: User,
attributes: ['name', 'email'] attributes: ["name", "email"]
}, },
{ {
model: Queue, model: Queue,
attributes: ['name'] attributes: ["name"]
}, },
{ {
model: Whatsapp, model: Whatsapp,
attributes: ['name'] attributes: ["name"]
}, }
], ],
order: [["updatedAt", "DESC"]]
order: [
['id', 'ASC']
]
}); });
const hasMore = count > offset + tickets.length; const hasMore = count > offset + tickets.length;
if (!tickets) { if (!tickets) {
throw new AppError("ERR_NO_TICKET_FOUND", 404); throw new AppError("ERR_NO_TICKET_FOUND", 404);
} }
return {tickets, count, hasMore}; if (tickets.length > 0) {
const waiting_time: any = await sequelize.query(
`SELECT t.id as ticketId, t.status, TIME_FORMAT(
SEC_TO_TIME(
TIMESTAMPDIFF(
SECOND,
(
SELECT createdAt
FROM Messages
WHERE ticketId = m.ticketId
AND fromMe = 0
ORDER BY createdAt ASC
LIMIT 1
),
(
SELECT createdAt
FROM Messages
WHERE ticketId = m.ticketId
AND fromAgent = 1
ORDER BY createdAt ASC
LIMIT 1
)
)
), '%H:%i:%s') AS WAITING_TIME
FROM Tickets t
JOIN Messages m ON t.id = m.ticketId
JOIN Whatsapps w ON t.whatsappId = w.id
JOIN Queues q ON q.id = t.queueId
WHERE DATE(m.createdAt) BETWEEN '${startDate} 00:00:00.000000' AND '${endDate} 23:59:59.999999'
AND t.id IN (${tickets.map((t: any) => t.id).join()})
AND m.createdAt = (SELECT MIN(createdAt) FROM Messages WHERE ticketId = t.id)
AND m.fromMe = 0
AND t.status IN ('open', 'closed')
HAVING WAITING_TIME IS NOT NULL
ORDER BY
WAITING_TIME;`,
{ type: QueryTypes.SELECT }
);
for (let w of waiting_time) {
const { ticketId, status, WAITING_TIME } = w;
const index = tickets.findIndex((t: any) => +t?.id == +ticketId);
if (index != -1) {
tickets[index].dataValues.waiting_time = WAITING_TIME;
}
}
}
return { tickets, count, hasMore };
}; };
export default ShowTicketReport; export default ShowTicketReport;

View File

@ -10,7 +10,7 @@ import { createOrUpdateTicketCache } from "../../helpers/TicketCache";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import sendWhatsAppMessageSocket from "../../helpers/SendWhatsappMessageSocket"; import sendWhatsAppMessageSocket from "../../helpers/SendWhatsappMessageSocket";
import BotIsOnQueue from "../../helpers/BotIsOnQueue"; import BotIsOnQueue from "../../helpers/BotIsOnQueue";
import { deleteObject } from "../../helpers/RedisClient" import { deleteObject } from "../../helpers/RedisClient";
var flatten = require("flat"); var flatten = require("flat");
interface TicketData { interface TicketData {
@ -18,6 +18,7 @@ interface TicketData {
userId?: number; userId?: number;
queueId?: number; queueId?: number;
statusChatEnd?: string; statusChatEnd?: string;
statusChatEndId?: number;
unreadMessages?: number; unreadMessages?: number;
whatsappId?: string | number; whatsappId?: string | number;
} }
@ -46,6 +47,7 @@ const UpdateTicketService = async ({
queueId, queueId,
statusChatEnd, statusChatEnd,
unreadMessages, unreadMessages,
statusChatEndId,
whatsappId whatsappId
} = ticketData; } = ticketData;
@ -73,6 +75,7 @@ const UpdateTicketService = async ({
userId, userId,
unreadMessages, unreadMessages,
statusChatEnd, statusChatEnd,
statusChatEndId,
whatsappId whatsappId
}); });
@ -80,7 +83,7 @@ const UpdateTicketService = async ({
if (msg?.trim().length > 0) { if (msg?.trim().length > 0) {
setTimeout(async () => { setTimeout(async () => {
sendWhatsAppMessageSocket(ticket, msg); sendWhatsAppMessageSocket(ticket, `\u200e${msg}`);
}, 2000); }, 2000);
} }

View File

@ -11,6 +11,7 @@ interface Request {
positionCompany?: string; positionCompany?: string;
queueIds?: number[]; queueIds?: number[];
profile?: string; profile?: string;
ignoreThrow?: boolean;
} }
interface Response { interface Response {
@ -27,25 +28,27 @@ const CreateUserService = async ({
name, name,
positionCompany, positionCompany,
queueIds = [], queueIds = [],
profile = "master" profile = "master",
}: Request): Promise<Response> => { ignoreThrow = false
}: Request): Promise<Response | any> => {
try { try {
const schema = Yup.object().shape({ const schema = Yup.object().shape({
name: Yup.string().required().min(2), name: Yup.string().required().min(2),
email: Yup.string().required().trim().test( email: Yup.string()
"Check-email", .required()
"An user with this email already exists.", .trim()
async value => { .test(
if (!value) return false; "Check-email",
const emailExists = await User.findOne({ "An user with this email already exists.",
where: { email: value } async value => {
}); if (!value) return false;
return !emailExists; const emailExists = await User.findOne({
} where: { email: value }
), });
return !emailExists;
}
),
// email: Yup.string().email().required().test( // email: Yup.string().email().required().test(
// "Check-email", // "Check-email",
@ -65,6 +68,8 @@ const CreateUserService = async ({
try { try {
await schema.validate({ email, password, name }); await schema.validate({ email, password, name });
} catch (err: any) { } catch (err: any) {
if (ignoreThrow) return { error: true, msg: err.message, status: 400 };
throw new AppError(err.message); throw new AppError(err.message);
} }
@ -86,12 +91,14 @@ const CreateUserService = async ({
const serializedUser = SerializeUser(user); const serializedUser = SerializeUser(user);
return serializedUser; return serializedUser;
} catch (error: any) { } catch (error: any) {
console.error('===> Error on CreateUserService.ts file: \n', error) console.error("===> Error on CreateUserService.ts file: \n", error);
if (ignoreThrow)
return { error: true, msg: "Create user error", status: 500 };
throw new AppError(error.message); throw new AppError(error.message);
} }
}; };
export default CreateUserService; export default CreateUserService;

View File

@ -2,14 +2,24 @@ import User from "../../models/User";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import Ticket from "../../models/Ticket"; import Ticket from "../../models/Ticket";
import UpdateDeletedUserOpenTicketsStatus from "../../helpers/UpdateDeletedUserOpenTicketsStatus"; import UpdateDeletedUserOpenTicketsStatus from "../../helpers/UpdateDeletedUserOpenTicketsStatus";
import { set } from "../../helpers/RedisClient" import { set } from "../../helpers/RedisClient";
const DeleteUserService = async (id: string | number): Promise<void> => { const DeleteUserService = async (
id: string | number,
ignoreThrow = false
): Promise<void | any> => {
const user = await User.findOne({ const user = await User.findOne({
where: { id } where: { id }
}); });
if (!user) { if (!user) {
if (ignoreThrow)
return {
error: true,
msg: `No user found with this id ${id}`,
status: 404
};
throw new AppError("ERR_NO_USER_FOUND", 404); throw new AppError("ERR_NO_USER_FOUND", 404);
} }

View File

@ -2,7 +2,7 @@ import { Op, Sequelize } from "sequelize";
import Queue from "../../models/Queue"; import Queue from "../../models/Queue";
import User from "../../models/User"; import User from "../../models/User";
import UserQueue from "../../models/UserQueue"; import UserQueue from "../../models/UserQueue";
import { List } from "whatsapp-web.js" import { List } from "whatsapp-web.js";
interface Request { interface Request {
userId?: string | number; userId?: string | number;
@ -12,7 +12,13 @@ interface Request {
userIds?: string | number; userIds?: string | number;
} }
const ListUser = async ({ profile, userId, raw, userIds, profiles }: Request): Promise<User[]> => { const ListUser = async ({
profile,
userId,
raw,
userIds,
profiles
}: Request): Promise<User[]> => {
let where_clause = {}; let where_clause = {};
if (userId && profile) { if (userId && profile) {
@ -47,7 +53,7 @@ const ListUser = async ({ profile, userId, raw, userIds, profiles }: Request): P
], ],
order: [["id", "ASC"]], order: [["id", "ASC"]],
group: ["User.id"] group: userIds ? undefined : ["User.id"]
}); });
return users; return users;

View File

@ -0,0 +1,37 @@
import { Op, Sequelize } from "sequelize";
import Whatsapp from "../../models/Whatsapp";
import WhatsappQueue from "../../models/WhatsappQueue";
import { List } from "whatsapp-web.js";
import UserQueue from "../../models/UserQueue";
import Queue from "../../models/Queue";
const dbConfig = require("../../config/database");
const { QueryTypes } = require("sequelize");
const sequelize = new Sequelize(dbConfig);
const ListWhatsappQueuesByUserQueue = async (userId: string | number) => {
try {
let userQueue: any = await UserQueue.findAll({
where: { userId },
attributes: ["queueId"],
raw: true
});
if (userQueue && userQueue.length > 0) {
userQueue = userQueue.map((u: any) => u.queueId);
const result = await sequelize.query(
`select w.id, w.number, wq.whatsappId, wq.queueId, q.id, q.name from WhatsappQueues wq join Queues q on
wq.queueId = q.id join Whatsapps w
on wq.whatsappId = w.id where q.id in (${userQueue.join()})`,
{ type: QueryTypes.SELECT }
);
return result;
}
} catch (error) {
console.error("Error fetching joined data:", error);
}
return [];
};
export default ListWhatsappQueuesByUserQueue;

View File

@ -16,6 +16,7 @@ interface UserData {
interface Request { interface Request {
userData: UserData; userData: UserData;
userId: string | number; userId: string | number;
ignoreThrow?: boolean;
} }
interface Response { interface Response {
@ -27,11 +28,10 @@ interface Response {
const UpdateUserService = async ({ const UpdateUserService = async ({
userData, userData,
userId userId,
}: Request): Promise<Response | undefined> => { ignoreThrow = false
}: Request): Promise<Response | undefined | any> => {
try { try {
const user = await ShowUserService(userId); const user = await ShowUserService(userId);
const schema = Yup.object().shape({ const schema = Yup.object().shape({
@ -40,28 +40,41 @@ const UpdateUserService = async ({
profile: Yup.string(), profile: Yup.string(),
password: Yup.string(), password: Yup.string(),
email: Yup.string().trim().required().test( email: Yup.string()
"Check-email", .trim()
"An user with this email already exists.", .required()
async value => { .test(
"Check-email",
"An user with this email already exists.",
async value => {
if (!value) return false;
if (!value) return false; const emailExists = await User.findOne({
where: { email: value },
raw: true,
attributes: ["email", "id"]
});
const emailExists = await User.findOne({ where: { email: value }, raw: true, attributes: ['email', 'id'] }); if (emailExists && user.id != emailExists?.id) {
console.error(
"The email already exists in another user profile!"
);
return !emailExists;
}
if (emailExists && user.id != emailExists?.id) { return true;
console.error('The email already exists in another user profile!')
return !emailExists;
} }
)
return true
}
),
}); });
const { email, password, profile, name, positionCompany, 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 });
@ -69,7 +82,6 @@ const UpdateUserService = async ({
throw new AppError(err.message); throw new AppError(err.message);
} }
await user.update({ await user.update({
email, email,
password, password,
@ -91,13 +103,18 @@ const UpdateUserService = async ({
}; };
return serializedUser; return serializedUser;
} catch (err: any) {
console.error("===> Error on UpdateUserService.ts file: \n", err);
} catch (error: any) { if (ignoreThrow)
console.error('===> Error on UpdateUserService.ts file: \n', error) return {
throw new AppError(error.message); error: true,
msg: err.message,
status: 500
};
throw new AppError(err.message);
} }
}; };
export default UpdateUserService; export default UpdateUserService;

View File

@ -1,25 +1,60 @@
import axios from "axios";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import endPointQuery from "../../helpers/EndPointQuery"; import endPointQuery from "../../helpers/EndPointQuery";
import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp"; import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp";
import { getWbot } from "../../libs/wbot"; import { getWbot } from "../../libs/wbot";
const CheckIsValidContact = async (number: string): Promise<any> => { const CheckIsValidContact = async (
number: string,
ignoreThrow?: boolean
): Promise<any> => {
const defaultWhatsapp = await GetDefaultWhatsApp({
ignoreNoWhatsappFound: true
});
const defaultWhatsapp = await GetDefaultWhatsApp({}); let isValidNumber;
const wbot_url = await getWbot(defaultWhatsapp.id); if (defaultWhatsapp) {
const wbot_url = await getWbot(defaultWhatsapp.id);
const isValidNumber = await endPointQuery(`${wbot_url}/api/validate`, { mobile: `${number}`, }) let { data } = await endPointQuery(`${wbot_url}/api/validate`, {
mobile: `${number}`
});
// console.log('isValidNumber.data.number: ', isValidNumber.data.number) if (data?.isValid) {
isValidNumber = data;
}
}
try { try {
let _status: any;
// const isValidNumber = await wbot.isRegisteredUser(`${number}@c.us`); if (!isValidNumber) {
if (!isValidNumber || isValidNumber && !isValidNumber.data.isValid) { const { data, status } = await axios.post(
`${process.env.WHATS_NUMBER_VALIDATOR_URL}/api/validate`,
{ mobile: number },
{
headers: {
"Content-Type": "application/json"
}
}
);
isValidNumber = data;
_status = status;
}
if (ignoreThrow) return isValidNumber?.number;
console.log('_status: ', _status)
if (_status && _status == 422) throw new AppError("ERR_NO_WAPP_FOUND");
if (!isValidNumber || (isValidNumber && !isValidNumber.isValid)) {
throw new AppError("invalidNumber"); throw new AppError("invalidNumber");
} }
if (isValidNumber && isValidNumber?.isValid) return isValidNumber.number;
} catch (err: any) { } catch (err: any) {
if (err.message === "invalidNumber") { if (err.message === "invalidNumber") {
throw new AppError("ERR_WAPP_INVALID_CONTACT"); throw new AppError("ERR_WAPP_INVALID_CONTACT");
@ -27,10 +62,6 @@ const CheckIsValidContact = async (number: string): Promise<any> => {
throw new AppError("ERR_WAPP_CHECK_CONTACT"); throw new AppError("ERR_WAPP_CHECK_CONTACT");
} }
if (isValidNumber && isValidNumber.data.isValid)
return isValidNumber.data.number
}; };
export default CheckIsValidContact; export default CheckIsValidContact;

View File

@ -1,23 +1,47 @@
import axios from "axios";
import endPointQuery from "../../helpers/EndPointQuery"; import endPointQuery from "../../helpers/EndPointQuery";
import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp"; import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp";
import { getWbot } from "../../libs/wbot"; import { getWbot } from "../../libs/wbot";
const GetProfilePicUrl = async (number: string): Promise<any> => { const GetProfilePicUrl = async (number: string): Promise<any> => {
const defaultWhatsapp = await GetDefaultWhatsApp({
ignoreNoWhatsappFound: true
});
const defaultWhatsapp = await GetDefaultWhatsApp({}); let profilePicUrl;
const wbot_url = await getWbot(defaultWhatsapp.id); if (defaultWhatsapp) {
const wbot_url = await getWbot(defaultWhatsapp.id);
let profilePicUrl = await endPointQuery(`${wbot_url}/api/GetProfilePicUrl`, { number: `${number}`, }) const {data} = await endPointQuery(`${wbot_url}/api/GetProfilePicUrl`, {
number: `${number}`
});
console.log('profilePicUrl.data.data: ', profilePicUrl.data.data) if (data?.data) {
profilePicUrl = data.data;
if (profilePicUrl && profilePicUrl.data.data) { }
return profilePicUrl.data.data;
} }
return null try {
if (!profilePicUrl) {
const { data } = await axios.post(
`${process.env.WHATS_NUMBER_VALIDATOR_URL}/api/GetProfilePicUrl`,
{ number },
{
headers: {
"Content-Type": "application/json"
}
}
);
profilePicUrl = data?.data;
}
if (profilePicUrl) {
return profilePicUrl;
}
} catch (error) {}
return null;
}; };
export default GetProfilePicUrl; export default GetProfilePicUrl;

View File

@ -10,6 +10,7 @@ import path from "path";
import { import {
isHoliday, isHoliday,
isOutBusinessTime, isOutBusinessTime,
isOutBusinessTimeSaturday,
isWeekend isWeekend
} from "../../helpers/TicketConfig"; } from "../../helpers/TicketConfig";
@ -45,7 +46,7 @@ import FindOrCreateTicketService from "../TicketServices/FindOrCreateTicketServi
import ShowWhatsAppService from "../WhatsappService/ShowWhatsAppService"; import ShowWhatsAppService from "../WhatsappService/ShowWhatsAppService";
import { debounce } from "../../helpers/Debounce"; import { debounce } from "../../helpers/Debounce";
import UpdateTicketService from "../TicketServices/UpdateTicketService"; import UpdateTicketService from "../TicketServices/UpdateTicketService";
import { date } from "faker"; import { date, name } from "faker";
import ShowQueueService from "../QueueService/ShowQueueService"; import ShowQueueService from "../QueueService/ShowQueueService";
import ShowTicketMessage from "../TicketServices/ShowTicketMessage"; import ShowTicketMessage from "../TicketServices/ShowTicketMessage";
@ -84,10 +85,9 @@ import {
} from "../../helpers/WhatsappIdMultiSessionControl"; } from "../../helpers/WhatsappIdMultiSessionControl";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import { setMessageAsRead } from "../../helpers/SetMessageAsRead"; import { setMessageAsRead } from "../../helpers/SetMessageAsRead";
import FindOrCreateTicketServiceBot from "../TicketServices/FindOrCreateTicketServiceBot";
import { getSettingValue } from "../../helpers/WhaticketSettings"; import { getSettingValue } from "../../helpers/WhaticketSettings";
import { Op } from "sequelize"; import { Op, json } from "sequelize";
import SettingTicket from "../../models/SettingTicket"; import SettingTicket from "../../models/SettingTicket";
import mostRepeatedPhrase from "../../helpers/MostRepeatedPhrase"; import mostRepeatedPhrase from "../../helpers/MostRepeatedPhrase";
@ -95,12 +95,19 @@ import ListWhatsAppsNumber from "../WhatsappService/ListWhatsAppsNumber";
import { import {
createObject, createObject,
del, del,
findByContain,
findObject, findObject,
get, get,
getSimple,
set set
} from "../../helpers/RedisClient"; } from "../../helpers/RedisClient";
import FindOrCreateTicketServiceBot from "../TicketServices/FindOrCreateTicketServiceBot";
import ShowTicketService from "../TicketServices/ShowTicketService";
import ShowQueuesByUser from "../UserServices/ShowQueuesByUser";
import ListWhatsappQueuesByUserQueue from "../UserServices/ListWhatsappQueuesByUserQueue";
import CreateContactService from "../ContactServices/CreateContactService";
import { number } from "yup";
import ShowContactCustomFieldService from "../ContactServices/ShowContactCustomFieldsService"; import ShowContactCustomFieldService from "../ContactServices/ShowContactCustomFieldsService";
import ShowTicketService from "../TicketServices/ShowTicketService"
var lst: any[] = getWhatsappIds(); var lst: any[] = getWhatsappIds();
@ -177,9 +184,14 @@ const verifyMediaMessage = async (
mediaUrl: media.filename, mediaUrl: media.filename,
mediaType: media.mimetype.split("/")[0], mediaType: media.mimetype.split("/")[0],
quotedMsgId: quotedMsg, quotedMsgId: quotedMsg,
phoneNumberId: msg?.phoneNumberId phoneNumberId: msg?.phoneNumberId,
fromAgent: false
}; };
if (msg?.fromMe) {
messageData = { ...messageData, fromAgent: true };
}
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];
@ -215,20 +227,43 @@ const verifyMessage = async (
contact: Contact, contact: Contact,
quotedMsg?: any quotedMsg?: any
) => { ) => {
const messageData = { let messageData = {
id: msg.id.id, id: msg.id.id,
ticketId: ticket.id, ticketId: ticket.id,
contactId: msg.fromMe ? undefined : contact.id, contactId: msg.fromMe ? undefined : contact.id,
body: msg.body, body: msg.body,
fromMe: msg.fromMe, fromMe: msg.fromMe,
fromAgent: false,
mediaType: msg.type, mediaType: msg.type,
read: msg.fromMe, read: msg.fromMe,
quotedMsgId: quotedMsg, quotedMsgId: quotedMsg,
phoneNumberId: msg?.phoneNumberId phoneNumberId: msg?.phoneNumberId
}; };
if (msg?.fromMe) {
const botInfo = await BotIsOnQueue("botqueue");
if (botInfo.isOnQueue) {
const ura: any = await get({ key: "ura" });
if (ura && !ura.includes(JSON.stringify(msg?.body))) {
messageData = { ...messageData, fromAgent: true };
}
} else if (msg?.body?.trim().length > 0 && !/\u200e/.test(msg.body[0])) {
messageData = { ...messageData, fromAgent: true };
}
}
await ticket.update({ lastMessage: msg.body }); await ticket.update({ lastMessage: msg.body });
if (!msg?.fromMe && msg?.vCards && msg?.vCards?.length > 0) {
if (msg.vCards.length == 1) {
messageData = { ...messageData, body: msg.vCards[0] };
} else {
messageData = { ...messageData, body: JSON.stringify(msg.vCards) };
}
}
await CreateMessageService({ messageData }); await CreateMessageService({ messageData });
}; };
@ -621,13 +656,14 @@ const verifyQueue = async (
selectedOption = 1; selectedOption = 1;
choosenQueue = queues[+selectedOption - 1]; choosenQueue = queues[+selectedOption - 1];
} else { } else {
selectedOption = msg.body; selectedOption = msg?.body;
//////////////// EXTRAIR APENAS O NÚMERO /////////////////// if (selectedOption && selectedOption.trim().length > 0) {
selectedOption = selectedOption.replace(/[^1-9]/g, ""); //////////////// EXTRAIR APENAS O NÚMERO ///////////////////
/////////////////////////////////// selectedOption = selectedOption.replace(/[^1-9]/g, "");
///////////////////////////////////
choosenQueue = queues[+selectedOption - 1]; choosenQueue = queues[+selectedOption - 1];
}
} }
if (choosenQueue) { if (choosenQueue) {
@ -651,13 +687,14 @@ const verifyQueue = async (
ticketId: ticket.id ticketId: ticket.id
}); });
const data = await get("ura"); const data = await get({ key: "ura", parse: true });
await createObject({ await createObject({
whatsappId: `${ticket.whatsappId}`, whatsappId: `${ticket.whatsappId}`,
contactId: `${ticket.contactId}`, contactId: `${ticket.contactId}`,
identifier: "ura", identifier: "ura",
value: data[1].id value: data[1].id,
history: `|${data[1].id}`
}); });
botSendMessage(ticket, data[1].value); botSendMessage(ticket, data[1].value);
@ -672,7 +709,7 @@ const verifyQueue = async (
if (outService.length > 0) { if (outService.length > 0) {
const { type, msg: msgOutService } = outService[0]; const { type, msg: msgOutService } = outService[0];
console.log(`${type} message ignored on queue`); console.log(`${type} message ignored on queue`);
botSendMessage(ticket, msgOutService); botSendMessage(ticket, `\u200e${msgOutService}`);
return; return;
} }
@ -689,7 +726,7 @@ const verifyQueue = async (
//test del transfere o atendimento se entrar na ura infinita //test del transfere o atendimento se entrar na ura infinita
const repet: any = await mostRepeatedPhrase(ticket.id); const repet: any = await mostRepeatedPhrase(ticket.id);
if (repet.occurrences > 4) { if (repet.occurrences > 10) {
await UpdateTicketService({ await UpdateTicketService({
ticketData: { status: "pending", queueId: queues[0].id }, ticketData: { status: "pending", queueId: queues[0].id },
ticketId: ticket.id ticketId: ticket.id
@ -794,6 +831,7 @@ const isValidMsg = (msg: any): boolean => {
msg.type === "image" || msg.type === "image" ||
msg.type === "document" || msg.type === "document" ||
msg.type === "vcard" || msg.type === "vcard" ||
msg.type === "multi_vcard" ||
msg.type === "sticker" msg.type === "sticker"
) )
return true; return true;
@ -812,10 +850,14 @@ const queuesOutBot = async (wbot: Session, botId: string | number) => {
return { queues, greetingMessage }; return { queues, greetingMessage };
}; };
const transferTicket = async (queueName: any, wbot: any, ticket: Ticket) => { const transferTicket = async (
queueName: any,
wbot: any,
ticket: Ticket,
sendGreetingMessage?: boolean
) => {
const botInfo = await BotIsOnQueue("botqueue"); const botInfo = await BotIsOnQueue("botqueue");
const io = getIO();
console.log("kkkkkkkkkkkkkkkkkkkkk queueName: ", queueName);
const queuesWhatsGreetingMessage = await queuesOutBot( const queuesWhatsGreetingMessage = await queuesOutBot(
wbot, wbot,
@ -826,27 +868,48 @@ const transferTicket = async (queueName: any, wbot: any, ticket: Ticket) => {
const queues = queuesWhatsGreetingMessage.queues; const queues = queuesWhatsGreetingMessage.queues;
// console.log("queues ---> ", console.log(JSON.stringify(queues, null, 6)));
if (typeof queueName == "string") { if (typeof queueName == "string") {
queue = queues.find( queue = queues.find(
(q: any) => q?.name?.toLowerCase() == queueName.trim().toLowerCase() (q: any) => q?.name?.toLowerCase() == queueName.trim().toLowerCase()
); );
// await deleteObject(wbot.id, `${ticket.contactId}`, "ura");
} else if (typeof queueName == "number") { } else if (typeof queueName == "number") {
queue = queues[queueName]; queue = queues[queueName];
} }
if (queue) await botTransferTicket(queue, ticket); if (queue) await botTransferTicket(queue, ticket, sendGreetingMessage);
io.emit("notifyPeding", { data: { ticket, queue } });
}; };
const botTransferTicket = async (queues: Queue, ticket: Ticket) => { const botTransferTicket = async (
queues: Queue,
ticket: Ticket,
sendGreetingMessage?: boolean
) => {
await ticket.update({ userId: null }); await ticket.update({ userId: null });
await UpdateTicketService({ await UpdateTicketService({
ticketData: { status: "pending", queueId: queues.id }, ticketData: { status: "pending", queueId: queues.id },
ticketId: ticket.id ticketId: ticket.id
}); });
if (sendGreetingMessage && queues?.greetingMessage?.length > 0) {
botSendMessage(ticket, queues.greetingMessage);
}
};
const botTransferTicketToUser = async (
userId: number,
ticket: Ticket,
queueId?: number | undefined
) => {
console.log("USER ID: ", userId);
// await ticket.update({ userId: userId });
await UpdateTicketService({
ticketData: { status: "open", userId, queueId },
ticketId: ticket.id
});
}; };
const botSendMessage = (ticket: Ticket, msg: string) => { const botSendMessage = (ticket: Ticket, msg: string) => {
@ -909,31 +972,20 @@ const handleMessage = async (
// let groupContact: Contact | undefined; // let groupContact: Contact | undefined;
if (msg.fromMe) { if (msg.fromMe) {
const whatsapp = await whatsappInfo(wbot.id);
if (whatsapp?.number) {
const ticketExpiration = await SettingTicket.findOne({
where: { key: "ticketExpiration", number: whatsapp.number }
});
if (
ticketExpiration &&
ticketExpiration.value == "enabled" &&
ticketExpiration?.message.trim() == msg.body.trim()
) {
console.log("*********** TICKET EXPIRATION");
return;
}
}
// messages sent automatically by wbot have a special character in front of it // messages sent automatically by wbot have a special character in front of it
// if so, this message was already been stored in database; // if so, this message was already been stored in database;
// if (/\u200e/.test(msg.body[0])) return; if (/\u200e/.test(msg.body[0])) return;
// media messages sent from me from cell phone, first comes with "hasMedia = false" and type = "image/ptt/etc" // media messages sent from me from cell phone, first comes with "hasMedia = false" and type = "image/ptt/etc"
// in this case, return and let this message be handled by "media_uploaded" event, when it will have "hasMedia = true" // in this case, return and let this message be handled by "media_uploaded" event, when it will have "hasMedia = true"
if (!msg.hasMedia && msg.type !== "chat" && msg.type !== "vcard") return; if (
!msg.hasMedia &&
msg.type !== "chat" &&
msg.type !== "vcard" &&
msg.type !== "multi_vcard"
)
return;
} else { } else {
console.log(`\n <<<<<<<<<< RECEIVING MESSAGE: console.log(`\n <<<<<<<<<< RECEIVING MESSAGE:
Parcial msg and msgContact info: Parcial msg and msgContact info:
@ -1062,11 +1114,55 @@ const handleMessage = async (
await verifyQueue(wbot, msg, ticket, contact); await verifyQueue(wbot, msg, ticket, contact);
} }
// O bot interage com o cliente e encaminha o atendimento para fila de atendende quando o usuário escolhe a opção falar com atendente if (msg.type === "vcard" || msg.type === "multi_vcard") {
await vcard(msg);
}
//Habilitar esse caso queira usar o bot
const botInfo = await BotIsOnQueue("botqueue"); const botInfo = await BotIsOnQueue("botqueue");
// const botInfo = { isOnQueue: false, botQueueId: 0, userIdBot: 0 };
// Transfer to agent
// O bot interage com o cliente e encaminha o atendimento para fila de atendende quando o usuário escolhe a opção falar com atendente
if (
!msg.fromMe &&
((ticket.status == "open" &&
botInfo &&
ticket.userId == +botInfo.userIdBot) ||
ticket.status == "pending" ||
ticket.status == "queueChoice")
) {
const filteredUsers = await findByContain("user:*", "name", msg?.body);
if (filteredUsers && filteredUsers.length > 0) {
if (botInfo.isOnQueue) {
transferTicket(filteredUsers[0].name, wbot, ticket, true);
return;
}
const whatsappQueues = await ListWhatsappQueuesByUserQueue(
+filteredUsers[0].id
);
const obj: any = whatsappQueues.find(
(ob: any) => ob.whatsappId == wbot.id
);
if (obj) {
await botTransferTicketToUser(
+filteredUsers[0].id,
ticket,
+obj.queueId
);
await botSendMessage(
ticket,
`Você foi transferido para falar com o agente ${filteredUsers[0].name}, aguarde.`
);
}
return;
}
}
//
if ( if (
botInfo.isOnQueue && botInfo.isOnQueue &&
@ -1077,7 +1173,7 @@ const handleMessage = async (
console.log("repet.occurrences: ", repet.occurrences); console.log("repet.occurrences: ", repet.occurrences);
if (repet.occurrences > 4) { if (repet.occurrences > 10) {
await transferTicket(0, wbot, ticket); await transferTicket(0, wbot, ticket);
await SendWhatsAppMessage({ await SendWhatsAppMessage({
@ -1105,9 +1201,9 @@ const handleMessage = async (
return; return;
} }
let query = await get( let query = await get({
`whatsappId:${wbot.id}:contactId:${contact?.id}:identifier:query` key: `whatsappId:${wbot.id}:contactId:${contact?.id}:identifier:query`
); });
if (query && msg?.body?.trim() != "0") { if (query && msg?.body?.trim() != "0") {
query = JSON.parse(query); query = JSON.parse(query);
@ -1125,7 +1221,6 @@ const handleMessage = async (
await set( await set(
`whatsappId:${wbot.id}:contactId:${contact?.id}:identifier:query`, `whatsappId:${wbot.id}:contactId:${contact?.id}:identifier:query`,
JSON.stringify({ ...query, value: msg.body }), JSON.stringify({ ...query, value: msg.body }),
true
); );
} }
@ -1156,11 +1251,6 @@ dialog_actions=request_endpoint=https://sos.espacolaser.com.br/api/whatsapp/tick
menuMsg?.transferToQueue && menuMsg?.transferToQueue &&
menuMsg.transferToQueue.trim().length > 0 menuMsg.transferToQueue.trim().length > 0
) { ) {
console.log(
"YYYYYYYYYYYYYYYYYYYY menuMsg.transferToQueue: ",
menuMsg.transferToQueue
);
transferTicket(menuMsg.transferToQueue.trim(), wbot, ticket); transferTicket(menuMsg.transferToQueue.trim(), wbot, ticket);
} else if (menuMsg?.query) { } else if (menuMsg?.query) {
await set( await set(
@ -1176,7 +1266,6 @@ dialog_actions=request_endpoint=https://sos.espacolaser.com.br/api/whatsapp/tick
queryMsgInvalidParam: menuMsg?.queryMsgInvalidParam, queryMsgInvalidParam: menuMsg?.queryMsgInvalidParam,
optionsMenu: menuMsg?.optionsMenu optionsMenu: menuMsg?.optionsMenu
}), }),
true
); );
} }
} }
@ -1187,6 +1276,29 @@ dialog_actions=request_endpoint=https://sos.espacolaser.com.br/api/whatsapp/tick
msg.body == "0" && msg.body == "0" &&
ticket.status == "pending" && ticket.status == "pending" &&
ticket.queueId ticket.queueId
) {
if (botInfo.isOnQueue) {
let choosenQueue = await ShowQueueService(botInfo.botQueueId);
await UpdateTicketService({
ticketData: {
status: "open",
userId: botInfo.userIdBot,
queueId: choosenQueue.id
},
ticketId: ticket.id
});
const menuMsg: any = await menu(msg.body, wbot.id, contact.id);
await botSendMessage(ticket, menuMsg.value);
}
return;
} else if (
!msg.fromMe &&
msg.body == "#" &&
ticket.status == "pending" &&
ticket.queueId &&
botInfo.isOnQueue
) { ) {
let choosenQueue = await ShowQueueService(botInfo.botQueueId); let choosenQueue = await ShowQueueService(botInfo.botQueueId);
@ -1198,10 +1310,20 @@ dialog_actions=request_endpoint=https://sos.espacolaser.com.br/api/whatsapp/tick
}, },
ticketId: ticket.id ticketId: ticket.id
}); });
// const menuMsg: any = await menu(msg.body, wbot.id, contact.id);
// await botSendMessage(ticket, menuMsg.value);
// const data: any = await get({ key: "ura", parse: true });
// return await backUra(ticket.whatsappId, ticket.contactId, data);
const menuMsg: any = await menu(msg.body, wbot.id, contact.id); const menuMsg: any = await menu(msg.body, wbot.id, contact.id);
console.log("menuMsg: ", menuMsg);
await botSendMessage(ticket, menuMsg.value); await botSendMessage(ticket, menuMsg.value);
return; // return;
} }
if (msg && !msg.fromMe && ticket.status == "pending") { if (msg && !msg.fromMe && ticket.status == "pending") {
@ -1227,7 +1349,7 @@ dialog_actions=request_endpoint=https://sos.espacolaser.com.br/api/whatsapp/tick
return; return;
} }
botSendMessage(ticket, msgOutService); botSendMessage(ticket, `\u200e${msgOutService}`);
return; return;
} }
} }
@ -1240,7 +1362,7 @@ dialog_actions=request_endpoint=https://sos.espacolaser.com.br/api/whatsapp/tick
const menu = async (userTyped: string, whatsappId: any, contactId: any) => { const menu = async (userTyped: string, whatsappId: any, contactId: any) => {
let lastId = await findObject(whatsappId, contactId, "ura"); let lastId = await findObject(whatsappId, contactId, "ura");
const data: any = await get("ura"); const data: any = await get({ key: "ura", parse: true });
console.log("lastId[0]: ", lastId[0]); console.log("lastId[0]: ", lastId[0]);
@ -1249,7 +1371,8 @@ const menu = async (userTyped: string, whatsappId: any, contactId: any) => {
whatsappId, whatsappId,
contactId, contactId,
identifier: "ura", identifier: "ura",
value: data[1].id value: data[1].id,
history: `|${data[1].id}`
}); });
} }
@ -1259,7 +1382,7 @@ const menu = async (userTyped: string, whatsappId: any, contactId: any) => {
if ( if (
lastId && lastId &&
lastId.length == 4 && (lastId.length == 4 || lastId.length == 5) &&
lastId[3] && lastId[3] &&
lastId[3].trim().length > 0 lastId[3].trim().length > 0
) { ) {
@ -1269,23 +1392,48 @@ const menu = async (userTyped: string, whatsappId: any, contactId: any) => {
o.value.toLowerCase() == userTyped.toLowerCase() o.value.toLowerCase() == userTyped.toLowerCase()
); );
// TEST DEL if (!option && userTyped != "0" && userTyped != "#") {
console.log("OPTION: ", option);
if (!option && userTyped != "0") {
if (!existSubMenu()) { if (!existSubMenu()) {
const response = await mainOptionsMenu(userTyped); const response = await mainOptionsMenu(userTyped);
if (response) return response; if (response) return response;
else { else {
console.log("kkkkkkkkkkkkkkkkkkk"); let uraOptionSelected = await findObject(
await createObject({
whatsappId, whatsappId,
contactId, contactId,
identifier: "ura", "ura"
value: data[1].id );
});
return data[1]; uraOptionSelected = uraOptionSelected[4].split("|");
if (uraOptionSelected.length == 1) {
await createObject({
whatsappId,
contactId,
identifier: "ura",
value: data[1].id,
history: `|${data[1].id}`
});
return data[1];
} else if (uraOptionSelected.length > 1) {
const id = uraOptionSelected[uraOptionSelected.length - 1];
console.log(" ID FROM THE MENU/SUBMENU: ", id);
const history = await historyUra(whatsappId, contactId, id);
await createObject({
whatsappId,
contactId,
identifier: "ura",
value: id,
history
});
let response: any = data.find((o: any) => o.id == id);
return response;
}
} }
} }
} }
@ -1293,19 +1441,16 @@ const menu = async (userTyped: string, whatsappId: any, contactId: any) => {
if (option) { if (option) {
let response: any = data.find((o: any) => o.idmaster == option.id); let response: any = data.find((o: any) => o.idmaster == option.id);
console.log(" RESPONSE OPTION: ", response, " | OPTION: ", option);
console.log( let history: any = await historyUra(whatsappId, contactId, response.id);
"RRRRRRRRRRRRRRRRRRRRRRRRRRRRR response: ",
response,
" | option: ",
option
);
await createObject({ await createObject({
whatsappId, whatsappId,
contactId, contactId,
identifier: "ura", identifier: "ura",
value: response.id value: response.id,
history
}); });
return response; return response;
@ -1314,7 +1459,8 @@ const menu = async (userTyped: string, whatsappId: any, contactId: any) => {
whatsappId, whatsappId,
contactId, contactId,
identifier: "ura", identifier: "ura",
value: data[1].id value: data[1].id,
history: `|${data[1].id}`
}); });
await del( await del(
@ -1322,21 +1468,8 @@ const menu = async (userTyped: string, whatsappId: any, contactId: any) => {
); );
return data[1]; return data[1];
} else { } else if (userTyped == "#") {
console.log("INVALID SEARCH"); return await backUra(whatsappId, contactId, data);
let response = await existSubMenu();
if (response) return response;
return {
value: data.find((o: any) => o.id == lastId[3])?.value
};
// return {
// value: `Você digitou uma opçao inválida!\n\n${
// data.find((o: any) => o.id == lastId[3])?.value
// }\n\nDigite 0 para voltar ao menu `
// };
} }
} }
@ -1349,18 +1482,29 @@ const menu = async (userTyped: string, whatsappId: any, contactId: any) => {
} }
async function mainOptionsMenu(userTyped: any) { async function mainOptionsMenu(userTyped: any) {
let currentMenu = await findObject(whatsappId, contactId, "ura");
const menuValues = data
.filter((m: any) => m.idmaster == currentMenu[3])
.map((m: any) => m.value);
let menuOption = data.find( let menuOption = data.find(
(o: any) => o.value.toLowerCase() == userTyped.toLowerCase() (o: any) =>
o.value.toLowerCase() == userTyped.toLowerCase() &&
menuValues.includes(userTyped.toLowerCase())
); );
console.log("============> menuOption OPTION: ", menuOption);
if (menuOption) { if (menuOption) {
let response = data.find((o: any) => o.idmaster == menuOption.id); let response = data.find((o: any) => o.idmaster == menuOption.id);
if (response) { if (response) {
let history = await historyUra(whatsappId, contactId, response.id);
await createObject({ await createObject({
whatsappId, whatsappId,
contactId, contactId,
identifier: "ura", identifier: "ura",
value: response.id value: response.id,
history
}); });
return response; return response;
@ -1440,6 +1584,13 @@ const outOfService = async (number: string) => {
objs.push({ type: "holiday", msg: holiday.msg }); objs.push({ type: "holiday", msg: holiday.msg });
} }
// MESSAGE TO SATURDAY BUSINESS TIME
const businessTimeSaturday = await isOutBusinessTimeSaturday(number);
if (businessTimeSaturday && businessTimeSaturday.set) {
objs.push({ type: "saturdayBusinessTime", msg: businessTimeSaturday.msg });
}
// MESSAGES TO SATURDAY OR SUNDAY // MESSAGES TO SATURDAY OR SUNDAY
const weekend: any = await isWeekend(number); const weekend: any = await isWeekend(number);
@ -1466,8 +1617,119 @@ export {
verifyMediaMessage, verifyMediaMessage,
verifyContact, verifyContact,
isValidMsg, isValidMsg,
mediaTypeWhatsappOfficial mediaTypeWhatsappOfficial,
botSendMessage
}; };
async function vcard(msg: any) {
let array: any[] = [];
let contact: any;
let obj: any[] = [];
try {
const multi_vcard = msg?.vCards?.length === 0 ? false : true;
if (multi_vcard) {
array = msg?.vCards;
contact = [];
} else {
array = msg.body.split("\n");
contact = "";
}
for (let index = 0; index < array.length; index++) {
const v = array[index];
const values = v.split(":");
for (let ind = 0; ind < values.length; ind++) {
if (values[ind].indexOf("+") !== -1) {
obj.push({ number: values[ind] });
}
if (values[ind].indexOf("FN") !== -1) {
if (multi_vcard)
contact.push({ name: values[ind + 1].split("\n")[0] });
else contact = values[ind + 1];
}
}
}
for (const i in obj) {
let data: any = {};
if (multi_vcard) {
data = {
name: contact[i].name,
number: obj[i].number.replace(/\D/g, "")
};
} else {
data = {
name: contact,
number: obj[i].number.replace(/\D/g, "")
};
}
const cont = await CreateContactService(data);
}
} catch (error) {
console.log(error);
}
}
async function backUra(whatsappId: any, contactId: any, data: any) {
let uraOptionSelected = await findObject(whatsappId, contactId, "ura");
uraOptionSelected = uraOptionSelected[4].split("|").filter(Boolean);
let id = uraOptionSelected[0];
let history = `|${uraOptionSelected[0]}`;
if (uraOptionSelected.length > 1) {
const idRemove = uraOptionSelected[uraOptionSelected.length - 1];
history = await historyUra(whatsappId, contactId, idRemove, true);
const lstIds = history.split("|").filter(Boolean);
id = lstIds[lstIds.length - 1];
}
await createObject({
whatsappId,
contactId,
identifier: "ura",
value: id,
history
});
let response: any = data.find((o: any) => o.id == id);
return response;
}
async function historyUra(
whatsappId: any,
contactId: any,
id: any,
remove?: boolean
) {
let uraOptionSelected = await findObject(whatsappId, contactId, "ura");
let history = "";
console.log("SELECED OPTION uraOptionSelected: ", uraOptionSelected);
if (remove) {
return uraOptionSelected[4]?.replace(`|${id}`, "");
}
if (uraOptionSelected && uraOptionSelected.length == 5) {
if (!uraOptionSelected[4]?.includes(`${id}`))
history += `${uraOptionSelected[4]}|${id}`;
else history = `${uraOptionSelected[4]}`;
} else {
history = `|${id}`;
}
return history;
}
async function whatsappInfo(whatsappId: string | number) { async function whatsappInfo(whatsappId: string | number) {
return await Whatsapp.findByPk(whatsappId); return await Whatsapp.findByPk(whatsappId);
} }

View File

@ -16,6 +16,7 @@ interface Request {
phoneNumberId?: string; phoneNumberId?: string;
wabaId?: string; wabaId?: string;
isOfficial?: boolean; isOfficial?: boolean;
number?: string;
} }
interface Response { interface Response {
@ -34,7 +35,8 @@ const CreateWhatsAppService = async ({
isDefault = false, isDefault = false,
isOfficial = false, isOfficial = false,
phoneNumberId, phoneNumberId,
wabaId wabaId,
number
}: Request): Promise<Response> => { }: Request): Promise<Response> => {
try { try {
const schema = Yup.object().shape({ const schema = Yup.object().shape({
@ -98,6 +100,7 @@ const CreateWhatsAppService = async ({
phoneNumberId, phoneNumberId,
wabaId, wabaId,
isOfficial, isOfficial,
number,
classification classification
}, },
{ include: ["queues"] } { include: ["queues"] }

View File

@ -6,17 +6,30 @@ const { QueryTypes } = require("sequelize");
const sequelize = new Sequelize(dbConfig); const sequelize = new Sequelize(dbConfig);
const ListWhatsAppsForQueueService = async (queueId: number | string): Promise<any> => { const ListWhatsAppsForQueueService = async (
const distinctWhatsapps = await sequelize.query( queueId: number | string,
`SELECT w.id, w.number, w.status, wq.whatsappId, wq.queueId status?: string
FROM Whatsapps w ): Promise<any> => {
JOIN WhatsappQueues wq ON w.id = wq.whatsappId AND wq.queueId = ${queueId} let distinctWhatsapps: any;
GROUP BY w.number;`,
{ type: QueryTypes.SELECT } if (status) {
); distinctWhatsapps = await sequelize.query(
`SELECT w.id, w.number, w.status, wq.whatsappId, wq.queueId FROM Whatsapps w
JOIN WhatsappQueues wq ON w.id = wq.whatsappId AND wq.queueId = ${queueId} AND w.status = '${status}'
AND phoneNumberId = false
GROUP BY w.number;`,
{ type: QueryTypes.SELECT }
);
} else {
distinctWhatsapps = await sequelize.query(
`SELECT w.id, w.number, w.status, wq.whatsappId, wq.queueId FROM Whatsapps w
JOIN WhatsappQueues wq ON w.id = wq.whatsappId AND wq.queueId = ${queueId}
GROUP BY w.number;`,
{ type: QueryTypes.SELECT }
);
}
return distinctWhatsapps; return distinctWhatsapps;
}; };
export default ListWhatsAppsForQueueService; export default ListWhatsAppsForQueueService;

View File

@ -22,6 +22,7 @@ interface WhatsappData {
greetingMessage?: string; greetingMessage?: string;
farewellMessage?: string; farewellMessage?: string;
queueIds?: number[]; queueIds?: number[];
number?:string;
} }
interface Request { interface Request {
@ -52,6 +53,7 @@ const UpdateWhatsAppService = async ({
phoneNumberId, phoneNumberId,
wabaId, wabaId,
isOfficial, isOfficial,
number,
url, url,
urlApi, urlApi,
session, session,
@ -116,6 +118,7 @@ const UpdateWhatsAppService = async ({
isOfficial, isOfficial,
phoneNumberId, phoneNumberId,
wabaId, wabaId,
number,
classification classification
}); });

View File

@ -0,0 +1,62 @@
import { Button } from "@material-ui/core";
import React, { useRef } from "react";
import { useEffect } from "react";
import { useState } from "react";
const LS_NAME = 'audioMessageRate';
export default function({url}) {
const audioRef = useRef(null);
const [audioRate, setAudioRate] = useState( parseFloat(localStorage.getItem(LS_NAME) || "1") );
const [showButtonRate, setShowButtonRate] = useState(false);
useEffect(() => {
audioRef.current.playbackRate = audioRate;
localStorage.setItem(LS_NAME, audioRate);
}, [audioRate]);
useEffect(() => {
audioRef.current.onplaying = () => {
setShowButtonRate(true);
};
audioRef.current.onpause = () => {
setShowButtonRate(false);
};
audioRef.current.onended = () => {
setShowButtonRate(false);
};
}, []);
const toogleRate = () => {
let newRate = null;
switch(audioRate) {
case 0.5:
newRate = 1;
break;
case 1:
newRate = 1.5;
break;
case 1.5:
newRate = 2;
break;
case 2:
newRate = 0.5;
break;
default:
newRate = 1;
break;
}
setAudioRate(newRate);
};
return (
<>
<audio ref={audioRef} controls>
<source src={url} type="audio/ogg"></source>
</audio>
{showButtonRate && <Button style={{marginLeft: "5px", marginTop: "-45px"}} onClick={toogleRate}>{audioRate}x</Button>}
</>
);
}

View File

@ -77,8 +77,16 @@ const ConfigModal = ({ open, onClose, change }) => {
const initialState = { const initialState = {
startTimeBus: new Date(), startTimeBus: new Date(),
endTimeBus: new Date(), endTimeBus: new Date(),
startTimeBusSaturday: new Date(),
endTimeBusSaturday: new Date(),
messageBus: '', messageBus: '',
messageBusSaturday: '',
businessTimeEnable: false, businessTimeEnable: false,
businessTimeEnableSaturday: false,
ticketTimeExpiration: new Date(), ticketTimeExpiration: new Date(),
ticketExpirationMsg: '', ticketExpirationMsg: '',
ticketExpirationEnable: false, ticketExpirationEnable: false,
@ -122,6 +130,9 @@ const ConfigModal = ({ open, onClose, change }) => {
} }
const outBusinessHours = data.config.find((c) => c.key === "outBusinessHours") const outBusinessHours = data.config.find((c) => c.key === "outBusinessHours")
const saturdayBusinessTime = data.config.find((c) => c.key === "saturdayBusinessTime")
const ticketExpiration = data.config.find((c) => c.key === "ticketExpiration") const ticketExpiration = data.config.find((c) => c.key === "ticketExpiration")
const saturday = data.config.find((c) => c.key === "saturday") const saturday = data.config.find((c) => c.key === "saturday")
const sunday = data.config.find((c) => c.key === "sunday") const sunday = data.config.find((c) => c.key === "sunday")
@ -134,6 +145,11 @@ const ConfigModal = ({ open, onClose, change }) => {
messageBus: outBusinessHours.message, messageBus: outBusinessHours.message,
businessTimeEnable: outBusinessHours.value === 'enabled' ? true : false, businessTimeEnable: outBusinessHours.value === 'enabled' ? true : false,
startTimeBusSaturday: saturdayBusinessTime.startTime,
endTimeBusSaturday: saturdayBusinessTime.endTime,
messageBusSaturday: saturdayBusinessTime.message,
businessTimeEnableSaturday: saturdayBusinessTime.value === 'enabled' ? true : false,
ticketTimeExpiration: ticketExpiration.startTime, ticketTimeExpiration: ticketExpiration.startTime,
ticketExpirationMsg: ticketExpiration.message, ticketExpirationMsg: ticketExpiration.message,
ticketExpirationEnable: ticketExpiration.value === 'enabled' ? true : false, ticketExpirationEnable: ticketExpiration.value === 'enabled' ? true : false,
@ -165,6 +181,14 @@ const ConfigModal = ({ open, onClose, change }) => {
message: values.messageBus, message: values.messageBus,
value: values.businessTimeEnable ? 'enabled' : 'disabled' value: values.businessTimeEnable ? 'enabled' : 'disabled'
}, },
saturdayBusinessTime: {
startTime: values.startTimeBusSaturday,
endTime: values.endTimeBusSaturday,
message: values.messageBusSaturday,
value: values.businessTimeEnableSaturday ? 'enabled' : 'disabled'
},
ticketExpiration: { ticketExpiration: {
startTime: values.ticketTimeExpiration, startTime: values.ticketTimeExpiration,
message: values.ticketExpirationMsg, message: values.ticketExpirationMsg,
@ -325,6 +349,61 @@ const ConfigModal = ({ open, onClose, change }) => {
</div> </div>
<br />
{/* SABADO INICIO */}
<div className={classes.multFieldLine}>
<Field
component={TimePicker}
name="startTimeBusSaturday"
label="Inicio atendimentos"
ampm={false}
openTo="hours"
views={['hours', 'minutes',]}
format="HH:mm"
/>
{' '}
<Field
component={TimePicker}
name="endTimeBusSaturday"
label="Fim atendimento"
ampm={false}
openTo="hours"
views={['hours', 'minutes',]}
format="HH:mm"
/>
<FormControlLabel
control={
<Field
as={Switch}
color="primary"
name="businessTimeEnableSaturday"
checked={values.businessTimeEnableSaturday}
/>
}
label={'Ativar/Desativar'} />
</div>
<div>
<Field
as={TextField}
label={'Mensagem fora do horário de atendimento sábado'}
type="messageBusSaturday"
multiline
rows={5}
fullWidth
name="messageBusSaturday"
error={
touched.messageBusSaturday && Boolean(errors.messageBusSaturday)
}
helperText={
touched.messageBusSaturday && errors.messageBusSaturday
}
variant="outlined"
margin="dense"
/>
</div>
{/* SABADO FIM */}
<br /> <br />

View File

@ -157,7 +157,7 @@ const ContactCreateTicketModal = ({ modalOpen, onClose, contactId }) => {
const { data } = await api.get("/whatsapp/official/matchQueue", { params: { userId: user.id, queueId: selectedQueue }, }) const { data } = await api.get("/whatsapp/official/matchQueue", { params: { userId: user.id, queueId: selectedQueue }, })
console.log('WHATSAPP DATA: ', data) // console.log('WHATSAPP DATA: ', data)
setWhatsQueue(data) setWhatsQueue(data)

View File

@ -1,26 +1,26 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react"
import * as Yup from "yup"; import * as Yup from "yup"
import { Formik, FieldArray, Form, Field } from "formik"; import { Formik, FieldArray, Form, Field } from "formik"
import { toast } from "react-toastify"; import { toast } from "react-toastify"
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles"
import { green } from "@material-ui/core/colors"; import { green } from "@material-ui/core/colors"
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button"
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField"
import Dialog from "@material-ui/core/Dialog"; import Dialog from "@material-ui/core/Dialog"
import DialogActions from "@material-ui/core/DialogActions"; import DialogActions from "@material-ui/core/DialogActions"
import DialogContent from "@material-ui/core/DialogContent"; import DialogContent from "@material-ui/core/DialogContent"
import DialogTitle from "@material-ui/core/DialogTitle"; import DialogTitle from "@material-ui/core/DialogTitle"
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography"
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton"
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"; import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"
import CircularProgress from "@material-ui/core/CircularProgress"; import CircularProgress from "@material-ui/core/CircularProgress"
import { i18n } from "../../translate/i18n"; import { i18n } from "../../translate/i18n"
import api from "../../services/api"; import api from "../../services/api"
import toastError from "../../errors/toastError"; import toastError from "../../errors/toastError"
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
root: { root: {
@ -50,7 +50,7 @@ const useStyles = makeStyles(theme => ({
marginTop: -12, marginTop: -12,
marginLeft: -12, marginLeft: -12,
}, },
})); }))
const ContactSchema = Yup.object().shape({ const ContactSchema = Yup.object().shape({
name: Yup.string() name: Yup.string()
@ -60,75 +60,77 @@ const ContactSchema = Yup.object().shape({
number: Yup.string().min(8, "Too Short!").max(50, "Too Long!"), number: Yup.string().min(8, "Too Short!").max(50, "Too Long!"),
email: Yup.string().min(2, "Too Short!") email: Yup.string().min(2, "Too Short!")
.max(50, "Too Long!"), .max(50, "Too Long!"),
// email: Yup.string().email("Invalid email"), // email: Yup.string().email("Invalid email"),
}); })
const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => { const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
const classes = useStyles(); const classes = useStyles()
const isMounted = useRef(true); const isMounted = useRef(true)
const initialState = { const initialState = {
name: "", name: "",
number: "", number: "",
email: "", email: "",
useDialogflow: true, useDialogflow: true,
}; }
const [contact, setContact] = useState(initialState); const [contact, setContact] = useState(initialState)
const [isSaving, setSaving] = useState(false)
useEffect(() => { useEffect(() => {
return () => { return () => {
isMounted.current = false; isMounted.current = false
}; }
}, []); }, [])
useEffect(() => { useEffect(() => {
const fetchContact = async () => { const fetchContact = async () => {
if (initialValues) { if (initialValues) {
setContact(prevState => { setContact(prevState => {
return { ...prevState, ...initialValues }; return { ...prevState, ...initialValues }
}); })
} }
if (!contactId) return; if (!contactId) return
try { try {
const { data } = await api.get(`/contacts/${contactId}`); const { data } = await api.get(`/contacts/${contactId}`)
if (isMounted.current) { if (isMounted.current) {
setContact(data); setContact(data)
} }
} catch (err) { } catch (err) {
toastError(err); toastError(err)
} }
}; }
fetchContact(); fetchContact()
}, [contactId, open, initialValues]); }, [contactId, open, initialValues])
const handleClose = () => { const handleClose = () => {
onClose(); onClose()
setContact(initialState); setContact(initialState)
}; }
const handleSaveContact = async values => { const handleSaveContact = async values => {
try { try {
if (contactId) { if (contactId) {
await api.put(`/contacts/${contactId}`, values); await api.put(`/contacts/${contactId}`, values)
handleClose(); handleClose()
} else { } else {
const { data } = await api.post("/contacts", values); const { data } = await api.post("/contacts", values)
if (onSave) { if (onSave) {
onSave(data); onSave(data)
} }
handleClose(); handleClose()
} }
toast.success(i18n.t("contactModal.success")); toast.success(i18n.t("contactModal.success"))
} catch (err) { } catch (err) {
toastError(err); toastError(err)
} }
}; setSaving(false)
}
return ( return (
<div className={classes.root}> <div className={classes.root}>
@ -143,10 +145,11 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
enableReinitialize={true} enableReinitialize={true}
validationSchema={ContactSchema} validationSchema={ContactSchema}
onSubmit={(values, actions) => { onSubmit={(values, actions) => {
setSaving(true)
setTimeout(() => { setTimeout(() => {
handleSaveContact(values); handleSaveContact(values)
actions.setSubmitting(false); actions.setSubmitting(false)
}, 400); }, 400)
}} }}
> >
{({ values, errors, touched, isSubmitting }) => ( {({ values, errors, touched, isSubmitting }) => (
@ -256,14 +259,14 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
<Button <Button
type="submit" type="submit"
color="primary" color="primary"
disabled={isSubmitting} disabled={isSaving}
variant="contained" variant="contained"
className={classes.btnWrapper} className={classes.btnWrapper}
> >
{contactId {contactId
? `${i18n.t("contactModal.buttons.okEdit")}` ? `${i18n.t("contactModal.buttons.okEdit")}`
: `${i18n.t("contactModal.buttons.okAdd")}`} : `${i18n.t("contactModal.buttons.okAdd")}`}
{isSubmitting && ( {isSaving && (
<CircularProgress <CircularProgress
size={24} size={24}
className={classes.buttonProgress} className={classes.buttonProgress}
@ -276,7 +279,7 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
</Formik> </Formik>
</Dialog> </Dialog>
</div> </div>
); )
}; }
export default ContactModal; export default ContactModal

View File

@ -0,0 +1,53 @@
import React, { useEffect } from 'react';
import toastError from "../../errors/toastError";
import Typography from "@material-ui/core/Typography";
import Grid from "@material-ui/core/Grid";
import { Button, Divider, } from "@material-ui/core";
const LocationPreview = ({ image, link, description }) => {
useEffect(() => {}, [image, link, description]);
const handleLocation = async() => {
try {
window.open(link);
} catch (err) {
toastError(err);
}
}
return (
<>
<div style={{
minWidth: "250px",
}}>
<div>
<div style={{ float: "left" }}>
<img src={image} onClick={handleLocation} style={{ width: "100px" }} />
</div>
{ description && (
<div style={{ display: "flex", flexWrap: "wrap" }}>
<Typography style={{ marginTop: "12px", marginLeft: "15px", marginRight: "15px", float: "left" }} variant="subtitle1" color="primary" gutterBottom>
<div dangerouslySetInnerHTML={{ __html: description.replace('\\n', '<br />') }}></div>
</Typography>
</div>
)}
<div style={{ display: "block", content: "", clear: "both" }}></div>
<div>
<Divider />
<Button
fullWidth
color="primary"
onClick={handleLocation}
disabled={!link}
>Visualizar</Button>
</div>
</div>
</div>
</>
);
};
export default LocationPreview;

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react"
import Markdown from "markdown-to-jsx"; import Markdown from "markdown-to-jsx"
const elements = [ const elements = [
"a", "a",
@ -139,25 +139,32 @@ const elements = [
"svg", "svg",
"text", "text",
"tspan", "tspan",
]; ]
const allowedElements = ["a", "b", "strong", "em", "u", "code", "del"]; const allowedElements = ["a", "b", "strong", "em", "u", "code", "del"]
const CustomLink = ({ children, ...props }) => ( const CustomLink = ({ children, ...props }) => (
<a {...props} target="_blank" rel="noopener noreferrer"> <a {...props} target="_blank" rel="noopener noreferrer">
{children} {children}
</a> </a>
); )
const MarkdownWrapper = ({ children }) => { const MarkdownWrapper = ({ children }) => {
const boldRegex = /\*(.*?)\*/g; const boldRegex = /\*(.*?)\*/g
const tildaRegex = /~(.*?)~/g; const tildaRegex = /~(.*?)~/g
if (children && children.includes('BEGIN:VCARD'))
//children = "Diga olá ao seu novo contato clicando em *conversar*!";
children = null
if (children && children.includes('data:image/'))
children = null
if (children && boldRegex.test(children)) { if (children && boldRegex.test(children)) {
children = children.replace(boldRegex, "**$1**"); children = children.replace(boldRegex, "**$1**")
} }
if (children && tildaRegex.test(children)) { if (children && tildaRegex.test(children)) {
children = children.replace(tildaRegex, "~~$1~~"); children = children.replace(tildaRegex, "~~$1~~")
} }
const options = React.useMemo(() => { const options = React.useMemo(() => {
@ -167,20 +174,20 @@ const MarkdownWrapper = ({ children }) => {
overrides: { overrides: {
a: { component: CustomLink }, a: { component: CustomLink },
}, },
}; }
elements.forEach(element => { elements.forEach(element => {
if (!allowedElements.includes(element)) { if (!allowedElements.includes(element)) {
markdownOptions.overrides[element] = el => el.children || null; markdownOptions.overrides[element] = el => el.children || null
} }
}); })
return markdownOptions; return markdownOptions
}, []); }, [])
if (!children) return null; if (!children) return null
return <Markdown options={options}>{children}</Markdown>; return <Markdown options={options}>{children}</Markdown>
}; }
export default MarkdownWrapper; export default MarkdownWrapper

View File

@ -312,8 +312,6 @@ const MessageInput = ({ ticketStatus }) => {
const handleSendMessage = async (templateParams = null) => { const handleSendMessage = async (templateParams = null) => {
console.log('templateParams: ', templateParams, ' | inputMessage: ', inputMessage)
if (inputMessage.trim() === "") return if (inputMessage.trim() === "") return
setLoading(true) setLoading(true)
@ -324,8 +322,6 @@ const MessageInput = ({ ticketStatus }) => {
if (templateParams) { if (templateParams) {
for (let key in templateParams) { for (let key in templateParams) {
if (templateParams.hasOwnProperty(key)) { if (templateParams.hasOwnProperty(key)) {
// let value = templateParams[key]
// console.log('key: ', key, ' | ', 'VALUE: ', value)
if (key === '_reactName') { if (key === '_reactName') {
templateParams = null templateParams = null
@ -371,7 +367,11 @@ const MessageInput = ({ ticketStatus }) => {
if (!params) return if (!params) return
const body_params = params.find(p => p?.type === 'BODY') const body_params = params?.find(p => p?.type === 'BODY')
console.log('------------> body_params: ', body_params)
if(!body_params) return
let { text } = body_params let { text } = body_params

View File

@ -1,18 +1,18 @@
import React, { useContext, useState, useEffect, useReducer, useRef } from "react"; import React, { useContext, useState, useEffect, useReducer, useRef } from "react"
import { isSameDay, parseISO, format } from "date-fns"; import { isSameDay, parseISO, format } from "date-fns"
import openSocket from "socket.io-client"; import openSocket from "socket.io-client"
import clsx from "clsx"; import clsx from "clsx"
import { AuthContext } from "../../context/Auth/AuthContext"; import { AuthContext } from "../../context/Auth/AuthContext"
import { green } from "@material-ui/core/colors"; import { green } from "@material-ui/core/colors"
import { import {
Button, Button,
CircularProgress, CircularProgress,
Divider, Divider,
IconButton, IconButton,
makeStyles, makeStyles,
} from "@material-ui/core"; } from "@material-ui/core"
import { import {
AccessTime, AccessTime,
Block, Block,
@ -20,15 +20,20 @@ import {
DoneAll, DoneAll,
ExpandMore, ExpandMore,
GetApp, GetApp,
} from "@material-ui/icons"; } from "@material-ui/icons"
import MarkdownWrapper from "../MarkdownWrapper"; import MarkdownWrapper from "../MarkdownWrapper"
import ModalImageCors from "../ModalImageCors"; import VcardPreview from "../VcardPreview"
import MessageOptionsMenu from "../MessageOptionsMenu"; import LocationPreview from "../LocationPreview"
import whatsBackground from "../../assets/wa-background.png"; import Audio from "../Audio"
import api from "../../services/api";
import toastError from "../../errors/toastError"; import ModalImageCors from "../ModalImageCors"
import MessageOptionsMenu from "../MessageOptionsMenu"
import whatsBackground from "../../assets/wa-background.png"
import api from "../../services/api"
import toastError from "../../errors/toastError"
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
messagesListWrapper: { messagesListWrapper: {
@ -257,78 +262,78 @@ const useStyles = makeStyles((theme) => ({
backgroundColor: "inherit", backgroundColor: "inherit",
padding: 10, padding: 10,
}, },
})); }))
const reducer = (state, action) => { const reducer = (state, action) => {
if (action.type === "LOAD_MESSAGES") { if (action.type === "LOAD_MESSAGES") {
const messages = action.payload; const messages = action.payload
const newMessages = []; const newMessages = []
messages.forEach((message) => { messages.forEach((message) => {
const messageIndex = state.findIndex((m) => m.id === message.id); const messageIndex = state.findIndex((m) => m.id === message.id)
if (messageIndex !== -1) { if (messageIndex !== -1) {
state[messageIndex] = message; state[messageIndex] = message
} else { } else {
newMessages.push(message); newMessages.push(message)
} }
}); })
return [...newMessages, ...state]; return [...newMessages, ...state]
} }
if (action.type === "ADD_MESSAGE") { if (action.type === "ADD_MESSAGE") {
const newMessage = action.payload; const newMessage = action.payload
const messageIndex = state.findIndex((m) => m.id === newMessage.id); const messageIndex = state.findIndex((m) => m.id === newMessage.id)
if (messageIndex !== -1) { if (messageIndex !== -1) {
state[messageIndex] = newMessage; state[messageIndex] = newMessage
} else { } else {
state.push(newMessage); state.push(newMessage)
} }
return [...state]; return [...state]
} }
if (action.type === "UPDATE_MESSAGE") { if (action.type === "UPDATE_MESSAGE") {
const messageToUpdate = action.payload; const messageToUpdate = action.payload
const messageIndex = state.findIndex((m) => m.id === messageToUpdate.id); const messageIndex = state.findIndex((m) => m.id === messageToUpdate.id)
if (messageIndex !== -1) { if (messageIndex !== -1) {
state[messageIndex] = messageToUpdate; state[messageIndex] = messageToUpdate
} }
return [...state]; return [...state]
} }
if (action.type === "RESET") { if (action.type === "RESET") {
return []; return []
} }
}; }
const MessagesList = ({ ticketId, isGroup }) => { const MessagesList = ({ ticketId, isGroup }) => {
const classes = useStyles(); const classes = useStyles()
const [messagesList, dispatch] = useReducer(reducer, []); const [messagesList, dispatch] = useReducer(reducer, [])
const [pageNumber, setPageNumber] = useState(1); const [pageNumber, setPageNumber] = useState(1)
const [hasMore, setHasMore] = useState(false); const [hasMore, setHasMore] = useState(false)
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false)
const lastMessageRef = useRef(); const lastMessageRef = useRef()
const [selectedMessage, setSelectedMessage] = useState({}); const [selectedMessage, setSelectedMessage] = useState({})
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null)
const messageOptionsMenuOpen = Boolean(anchorEl); const messageOptionsMenuOpen = Boolean(anchorEl)
const currentTicketId = useRef(ticketId); const currentTicketId = useRef(ticketId)
const [sendSeen, setSendSeen] = useState(false) const [sendSeen, setSendSeen] = useState(false)
const { user } = useContext(AuthContext); const { user } = useContext(AuthContext)
useEffect(() => { useEffect(() => {
dispatch({ type: "RESET" }); dispatch({ type: "RESET" })
setPageNumber(1); setPageNumber(1)
currentTicketId.current = ticketId; currentTicketId.current = ticketId
}, [ticketId]); }, [ticketId])
useEffect(() => { useEffect(() => {
@ -354,7 +359,7 @@ const MessagesList = ({ ticketId, isGroup }) => {
try { try {
const { data } = await api.get("/messages/" + ticketId, { const { data } = await api.get("/messages/" + ticketId, {
params: { pageNumber }, params: { pageNumber },
}); })
setSendSeen(false) setSendSeen(false)
@ -377,138 +382,239 @@ const MessagesList = ({ ticketId, isGroup }) => {
} }
} catch (err) { } catch (err) {
setLoading(false); setLoading(false)
toastError(err); toastError(err)
} }
}; }
sendSeenMessage(); sendSeenMessage()
}, 500); }, 500)
return () => { return () => {
clearTimeout(delayDebounceFn); clearTimeout(delayDebounceFn)
}; }
}, [sendSeen, pageNumber, ticketId, user.id]); }, [sendSeen, pageNumber, ticketId, user.id])
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true)
const delayDebounceFn = setTimeout(() => { const delayDebounceFn = setTimeout(() => {
const fetchMessages = async () => { const fetchMessages = async () => {
try { try {
const { data } = await api.get("/messages/" + ticketId, { const { data } = await api.get("/messages/" + ticketId, {
params: { pageNumber }, params: { pageNumber },
}); })
if (currentTicketId.current === ticketId) { if (currentTicketId.current === ticketId) {
dispatch({ type: "LOAD_MESSAGES", payload: data.messages }); dispatch({ type: "LOAD_MESSAGES", payload: data.messages })
setHasMore(data.hasMore); setHasMore(data.hasMore)
setLoading(false); setLoading(false)
} }
if (pageNumber === 1 && data.messages.length > 1) { if (pageNumber === 1 && data.messages.length > 1) {
scrollToBottom(); scrollToBottom()
} }
} catch (err) { } catch (err) {
setLoading(false); setLoading(false)
toastError(err); toastError(err)
} }
}; }
fetchMessages(); fetchMessages()
}, 500); }, 500)
return () => { return () => {
clearTimeout(delayDebounceFn); clearTimeout(delayDebounceFn)
}; }
}, [pageNumber, ticketId]); }, [pageNumber, ticketId])
useEffect(() => { useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL); const socket = openSocket(process.env.REACT_APP_BACKEND_URL)
socket.on("connect", () => socket.emit("joinChatBox", ticketId)); socket.on("connect", () => socket.emit("joinChatBox", ticketId))
socket.on("appMessage", (data) => { socket.on("appMessage", (data) => {
if (data.action === "create") { if (data.action === "create") {
dispatch({ type: "ADD_MESSAGE", payload: data.message }); dispatch({ type: "ADD_MESSAGE", payload: data.message })
scrollToBottom(); scrollToBottom()
} }
if (data.action === "update") { if (data.action === "update") {
dispatch({ type: "UPDATE_MESSAGE", payload: data.message }); dispatch({ type: "UPDATE_MESSAGE", payload: data.message })
} }
}); })
return () => { return () => {
socket.disconnect(); socket.disconnect()
}; }
}, [ticketId]); }, [ticketId])
const loadMore = () => { const loadMore = () => {
setPageNumber((prevPageNumber) => prevPageNumber + 1); setPageNumber((prevPageNumber) => prevPageNumber + 1)
}; }
const scrollToBottom = () => { const scrollToBottom = () => {
if (lastMessageRef.current) { if (lastMessageRef.current) {
setSendSeen(true) setSendSeen(true)
lastMessageRef.current.scrollIntoView({}); lastMessageRef.current.scrollIntoView({})
} }
}; }
const handleScroll = (e) => { const handleScroll = (e) => {
if (!hasMore) return; if (!hasMore) return
const { scrollTop } = e.currentTarget; const { scrollTop } = e.currentTarget
if (scrollTop === 0) { if (scrollTop === 0) {
document.getElementById("messagesList").scrollTop = 1; document.getElementById("messagesList").scrollTop = 1
} }
if (loading) { if (loading) {
return; return
} }
if (scrollTop < 50) { if (scrollTop < 50) {
loadMore(); loadMore()
} }
}; }
const handleOpenMessageOptionsMenu = (e, message) => { const handleOpenMessageOptionsMenu = (e, message) => {
setAnchorEl(e.currentTarget); setAnchorEl(e.currentTarget)
setSelectedMessage(message); setSelectedMessage(message)
}; }
const handleCloseMessageOptionsMenu = (e) => { const handleCloseMessageOptionsMenu = (e) => {
setAnchorEl(null); setAnchorEl(null)
}; }
// const checkMessageMedia = (message) => {
// if (message.mediaType === "image") {
// return <ModalImageCors imageUrl={message.mediaUrl} />;
// }
// if (message.mediaType === "audio") {
// return (
// <audio controls>
// <source src={message.mediaUrl} type="audio/ogg"></source>
// </audio>
// );
// }
// if (message.mediaType === "video") {
// return (
// <video
// className={classes.messageMedia}
// src={message.mediaUrl}
// controls
// />
// );
// } else {
// return (
// <>
// <div className={classes.downloadMedia}>
// <Button
// startIcon={<GetApp />}
// color="primary"
// variant="outlined"
// target="_blank"
// href={message.mediaUrl}
// >
// Download
// </Button>
// </div>
// <Divider />
// </>
// );
// }
// };
const checkMessageMedia = (message) => { const checkMessageMedia = (message) => {
if (message.mediaType === "image") { if (message.mediaType === "location" && message.body.split('|').length >= 2) {
return <ModalImageCors imageUrl={message.mediaUrl} />; let locationParts = message.body.split('|')
} let imageLocation = locationParts[0]
if (message.mediaType === "audio") { let linkLocation = locationParts[1]
return ( let descriptionLocation = null
<audio controls>
<source src={message.mediaUrl} type="audio/ogg"></source>
</audio>
);
}
if (message.mediaType === "video") { if (locationParts.length > 2)
descriptionLocation = message.body.split('|')[2]
return <LocationPreview image={imageLocation} link={linkLocation} description={descriptionLocation} />
}
else if (message.mediaType === "vcard") {
let array = message.body.split("\n")
let obj = []
let contact = ""
for (let index = 0; index < array.length; index++) {
const v = array[index]
let values = v.split(":")
for (let ind = 0; ind < values.length; ind++) {
if (values[ind].indexOf("+") !== -1) {
obj.push({ number: values[ind] })
}
if (values[ind].indexOf("FN") !== -1) {
contact = values[ind + 1]
}
}
}
return <VcardPreview contact={contact} numbers={obj[0]?.number} />
}
else if (message.mediaType === "multi_vcard") {
if (message.body !== null && message.body !== "") {
let newBody = JSON.parse(message.body)
let multi_vcard = newBody.map(v => {
let array = v.split("\n")
let obj = []
let contact = ""
for (let index = 0; index < array.length; index++) {
const v = array[index]
let values = v.split(":")
for (let ind = 0; ind < values.length; ind++) {
if (values[ind].indexOf("+") !== -1) {
obj.push({ number: values[ind] })
}
if (values[ind].indexOf("FN") !== -1) {
contact = values[ind + 1]
}
}
}
return { name: contact, number: obj[0]?.number }
})
return (
<>
{
multi_vcard.map((v, index) => (
<>
<VcardPreview contact={v.name} numbers={v.number} multi_vCard={true} id={v.number} />
{((index + 1) <= multi_vcard.length - 1) && <Divider />}
</>
))
}
</>
)
} else return (<></>)
}
else if (/^.*\.(jpe?g|png|gif)?$/i.exec(message.mediaUrl) && message.mediaType === "image") {
return <ModalImageCors imageUrl={message.mediaUrl} />
} else if (message.mediaType === "audio") {
return <Audio url={message.mediaUrl} />
} else if (message.mediaType === "video") {
return ( return (
<video <video
className={classes.messageMedia} className={classes.messageMedia}
src={message.mediaUrl} src={message.mediaUrl}
controls controls
/> />
); )
} else { } else {
return ( return (
<> <>
@ -525,24 +631,24 @@ const MessagesList = ({ ticketId, isGroup }) => {
</div> </div>
<Divider /> <Divider />
</> </>
); )
} }
}; }
const renderMessageAck = (message) => { const renderMessageAck = (message) => {
if (message.ack === 0) { if (message.ack === 0) {
return <AccessTime fontSize="small" className={classes.ackIcons} />; return <AccessTime fontSize="small" className={classes.ackIcons} />
} }
if (message.ack === 1) { if (message.ack === 1) {
return <Done fontSize="small" className={classes.ackIcons} />; return <Done fontSize="small" className={classes.ackIcons} />
} }
if (message.ack === 2) { if (message.ack === 2) {
return <DoneAll fontSize="small" className={classes.ackIcons} />; return <DoneAll fontSize="small" className={classes.ackIcons} />
} }
if (message.ack === 3 || message.ack === 4) { if (message.ack === 3 || message.ack === 4) {
return <DoneAll fontSize="small" className={classes.ackDoneAllIcon} />; return <DoneAll fontSize="small" className={classes.ackDoneAllIcon} />
} }
}; }
const renderDailyTimestamps = (message, index) => { const renderDailyTimestamps = (message, index) => {
if (index === 0) { if (index === 0) {
@ -555,12 +661,12 @@ const MessagesList = ({ ticketId, isGroup }) => {
{format(parseISO(messagesList[index].createdAt), "dd/MM/yyyy")} {format(parseISO(messagesList[index].createdAt), "dd/MM/yyyy")}
</div> </div>
</span> </span>
); )
} }
if (index < messagesList.length - 1) { if (index < messagesList.length - 1) {
let messageDay = parseISO(messagesList[index].createdAt); let messageDay = parseISO(messagesList[index].createdAt)
let previousMessageDay = parseISO(messagesList[index - 1].createdAt); let previousMessageDay = parseISO(messagesList[index - 1].createdAt)
if (!isSameDay(messageDay, previousMessageDay)) { if (!isSameDay(messageDay, previousMessageDay)) {
@ -573,14 +679,14 @@ const MessagesList = ({ ticketId, isGroup }) => {
{format(parseISO(messagesList[index].createdAt), "dd/MM/yyyy")} {format(parseISO(messagesList[index].createdAt), "dd/MM/yyyy")}
</div> </div>
</span> </span>
); )
} }
} }
if (index === messagesList.length - 1) { if (index === messagesList.length - 1) {
let messageDay = parseISO(messagesList[index].createdAt); let messageDay = parseISO(messagesList[index].createdAt)
let previousMessageDay = parseISO(messagesList[index - 1].createdAt); let previousMessageDay = parseISO(messagesList[index - 1].createdAt)
return ( return (
<> <>
@ -600,24 +706,24 @@ const MessagesList = ({ ticketId, isGroup }) => {
style={{ float: "left", clear: "both" }} style={{ float: "left", clear: "both" }}
/> />
</> </>
); )
} }
}; }
const renderMessageDivider = (message, index) => { const renderMessageDivider = (message, index) => {
if (index < messagesList.length && index > 0) { if (index < messagesList.length && index > 0) {
let messageUser = messagesList[index].fromMe; let messageUser = messagesList[index].fromMe
let previousMessageUser = messagesList[index - 1].fromMe; let previousMessageUser = messagesList[index - 1].fromMe
if (messageUser !== previousMessageUser) { if (messageUser !== previousMessageUser) {
return ( return (
<span style={{ marginTop: 16 }} key={`divider-${message.id}`}></span> <span style={{ marginTop: 16 }} key={`divider-${message.id}`}></span>
); )
} }
} }
}; }
const renderQuotedMessage = (message) => { const renderQuotedMessage = (message) => {
return ( return (
@ -640,8 +746,90 @@ const MessagesList = ({ ticketId, isGroup }) => {
{message.quotedMsg?.body} {message.quotedMsg?.body}
</div> </div>
</div> </div>
); )
}; }
// const renderMessages = () => {
// if (messagesList.length > 0) {
// const viewMessagesList = messagesList.map((message, index) => {
// if (!message.fromMe) {
// return (
// <React.Fragment key={message.id}>
// {renderDailyTimestamps(message, index)}
// {renderMessageDivider(message, index)}
// <div className={classes.messageLeft}>
// <IconButton
// variant="contained"
// size="small"
// id="messageActionsButton"
// disabled={message.isDeleted}
// className={classes.messageActionsButton}
// onClick={(e) => handleOpenMessageOptionsMenu(e, message)}
// >
// <ExpandMore />
// </IconButton>
// {isGroup && (
// <span className={classes.messageContactName}>
// {message.contact?.name}
// </span>
// )}
// {message.mediaUrl && checkMessageMedia(message)}
// <div className={classes.textContentItem}>
// {message.quotedMsg && renderQuotedMessage(message)}
// <MarkdownWrapper>{message.body}</MarkdownWrapper>
// <span className={classes.timestamp}>
// {format(parseISO(message.createdAt), "HH:mm")}
// </span>
// </div>
// </div>
// </React.Fragment>
// );
// } else {
// return (
// <React.Fragment key={message.id}>
// {renderDailyTimestamps(message, index)}
// {renderMessageDivider(message, index)}
// <div className={classes.messageRight}>
// <IconButton
// variant="contained"
// size="small"
// id="messageActionsButton"
// disabled={message.isDeleted}
// className={classes.messageActionsButton}
// onClick={(e) => handleOpenMessageOptionsMenu(e, message)}
// >
// <ExpandMore />
// </IconButton>
// {message.mediaUrl && checkMessageMedia(message)}
// <div
// className={clsx(classes.textContentItem, {
// [classes.textContentItemDeleted]: message.isDeleted,
// })}
// >
// {message.isDeleted && (
// <Block
// color="disabled"
// fontSize="small"
// className={classes.deletedIcon}
// />
// )}
// {message.quotedMsg && renderQuotedMessage(message)}
// <MarkdownWrapper>{message.body}</MarkdownWrapper>
// <span className={classes.timestamp}>
// {format(parseISO(message.createdAt), "HH:mm")}
// {renderMessageAck(message)}
// </span>
// </div>
// </div>
// </React.Fragment>
// );
// }
// });
// return viewMessagesList;
// } else {
// return <div>Say hello to your new contact!</div>;
// }
// };
const renderMessages = () => { const renderMessages = () => {
if (messagesList.length > 0) { if (messagesList.length > 0) {
@ -667,7 +855,9 @@ const MessagesList = ({ ticketId, isGroup }) => {
{message.contact?.name} {message.contact?.name}
</span> </span>
)} )}
{message.mediaUrl && checkMessageMedia(message)} {(message.mediaUrl || message.mediaType === "location" || message.mediaType === "vcard"
|| message.mediaType === "multi_vcard"
) && checkMessageMedia(message)}
<div className={classes.textContentItem}> <div className={classes.textContentItem}>
{message.quotedMsg && renderQuotedMessage(message)} {message.quotedMsg && renderQuotedMessage(message)}
<MarkdownWrapper>{message.body}</MarkdownWrapper> <MarkdownWrapper>{message.body}</MarkdownWrapper>
@ -677,7 +867,7 @@ const MessagesList = ({ ticketId, isGroup }) => {
</div> </div>
</div> </div>
</React.Fragment> </React.Fragment>
); )
} else { } else {
return ( return (
<React.Fragment key={message.id}> <React.Fragment key={message.id}>
@ -694,7 +884,9 @@ const MessagesList = ({ ticketId, isGroup }) => {
> >
<ExpandMore /> <ExpandMore />
</IconButton> </IconButton>
{message.mediaUrl && checkMessageMedia(message)} {(message.mediaUrl || message.mediaType === "location" || message.mediaType === "vcard"
// || message.mediaType === "multi_vcard"
) && checkMessageMedia(message)}
<div <div
className={clsx(classes.textContentItem, { className={clsx(classes.textContentItem, {
[classes.textContentItemDeleted]: message.isDeleted, [classes.textContentItemDeleted]: message.isDeleted,
@ -716,14 +908,14 @@ const MessagesList = ({ ticketId, isGroup }) => {
</div> </div>
</div> </div>
</React.Fragment> </React.Fragment>
); )
} }
}); })
return viewMessagesList; return viewMessagesList
} else { } else {
return <div>Say hello to your new contact!</div>; return <div>Say hello to your new contact!</div>
} }
}; }
return ( return (
<div className={classes.messagesListWrapper}> <div className={classes.messagesListWrapper}>
@ -746,7 +938,7 @@ const MessagesList = ({ ticketId, isGroup }) => {
</div> </div>
)} )}
</div> </div>
); )
}; }
export default MessagesList; export default MessagesList

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef, } from 'react' import React, { useState, useEffect, useRef, } from 'react'
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
import Dialog from '@mui/material/Dialog' import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions' import DialogActions from '@mui/material/DialogActions'
@ -16,6 +16,8 @@ const ModalTemplate = ({ templates, modal_header, func }) => {
templates = [{}, ...templates] templates = [{}, ...templates]
// console.log('TEMPLATES: ', templates)
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const [scroll, /*setScroll*/] = useState('body') const [scroll, /*setScroll*/] = useState('body')
const [templateId, setTemplateId] = useState(null) const [templateId, setTemplateId] = useState(null)
@ -34,7 +36,25 @@ const ModalTemplate = ({ templates, modal_header, func }) => {
const handleChatEnd = () => { const handleChatEnd = () => {
console.log('PARAMS TO SEND TO MESSAGE INPUT: ', params) console.log('PARAMS TO SEND TO MESSAGE INPUT: ', params)
func(params) console.log('templateComponents: ', templateComponents)
if (params && params.length === 1) {
const bodyObject = templateComponents.find(obj => obj?.type === 'BODY')
if (bodyObject) {
const { text } = bodyObject
func([...params, {
"type": "BODY",
"text": text,
"language": "pt_BR",
}])
}
}
else {
func(params)
}
setOpen(false) setOpen(false)
} }
@ -127,7 +147,9 @@ const ModalTemplate = ({ templates, modal_header, func }) => {
useEffect(() => { useEffect(() => {
console.log('---------> PARAMS: ', params) console.log('---------> PARAMS: ', params)
}, [params]) console.log('---------> templateComponents: ', templateComponents)
}, [params, templateComponents])
const dinamicTextField = (replicateItems, func, type, text, language) => { const dinamicTextField = (replicateItems, func, type, text, language) => {
@ -204,7 +226,7 @@ const ModalTemplate = ({ templates, modal_header, func }) => {
{text && {text &&
<div style={{ margin: 0, padding: 0, 'marginBottom': '15px' }}> <div style={{ margin: 0, padding: 0, 'marginBottom': '15px' }}>
<p style={{ margin: 0, padding: 0, fontSize: 12 }}>{text}</p> <p style={{ margin: 0, padding: 0, fontSize: 12 }}>{text}</p>
{type && (type === 'BODY') && dinamicTextField(body_params.length, handleTextChange, type, text, language)} {type && (type === 'BODY') && body_params && dinamicTextField(body_params.length, handleTextChange, type, text, language)}
</div>} </div>}
{buttons && <div>{buttons.map((b) => { {buttons && <div>{buttons.map((b) => {
const { type, text, url } = b const { type, text, url } = b

View File

@ -41,6 +41,28 @@ const useStyles = makeStyles(theme => ({
})) }))
let _fifo
// const onlineEmitter = async (socket, user) => {
// try {
// clearInterval(_fifo);
// socket.emit("online", user.id)
// } catch (error) {
// console.log('error on onlineEmitter: ', error)
// }
// finally {
// _fifo = setInterval(onlineEmitter, 3000);
// }
// }
// _fifo = setInterval(onlineEmitter, 3000);
const NotificationsPopOver = () => { const NotificationsPopOver = () => {
const classes = useStyles() const classes = useStyles()
@ -65,6 +87,8 @@ const NotificationsPopOver = () => {
// const [lastRef] = useState(+history.location.pathname.split("/")[2]) // const [lastRef] = useState(+history.location.pathname.split("/")[2])
useEffect(() => { useEffect(() => {
soundAlertRef.current = play soundAlertRef.current = play
@ -80,6 +104,9 @@ const NotificationsPopOver = () => {
}, [tickets]) }, [tickets])
useEffect(() => { useEffect(() => {
ticketIdRef.current = ticketIdUrl ticketIdRef.current = ticketIdUrl
}, [ticketIdUrl]) }, [ticketIdUrl])
@ -103,6 +130,8 @@ const NotificationsPopOver = () => {
if (data.action === "logout") { if (data.action === "logout") {
if (`${user.id}` === data.userOnlineTime['userId']) { if (`${user.id}` === data.userOnlineTime['userId']) {
socket.emit("online", { logoutUserId: user.id }) socket.emit("online", { logoutUserId: user.id })
@ -111,6 +140,7 @@ const NotificationsPopOver = () => {
} }
}) })
// socket.on("isOnline", (data) => { // socket.on("isOnline", (data) => {
@ -129,24 +159,21 @@ const NotificationsPopOver = () => {
if (user.profile === 'user') { if (user.profile === 'user') {
// if (_fifo) { if (_fifo) {
// clearInterval(_fifo); clearInterval(_fifo)
// } }
// _fifo = setInterval(() => { _fifo = setInterval(() => {
// socket.emit("online", user.id)
// }, 3000);
const intID = setInterval(() => {
console.log('emitting the online')
socket.emit("online", user.id) socket.emit("online", user.id)
}, 3000) }, 3000)
return () => clearInterval(intID)
} }
return () => { return () => {
socket.disconnect() socket.disconnect()
} }
@ -164,6 +191,8 @@ const NotificationsPopOver = () => {
socket.on("ticket", data => { socket.on("ticket", data => {
if (data.action === "updateUnread" || data.action === "delete") { if (data.action === "updateUnread" || data.action === "delete") {
setNotifications(prevState => { setNotifications(prevState => {
const ticketIndex = prevState.findIndex(t => t.id === data.ticketId) const ticketIndex = prevState.findIndex(t => t.id === data.ticketId)
if (ticketIndex !== -1) { if (ticketIndex !== -1) {
@ -189,14 +218,25 @@ const NotificationsPopOver = () => {
socket.on("appMessage", data => { socket.on("appMessage", data => {
if ( if (
data.action === "create" && data.action === "create" &&
!data.message.read && !data.message.read &&
(data.ticket.userId === user?.id || !data.ticket.userId) (data.ticket.userId === user?.id || !data.ticket.userId)
) { ) {
setNotifications(prevState => { setNotifications(prevState => {
// prevState.forEach((e)=>{
//
// })
const ticketIndex = prevState.findIndex(t => t.id === data.ticket.id) const ticketIndex = prevState.findIndex(t => t.id === data.ticket.id)
if (ticketIndex !== -1) { if (ticketIndex !== -1) {
@ -207,12 +247,16 @@ const NotificationsPopOver = () => {
return [data.ticket, ...prevState] return [data.ticket, ...prevState]
}) })
const shouldNotNotificate = (data.message.ticketId === ticketIdRef.current && document.visibilityState === "visible") || const shouldNotNotificate = (data.message.ticketId === ticketIdRef.current && document.visibilityState === "visible") ||
(data.ticket.userId && data.ticket.userId !== user?.id) || (data.ticket.userId && data.ticket.userId !== user?.id) ||
data.ticket.isGroup || !data.ticket.userId data.ticket.isGroup || !data.ticket.userId
if (shouldNotNotificate) return if (shouldNotNotificate) return
handleNotifications(data) handleNotifications(data)
} }
}) })

View File

@ -0,0 +1,135 @@
import React, { useState, useEffect } from "react"
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import DialogContentText from '@mui/material/DialogContentText'
import DialogTitle from '@mui/material/DialogTitle'
import FormControl from '@mui/material/FormControl'
import InputLabel from '@mui/material/InputLabel'
import MenuItem from '@mui/material/MenuItem'
import Select from '@mui/material/Select'
export default function MaxWidthDialog(props) {
const [open, setOpen] = useState(false)
const [fullWidth,] = useState(true)
const [currency, setCurrency] = useState(props.reportOption)
const [textOption, setTextOption] = useState('')
useEffect(() => {
// props.func(currency)
if(currency === '2' || currency === '3'){
setTextOption('Retorna apenas tickets com status: fechado')
}
else{
setTextOption('Retorna todos os tickets com status: aberto, fechado, pendente')
}
}, [currency, props])
const handleClickOpen = () => {
setOpen(true)
}
const handleClose = () => {
props.func(currency)
setOpen(false)
}
const handleMaxWidthChange = (event) => {
setCurrency(event.target.value)
}
return (
<React.Fragment>
<Button variant="outlined" onClick={handleClickOpen}>
CSV ALL
</Button>
<Dialog
fullWidth={fullWidth}
maxWidth={'sm'}
open={open}
onClose={null}
>
<DialogTitle>Relatórios</DialogTitle>
<DialogContent>
<DialogContentText>
Escolha uma opção do tipo de relatório abaixo
</DialogContentText>
<Box
noValidate
component="form"
sx={{
display: 'flex',
flexDirection: 'column',
m: 'auto',
width: 'fit-content',
}}
>
<FormControl sx={{ mt: 2, minWidth: 420 }}>
<InputLabel htmlFor="opcoes">opcoes</InputLabel>
<Select
autoFocus
value={currency}
onChange={handleMaxWidthChange}
label="opcoes"
inputProps={{
name: 'opcoes',
id: 'opcoes',
}}
>
{props.currencies.map((option, index) => (
<MenuItem key={index} value={option.value}> {option.label} </MenuItem>
))}
</Select>
</FormControl>
{/* <FormControlLabel
sx={{ mt: 1 }}
control={
<Switch checked={fullWidth} onChange={handleFullWidthChange} />
}
label="Full width"
/> */}
</Box>
</DialogContent>
<div style={{ display: 'flex', justifyContent: "center" }}>{textOption}</div>
<div style={{ display: 'flex', justifyContent: "right" }}>
<DialogActions>
<Button onClick={() => setOpen(false)}>Cancelar</Button>
</DialogActions>
<DialogActions>
<Button onClick={handleClose}>Ok</Button>
</DialogActions>
</div>
</Dialog>
</React.Fragment>
)
}

View File

@ -1,20 +1,20 @@
import React, { useState, useEffect, useReducer, useContext } from "react"; import React, { useState, useEffect, useReducer, useContext } from "react"
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 List from "@material-ui/core/List"; import List from "@material-ui/core/List"
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper"
import TicketListItem from "../TicketListItem"; import TicketListItem from "../TicketListItem"
import TicketsListSkeleton from "../TicketsListSkeleton"; import TicketsListSkeleton from "../TicketsListSkeleton"
import useTickets from "../../hooks/useTickets"; import useTickets from "../../hooks/useTickets"
import { i18n } from "../../translate/i18n"; import { i18n } from "../../translate/i18n"
import { AuthContext } from "../../context/Auth/AuthContext"; import { AuthContext } from "../../context/Auth/AuthContext"
import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket"; import { SearchTicketContext } from "../../context/SearchTicket/SearchTicket"
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
ticketsListWrapper: { ticketsListWrapper: {
@ -73,64 +73,64 @@ const useStyles = makeStyles(theme => ({
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
}, },
})); }))
const reducer = (state, action) => { const reducer = (state, action) => {
if (action.type === "LOAD_TICKETS") { if (action.type === "LOAD_TICKETS") {
const newTickets = action.payload; const newTickets = action.payload
newTickets.forEach(ticket => { newTickets.forEach(ticket => {
const ticketIndex = state.findIndex(t => +t.id === +ticket.id); const ticketIndex = state.findIndex(t => +t.id === +ticket.id)
if (ticketIndex !== -1) { if (ticketIndex !== -1) {
state[ticketIndex] = ticket; state[ticketIndex] = ticket
if (+ticket.unreadMessages > 0) { if (+ticket.unreadMessages > 0) {
state.unshift(state.splice(ticketIndex, 1)[0]); state.unshift(state.splice(ticketIndex, 1)[0])
} }
} else { } else {
state.push(ticket); state.push(ticket)
} }
}); })
return [...state]; return [...state]
} }
if (action.type === "RESET_UNREAD") { if (action.type === "RESET_UNREAD") {
const ticketId = action.payload; const ticketId = action.payload
const ticketIndex = state.findIndex(t => +t.id === +ticketId); const ticketIndex = state.findIndex(t => +t.id === +ticketId)
if (ticketIndex !== -1) { if (ticketIndex !== -1) {
state[ticketIndex].unreadMessages = 0; state[ticketIndex].unreadMessages = 0
} }
return [...state]; return [...state]
} }
if (action.type === "UPDATE_TICKET") { if (action.type === "UPDATE_TICKET") {
const ticket = action.payload; const ticket = action.payload
const ticketIndex = state.findIndex(t => +t.id === +ticket.id); const ticketIndex = state.findIndex(t => +t.id === +ticket.id)
if (ticketIndex !== -1) { if (ticketIndex !== -1) {
state[ticketIndex] = ticket; state[ticketIndex] = ticket
} else { } else {
state.unshift(ticket); state.unshift(ticket)
} }
return [...state]; return [...state]
} }
if (action.type === "UPDATE_TICKET_UNREAD_MESSAGES") { if (action.type === "UPDATE_TICKET_UNREAD_MESSAGES") {
const message = action.payload.message const message = action.payload.message
const ticket = action.payload.ticket; const ticket = action.payload.ticket
const ticketIndex = state.findIndex(t => +t.id === +ticket.id); const ticketIndex = state.findIndex(t => +t.id === +ticket.id)
if (ticketIndex !== -1) { if (ticketIndex !== -1) {
@ -142,55 +142,55 @@ const reducer = (state, action) => {
ticket.unreadMessages += 1 ticket.unreadMessages += 1
} }
state[ticketIndex] = ticket; state[ticketIndex] = ticket
state.unshift(state.splice(ticketIndex, 1)[0]); state.unshift(state.splice(ticketIndex, 1)[0])
} else { } else {
state.unshift(ticket); state.unshift(ticket)
} }
return [...state]; return [...state]
} }
if (action.type === "UPDATE_TICKET_CONTACT") { if (action.type === "UPDATE_TICKET_CONTACT") {
const contact = action.payload; const contact = action.payload
const ticketIndex = state.findIndex(t => +t.contactId === +contact.id); const ticketIndex = state.findIndex(t => +t.contactId === +contact.id)
if (ticketIndex !== -1) { if (ticketIndex !== -1) {
state[ticketIndex].contact = contact; state[ticketIndex].contact = contact
} }
return [...state]; return [...state]
} }
if (action.type === "DELETE_TICKET") { if (action.type === "DELETE_TICKET") {
const ticketId = action.payload; const ticketId = action.payload
const ticketIndex = state.findIndex(t => +t.id === +ticketId); const ticketIndex = state.findIndex(t => +t.id === +ticketId)
if (ticketIndex !== -1) { if (ticketIndex !== -1) {
state.splice(ticketIndex, 1); state.splice(ticketIndex, 1)
} }
return [...state]; return [...state]
} }
if (action.type === "RESET") { if (action.type === "RESET") {
return []; return []
} }
}; }
const TicketsList = (props) => { const TicketsList = (props) => {
const { status, searchParam, searchParamContent, showAll, selectedQueueIds, updateCount, style, tab } = props; const { status, searchParam, searchParamContent, showAll, selectedQueueIds, updateCount, style, tab } = props
const classes = useStyles(); const classes = useStyles()
const [pageNumber, setPageNumber] = useState(1); const [pageNumber, setPageNumber] = useState(1)
const [ticketsList, dispatch] = useReducer(reducer, []); const [ticketsList, dispatch] = useReducer(reducer, [])
const { user } = useContext(AuthContext); const { user } = useContext(AuthContext)
const { searchTicket } = useContext(SearchTicketContext) const { searchTicket } = useContext(SearchTicketContext)
useEffect(() => { useEffect(() => {
dispatch({ type: "RESET" }); dispatch({ type: "RESET" })
setPageNumber(1); setPageNumber(1)
}, [status, searchParam, searchParamContent, showAll, selectedQueueIds, searchTicket]); }, [status, searchParam, searchParamContent, showAll, selectedQueueIds, searchTicket])
const { tickets, hasMore, loading } = useTickets({ const { tickets, hasMore, loading } = useTickets({
pageNumber, pageNumber,
@ -199,12 +199,13 @@ const TicketsList = (props) => {
status, status,
showAll, showAll,
queueIds: JSON.stringify(selectedQueueIds), queueIds: JSON.stringify(selectedQueueIds),
tab tab,
}); unlimited: status === 'open' ? "all" : "false"
})
useEffect(() => { useEffect(() => {
if (!status && !searchParam) return; if (!status && !searchParam) return
// if (searchParam) { // if (searchParam) {
// //
@ -217,31 +218,31 @@ const TicketsList = (props) => {
dispatch({ type: "RESET" }) dispatch({ type: "RESET" })
} }
dispatch({ type: "LOAD_TICKETS", payload: tickets, }); dispatch({ type: "LOAD_TICKETS", payload: tickets, })
}, [tickets, status, searchParam, pageNumber]); }, [tickets, status, searchParam, pageNumber])
useEffect(() => { useEffect(() => {
// if (tab=='search')return // if (tab=='search')return
const socket = openSocket(process.env.REACT_APP_BACKEND_URL); const socket = openSocket(process.env.REACT_APP_BACKEND_URL)
const shouldUpdateTicket = ticket => const shouldUpdateTicket = ticket =>
(!ticket.userId || ticket.userId === user?.id || showAll) && (!ticket.userId || ticket.userId === user?.id || showAll) &&
(!ticket.queueId || selectedQueueIds.indexOf(ticket.queueId) > -1); (!ticket.queueId || selectedQueueIds.indexOf(ticket.queueId) > -1)
const notBelongsToUserQueues = ticket => const notBelongsToUserQueues = ticket =>
ticket.queueId && selectedQueueIds.indexOf(ticket.queueId) === -1; ticket.queueId && selectedQueueIds.indexOf(ticket.queueId) === -1
socket.on("connect", () => { socket.on("connect", () => {
if (status) { if (status) {
socket.emit("joinTickets", status); socket.emit("joinTickets", status)
} else { } else {
socket.emit("joinNotification"); socket.emit("joinNotification")
} }
}); })
@ -254,7 +255,7 @@ const TicketsList = (props) => {
dispatch({ dispatch({
type: "RESET_UNREAD", type: "RESET_UNREAD",
payload: data.ticketId, payload: data.ticketId,
}); })
} }
if (data.action === "update" && shouldUpdateTicket(data.ticket)) { if (data.action === "update" && shouldUpdateTicket(data.ticket)) {
@ -264,17 +265,17 @@ const TicketsList = (props) => {
dispatch({ dispatch({
type: "UPDATE_TICKET", type: "UPDATE_TICKET",
payload: data.ticket, payload: data.ticket,
}); })
} }
if (data.action === "update" && notBelongsToUserQueues(data.ticket)) { if (data.action === "update" && notBelongsToUserQueues(data.ticket)) {
dispatch({ type: "DELETE_TICKET", payload: data.ticket.id }); dispatch({ type: "DELETE_TICKET", payload: data.ticket.id })
} }
if (data.action === "delete") { if (data.action === "delete") {
dispatch({ type: "DELETE_TICKET", payload: data.ticketId }); dispatch({ type: "DELETE_TICKET", payload: data.ticketId })
} }
}); })
socket.on("appMessage", data => { socket.on("appMessage", data => {
@ -287,51 +288,49 @@ const TicketsList = (props) => {
type: "UPDATE_TICKET_UNREAD_MESSAGES", type: "UPDATE_TICKET_UNREAD_MESSAGES",
// payload: data.ticket, // payload: data.ticket,
payload: data, payload: data,
}); })
} }
}); })
socket.on("contact", data => { socket.on("contact", data => {
if (data.action === "update") { if (data.action === "update") {
dispatch({ dispatch({
type: "UPDATE_TICKET_CONTACT", type: "UPDATE_TICKET_CONTACT",
payload: data.contact, payload: data.contact,
}); })
} }
}); })
return () => { return () => {
socket.disconnect(); socket.disconnect()
}; }
}, [status, showAll, user, selectedQueueIds, tab]); }, [status, showAll, user, selectedQueueIds, tab])
useEffect(() => { useEffect(() => {
if (typeof updateCount === "function") { if (typeof updateCount === "function") {
updateCount(ticketsList.length); updateCount(ticketsList.length)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ticketsList]); }, [ticketsList])
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()
} }
}; }
return ( return (
<Paper className={classes.ticketsListWrapper} style={style}> <Paper className={classes.ticketsListWrapper} style={style}>
@ -363,7 +362,7 @@ const TicketsList = (props) => {
</List> </List>
</Paper> </Paper>
</Paper> </Paper>
); )
}; }
export default TicketsList; export default TicketsList

View File

@ -0,0 +1,96 @@
import React, { useEffect, useState, useContext } from 'react'
import { useHistory } from "react-router-dom"
import toastError from "../../errors/toastError"
import api from "../../services/api"
import Avatar from "@material-ui/core/Avatar"
import Typography from "@material-ui/core/Typography"
import Grid from "@material-ui/core/Grid"
import { AuthContext } from "../../context/Auth/AuthContext"
import { Button, Divider, } from "@material-ui/core"
const VcardPreview = ({ contact, numbers, multi_vCard }) => {
const history = useHistory()
const { user } = useContext(AuthContext)
const [selectedContact, setContact] = useState({
name: "",
number: 0,
profilePicUrl: ""
})
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
const fetchContacts = async () => {
try {
let contactObj = {
name: contact,
// number: numbers.replace(/\D/g, ""),
number: numbers !== undefined && numbers.replace(/\D/g, ""),
email: ""
}
const { data } = await api.post("/contact", contactObj)
setContact(data)
} catch (err) {
console.log(err)
toastError(err)
}
}
fetchContacts()
}, 500)
return () => clearTimeout(delayDebounceFn)
}, [contact, numbers])
const handleNewChat = async () => {
try {
const { data: ticket } = await api.post("/tickets", {
contactId: selectedContact.id,
userId: user.id,
status: "open",
})
history.push(`/tickets/${ticket.id}`)
} catch (err) {
toastError(err)
}
}
return (
<>
<div style={{
minWidth: "250px",
}}>
<Grid container spacing={1}>
<Grid item xs={2}>
<Avatar src={selectedContact.profilePicUrl} />
</Grid>
<Grid item xs={9}>
<Typography style={{ marginTop: "12px", marginLeft: "10px" }} variant="subtitle1" color="primary" gutterBottom>
{selectedContact.name}
</Typography>
</Grid>
<Grid item xs={12}>
{!multi_vCard && <Divider />}
<Button
fullWidth
color="primary"
onClick={handleNewChat}
disabled={!selectedContact.number}
>Conversar</Button>
{/* {multi_vCard && <Divider />} */}
</Grid>
</Grid>
</div>
</>
)
}
export default VcardPreview

View File

@ -70,6 +70,7 @@ const WhatsAppModal = ({ open, onClose, whatsAppId, whatsAppOfficial }) => {
farewellMessage: '', farewellMessage: '',
isDefault: false, isDefault: false,
isOfficial: false, isOfficial: false,
number: '',
phoneNumberId: '', phoneNumberId: '',
wabaId: '' wabaId: ''
} }
@ -109,6 +110,7 @@ const WhatsAppModal = ({ open, onClose, whatsAppId, whatsAppOfficial }) => {
if (!isOfficial) { if (!isOfficial) {
values.phoneNumberId = '' values.phoneNumberId = ''
values.wabaId = '' values.wabaId = ''
values.number = ''
} }
@ -276,6 +278,18 @@ const WhatsAppModal = ({ open, onClose, whatsAppId, whatsAppOfficial }) => {
margin="dense" margin="dense"
className={classes.textField} className={classes.textField}
/> />
<Field
as={TextField}
label="Phone number"
autoFocus
name="number"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
className={classes.textField}
/>
</div> </div>

View File

@ -220,7 +220,7 @@ const LoggedInLayout = ({ children }) => {
className={classes.title} className={classes.title}
> >
OMNIHIT Espaçolaser - Lojas OMNIHIT
</Typography> </Typography>
{user.id && <NotificationsPopOver />} {user.id && <NotificationsPopOver />}

View File

@ -20,7 +20,7 @@ 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: "true" }); let { tickets } = useTickets({ date: date.current, unlimited: "current" });
const [chartData, setChartData] = useState([ const [chartData, setChartData] = useState([
{ time: "08:00", amount: 0 }, { time: "08:00", amount: 0 },

View File

@ -0,0 +1,134 @@
import { Box } from '@material-ui/core';
import React from 'react';
import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord';
import { PieChart as RechartsPieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
import Title from './Title';
const generateDataExample = (amount) => {
const arr = []
for (let i = 1; i <= amount; i++) {
arr.push({
"id": i,
"name": `Exemplo ${i}`,
"count": Math.floor(Math.random() * 10 + 2)
})
}
return arr
}
const dataExample = generateDataExample(20)
const COLORS = [
'#0088FE', // Azul escuro
'#00C49F', // Verde menta
'#FFBB28', // Laranja escuro
'#FF8042', // Vermelho escuro
'#9D38BD', // Roxo escuro
'#FFD166', // Laranja claro
'#331F00', // Marrom escuro
'#C0FFC0', // Verde Claro
'#C4E538', // Verde-amarelo vibrante
'#A2A2A2', // Cinza claro
'#FFF700', // Amarelo Canário
'#FF69B4', // Rosa Flamingo
'#87CEEB', // Azul Celeste
'#228B22', // Verde Esmeralda
'#9B59B6', // Roxo Ametista
'#FF9933', // Laranja Tangerina
'#FF7F50', // Coral Vivo
'#00CED1', // Verde Água
'#000080', // Azul Marinho
'#FFDB58', // Amarelo Mostarda
];
const RADIAN = Math.PI / 180;
const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, count }) => {
const radius = innerRadius + (outerRadius - innerRadius) * 0.80;
const x = cx + radius * Math.cos(-midAngle * RADIAN);
const y = cy + radius * Math.sin(-midAngle * RADIAN);
return (
<text x={x} y={y} fill="white" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central">
{count}
</text>
);
};
/**
* @param data array de objetos no formato
* {
"id": number | string,
"name": string,
"count": number
* }
*/
const PieChart = ({ data = dataExample }) => {
return (
<Box
width="100%"
height="100%"
position="relative"
display="flex"
sx={{ overflowY: "scroll" }}
>
<Box width="100%" height="100%" position="sticky" top="0" zIndex={1000}>
<Box sx={{ position: "absolute" }}>
<Title>Tickets encerramento</Title>
</Box>
<ResponsiveContainer width="100%" height="100%">
<RechartsPieChart width={400} height={400}>
<Pie
data={data}
cx="25%"
cy="60%"
labelLine={false}
label={renderCustomizedLabel}
outerRadius={100}
fill="#8884d8"
dataKey="count"
>
{data.map((entry, index) => (
<Cell key={`cell-${entry.id}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
</RechartsPieChart>
</ResponsiveContainer>
</Box>
<Box
component="ul"
sx={{
position: "absolute",
top: 0, right: 0,
display: "flex",
flexDirection: "column",
gap: "4px",
maxWidth: "60%",
minWidth: "50%",
zIndex: 0,
}}>
{data.map((entry, index) => {
return (
<Box
component="li"
key={entry.id}
sx={{
display: "flex", gap: "2px",
color: COLORS[index % COLORS.length],
alignItems: "center"
}}>
<FiberManualRecordIcon fill={COLORS[index % COLORS.length]} />
<text style={{ color: 'black' }}>{entry.name}</text>
</Box>
)
})}
</Box>
</Box >
);
}
export default PieChart

View File

@ -15,6 +15,7 @@ import Info from "@material-ui/icons/Info"
import { AuthContext } from "../../context/Auth/AuthContext" import { AuthContext } from "../../context/Auth/AuthContext"
// import { i18n } from "../../translate/i18n"; // import { i18n } from "../../translate/i18n";
import Chart from "./Chart" import Chart from "./Chart"
import PieChart from "./PieChart"
import openSocket from "socket.io-client" import openSocket from "socket.io-client"
import api from "../../services/api" import api from "../../services/api"
@ -31,7 +32,7 @@ const useStyles = makeStyles((theme) => ({
display: "flex", display: "flex",
overflow: "auto", overflow: "auto",
flexDirection: "column", flexDirection: "column",
height: 240, height: 280,
}, },
customFixedHeightPaper: { customFixedHeightPaper: {
padding: theme.spacing(2), padding: theme.spacing(2),
@ -257,7 +258,7 @@ const Dashboard = () => {
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 { user } = useContext(AuthContext) const { user } = useContext(AuthContext)
useEffect(() => { useEffect(() => {
@ -288,10 +289,15 @@ const Dashboard = () => {
params: { userId: null, startDate: dateToday, endDate: dateToday }, params: { userId: null, startDate: dateToday, endDate: dateToday },
}) })
// console.log('data.data: ', data.usersProfile)
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", {
params: { startDate: dateToday, endDate: dateToday },
})
setTicketStatusChatEnd(ticketStatusChatEndData.reportStatusChatEnd)
} catch (err) { } catch (err) {
} }
@ -497,10 +503,17 @@ const Dashboard = () => {
</Grid> </Grid>
</Paper> </Paper>
</Grid> </Grid>
<Grid item xs={12}> <Grid item container spacing={3}>
<Paper className={classes.fixedHeightPaper} variant="outlined"> <Grid item xs={12} sm={12} md={6} lg={6}>
<Chart allTickets={usersOnlineInfo} /> <Paper className={classes.fixedHeightPaper} variant="outlined">
</Paper> <Chart allTickets={usersOnlineInfo} />
</Paper>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={6}>
<Paper className={classes.fixedHeightPaper} variant="outlined">
<PieChart data={ticketStatusChatEnd} />
</Paper>
</Grid>
</Grid> </Grid>
</Grid> </Grid>
</Paper> </Paper>

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useReducer, useContext } from "react" import React, { useState, useEffect, useReducer, useContext, useCallback } from "react"
import MainContainer from "../../components/MainContainer" import MainContainer from "../../components/MainContainer"
import api from "../../services/api" import api from "../../services/api"
import SelectField from "../../components/Report/SelectField" import SelectField from "../../components/Report/SelectField"
@ -9,24 +9,35 @@ import PropTypes from 'prop-types'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import { AuthContext } from "../../context/Auth/AuthContext" import { AuthContext } from "../../context/Auth/AuthContext"
import { Can } from "../../components/Can" import { Can } from "../../components/Can"
import FormControlLabel from "@mui/material/FormControlLabel"
import Checkbox from '@mui/material/Checkbox'
import { Button } from "@material-ui/core" import { Button } from "@material-ui/core"
import ReportModal from "../../components/ReportModal" import ReportModal from "../../components/ReportModal"
import ReportModalType from "../../components/ReportModalType"
import MaterialTable from 'material-table' import MaterialTable from 'material-table'
import LogoutIcon from '@material-ui/icons/CancelOutlined' import LogoutIcon from '@material-ui/icons/CancelOutlined'
import apiBroker from "../../services/apiBroker" import apiBroker from "../../services/apiBroker"
import fileDownload from 'js-file-download' import fileDownload from 'js-file-download'
import openSocket from "socket.io-client" import openSocket from "socket.io-client"
import { i18n } from "../../translate/i18n" import { i18n } from "../../translate/i18n"
import Switch from '@mui/material/Switch'
const label = { inputProps: { 'aria-label': 'Size switch demo' } }
const report = [
{ 'value': '1', 'label': 'Atendimento por atendentes' },
{ 'value': '2', 'label': 'Usuários online/offline' },
{ 'value': '3', 'label': 'Relatorio de atendimento por numeros' },
{ 'value': '4', 'label': 'Relatorio de atendimento por filas' },
]
const reportOptType = [
{ 'value': '1', 'label': 'Padrão' },
{ 'value': '2', 'label': 'Sintético' },
{ 'value': '3', 'label': 'Analítico' }
]
const report = [{ 'value': '1', 'label': 'Atendimento por atendentes' }, { 'value': '2', 'label': 'Usuários online/offline' }]
const reducerQ = (state, action) => { const reducerQ = (state, action) => {
@ -48,9 +59,13 @@ const reducerQ = (state, action) => {
if (action.type === 'LOAD_QUERY') { if (action.type === 'LOAD_QUERY') {
const queries = action.payload let queries = action.payload
const newQueries = [] const newQueries = []
if (queries?.hasOwnProperty('usersProfile')) {
queries = queries.usersProfile
}
queries.forEach((query) => { queries.forEach((query) => {
const queryIndex = state.findIndex((q) => q.id === query.id) const queryIndex = state.findIndex((q) => q.id === query.id)
@ -219,7 +234,10 @@ let columnsData = [
{ title: `${i18n.t("reports.listColumns.column1_7")}`, field: 'createdAt' }, { title: `${i18n.t("reports.listColumns.column1_7")}`, field: 'createdAt' },
{ 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' },
{ title: `Espera`, field: 'waiting_time' },
{ title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true },
]
let columnsDataSuper = [ let columnsDataSuper = [
{ title: `${i18n.t("reports.listColumns.column1_1")}`, field: 'whatsapp.name' }, { title: `${i18n.t("reports.listColumns.column1_1")}`, field: 'whatsapp.name' },
@ -231,7 +249,9 @@ let columnsDataSuper = [
{ title: `${i18n.t("reports.listColumns.column1_7")}`, field: 'createdAt' }, { title: `${i18n.t("reports.listColumns.column1_7")}`, field: 'createdAt' },
{ 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' },
{ title: `Espera`, field: 'waiting_time' },
{ title: `Mensagens`, field: 'messagesToFilter', searchable: true, hidden: true },
] ]
@ -265,23 +285,27 @@ const Report = () => {
const [hasMore, setHasMore] = useState(false) const [hasMore, setHasMore] = useState(false)
const [pageNumberTickets, setTicketsPageNumber] = useState(1) const [pageNumberTickets, setTicketsPageNumber] = useState(1)
const [totalCountTickets, setTotalCountTickets] = useState(0) const [totalCountTickets, setTotalCountTickets] = useState(0)
const [pageNumber, setPageNumber] = useState(1) const [pageNumber, setPageNumber] = useState(1)
const [users, dispatch] = useReducer(reducer, []) const [users, dispatch] = useReducer(reducer, [])
const [startDate, setDatePicker1] = useState(new Date()) const [startDate, setDatePicker1] = useState(new Date())
const [endDate, setDatePicker2] = useState(new Date()) const [endDate, setDatePicker2] = useState(new Date())
const [userId, setUser] = useState(null) const [userId, setUser] = useState(null)
const [query, dispatchQ] = useReducer(reducerQ, []) const [query, dispatchQ] = useReducer(reducerQ, [])
const [reportOption, setReport] = useState('1') const [reportOption, setReport] = useState('1')
const [reporList,] = useState(report) const [reporList,] = useState(report)
const [profile, setProfile] = useState('') const [profile, setProfile] = useState('')
const [dataRows, setData] = useState([]) const [dataRows, setData] = useState([])
const [onQueueStatus, setOnQueueProcessStatus] = useState(undefined) const [onQueueStatus, setOnQueueProcessStatus] = useState(undefined)
const [csvFile, setCsvFile] = useState() const [csvFile, setCsvFile] = useState()
const [selectedValue, setSelectedValue] = useState('created')
const [checked, setChecked] = useState(true)
const [queues, setQueues] = useState([])
const [queueId, setQueue] = useState(null)
const [reportTypeList,] = useState(reportOptType)
const [reportType, setReportType] = useState('1')
const [firstLoad, setFirstLoad] = useState(true)
useEffect(() => { useEffect(() => {
dispatch({ type: "RESET" }) dispatch({ type: "RESET" })
@ -291,6 +315,14 @@ const Report = () => {
}, [searchParam, profile]) }, [searchParam, profile])
useEffect(() => {
if (firstLoad) {
setFirstLoad(false)
} else {
}
}, [firstLoad])
useEffect(() => { useEffect(() => {
//setLoading(true); //setLoading(true);
@ -322,32 +354,35 @@ const Report = () => {
useEffect(() => { useEffect(() => {
//setLoading(true); //setLoading(true);
if (firstLoad) return
const delayDebounceFn = setTimeout(() => { const delayDebounceFn = setTimeout(() => {
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, startDate, endDate, pageNumber: pageNumberTickets, createdOrUpdated: selectedValue, queueId }, userQueues: userA.queues })
const { data } = await api.get("/reports/", { params: { userId, startDate, endDate, pageNumber: pageNumberTickets }, userQueues: userA.queues}) let ticketsQueue = data.tickets
let userQueues = userA.queues
let ticketsQueue = data.tickets; let filterQueuesTickets = []
let userQueues = userA.queues; if (userQueues.length > 1) {
let filterQueuesTickets = []; filterQueuesTickets = ticketsQueue.filter(ticket => userQueues.some(queue => queue?.name === ticket?.queue?.name))
if(userQueues.length > 1){ } else if (userQueues.length > 0) {
filterQueuesTickets = ticketsQueue.filter(ticket => userQueues.some(queue => queue?.name === ticket?.queue?.name)); filterQueuesTickets = ticketsQueue.filter(ticket => ticket?.queue?.name === userQueues[0]?.name)
}else if(userQueues.length > 0) {
filterQueuesTickets = ticketsQueue.filter(ticket => ticket?.queue?.name === userQueues[0]?.name);
} }
data.tickets = filterQueuesTickets; data.tickets = filterQueuesTickets
dispatchQ({ type: "LOAD_QUERY", payload: data.tickets }) const tickets = data.tickets.map(ticket => ({
...ticket,
messagesToFilter: ticket.messages.map(message => message.body).join(' '),
}))
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)
setQueues(data.queues)
} }
@ -359,6 +394,20 @@ const Report = () => {
//setLoading(false); //setLoading(false);
} }
else if (reportOption === '3') {
const dataQuery = await api.get("/reports/services/numbers", { params: { startDate, endDate }, })
dispatchQ({ type: "RESET" })
dispatchQ({ type: "LOAD_QUERY", payload: dataQuery?.data?.reportService })
}
else if (reportOption === '4') {
const dataQuery = await api.get("/reports/services/queues", { params: { startDate, endDate }, })
console.log(' dataQuery?.data?.reportService: ', dataQuery?.data?.reportService)
dispatchQ({ type: "RESET" })
dispatchQ({ type: "LOAD_QUERY", payload: dataQuery?.data?.reportService })
}
} catch (err) { } catch (err) {
console.log(err) console.log(err)
@ -370,9 +419,13 @@ const Report = () => {
}, 500) }, 500)
return () => clearTimeout(delayDebounceFn) return () => clearTimeout(delayDebounceFn)
}, [userId, startDate, endDate, reportOption, pageNumberTickets, totalCountTickets]) }, [userId, queueId, checked, startDate, endDate, reportOption, pageNumberTickets, totalCountTickets, selectedValue])
const handleCheckBoxChange = (value) => {
setSelectedValue(value)
}
// Get from child 1 // Get from child 1
const datePicker1Value = (data) => { const datePicker1Value = (data) => {
@ -387,18 +440,35 @@ const Report = () => {
// Get from child 3 // Get from child 3
const textFieldSelectUser = (data) => { const textFieldSelectUser = (data) => {
setQueue(null)
setUser(data) setUser(data)
} }
const textFieldSelectQueue = (data) => {
setUser(0)
setQueue(data)
}
// Get from report option // Get from report option
const reportValue = (data) => { const reportValue = (data) => {
if (data === '2') {
setChecked(true)
}
setReport(data) setReport(data)
}
// Get from report type option
const reportTypeValue = (data) => {
let type = '1'
if (data === '1') type = 'default'
if (data === '2') type = 'synthetic'
if (data === '3') type = 'analytic'
handleCSVMessages(type)
setReportType(data)
} }
useEffect(() => { useEffect(() => {
@ -475,7 +545,7 @@ const Report = () => {
const handleCSVMessages = () => { const handleCSVMessages = (type = 'default') => {
const fetchQueries = async () => { const fetchQueries = async () => {
@ -490,10 +560,12 @@ const Report = () => {
identifier: 'csv' identifier: 'csv'
}, },
query_params: { query_params: {
queueId: queueId,
userId: userId, userId: userId,
startDate: startDate, startDate: startDate,
endDate: endDate endDate: endDate
} },
query_type: type
}) })
const onQueueStatus = querySavedOnQueue.data.queueStatus const onQueueStatus = querySavedOnQueue.data.queueStatus
@ -567,7 +639,7 @@ const Report = () => {
} }
}, [reportOption, startDate, endDate, userId, userA]) }, [reportOption, startDate, endDate, userId, queueId, checked, userA, selectedValue])
useEffect(() => { useEffect(() => {
@ -613,7 +685,10 @@ const Report = () => {
case 'empty': case 'empty':
return ( return (
<> <>
<Button {query && query.length > 0 &&
<ReportModalType currencies={reportTypeList} func={reportTypeValue} reportOption={reportType} />
}
{/* <Button
disabled={query && query.length > 0 ? false : true} disabled={query && query.length > 0 ? false : true}
variant="contained" variant="contained"
color="primary" color="primary"
@ -622,7 +697,8 @@ const Report = () => {
}} }}
> >
{"CSV ALL"} {"CSV ALL"}
</Button>
</Button> */}
</>) </>)
case 'pending' || 'processing': case 'pending' || 'processing':
@ -657,6 +733,10 @@ const Report = () => {
} }
const handleChange = (event) => {
setChecked(event.target.checked)
}
return ( return (
<Can <Can
@ -665,16 +745,72 @@ const Report = () => {
yes={() => ( yes={() => (
<MainContainer> <MainContainer>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)' }}> <Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 2fr) repeat(2, 1fr)', gap: '8px' }}>
<Item><SelectField func={textFieldSelectUser} emptyField={true} header={i18n.t("reports.user")} currencies={users.map((obj) => { <Box sx={{ display: 'flex', flexDirection: 'column', padding: '10px 0', alignItems: 'center', }}>
return { 'value': obj.id, 'label': obj.name }
})} /></Item>
<Item><DatePicker1 func={datePicker1Value} minDate={false} startEmpty={false} title={i18n.t("reports.dateStart")} /></Item> {(reportOption === '1' || reportOption === '2') &&
<Item><DatePicker2 func={datePicker2Value} minDate={false} startEmpty={false} title={i18n.t("reports.dateEnd")} /></Item> <>
{checked ?
<SelectField
func={textFieldSelectUser}
emptyField={true}
header={i18n.t("reports.user")}
currencies={users.map((obj) => {
return { 'value': obj.id, 'label': obj.name }
})} /> :
<SelectField
func={textFieldSelectQueue}
emptyField={true}
header={'Filas'}
currencies={queues.map((obj) => {
return { 'value': obj.id, 'label': obj.name }
})} />
}
</>
}
{reportOption === '1' &&
<div>
<label>
Filas
<Switch
checked={checked}
onChange={handleChange}
inputProps={{ 'aria-label': 'controlled' }}
/>
Usuarios
</label>
</div>
}
<Item sx={{ display: 'grid', gridColumn: '4 / 5', }}>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<DatePicker1 func={datePicker1Value} minDate={false} startEmpty={false} title={i18n.t("reports.dateStart")} />
</Box>
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}><DatePicker2 func={datePicker2Value} minDate={false} startEmpty={false} title={i18n.t("reports.dateEnd")} /></Box>
{reportOption === '1' ?
<Box sx={{ display: 'flex', flexDirection: 'column', padding: '10px 0', }}>
<FormControlLabel
control={<Checkbox checked={selectedValue === 'created'} onChange={() => handleCheckBoxChange('created')} />}
label="Criado"
/>
<FormControlLabel
control={<Checkbox checked={selectedValue === 'updated'} onChange={() => handleCheckBoxChange('updated')} />}
label="Atualizado"
/>
</Box> :
<Box sx={{ display: 'flex', flexDirection: 'column', padding: '10px 0', }}>
</Box>
}
<Box sx={{ display: 'flex', flexDirection: 'column', padding: '10px 0', gap: '8px', width: '100px', alignItems: 'center' }}>
<ReportModal currencies={reporList} func={reportValue} reportOption={reportOption} /> <ReportModal currencies={reporList} func={reportValue} reportOption={reportOption} />
@ -688,7 +824,7 @@ const Report = () => {
} }
</Item> </Box>
</Box> </Box>
@ -701,16 +837,14 @@ const Report = () => {
{reportOption === '1' && {reportOption === '1' &&
<> <>
<MTable data={query} <MTable data={query}
columns={userA.profile !== 'supervisor' ?columnsData:columnsDataSuper} columns={userA.profile !== 'supervisor' ? columnsData : columnsDataSuper}
hasChild={true} hasChild={true}
removeClickRow={false} removeClickRow={false}
handleScroll={handleScroll} handleScroll={handleScroll}
table_title={i18n.t("reports.listTitles.title1_1")} /> table_title={i18n.t("reports.listTitles.title1_1")} />
</> </>
} }
@ -819,6 +953,111 @@ const Report = () => {
/> />
} }
{reportOption === '3' &&
<MaterialTable
title={i18n.t("reports.listTitles.title4_1")}
columns={
[
{ title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, },
{ title: 'Conversas iniciadas', field: 'startedByAgent', },
{ title: 'Conversas recebidas', field: 'startedByClient' },
{ title: `Conversas finalizadas`, field: 'closedChat' },
{ title: `Tempo médio de espera`, field: 'avgChatWaitingTime' },
{ title: 'Aguardando', field: 'pendingChat' }
]
}
data={dataRows}
options={
{
search: true,
selection: false,
paging: false,
padding: 'dense',
sorting: true,
searchFieldStyle: {
width: 300,
},
pageSize: 20,
headerStyle: {
position: "sticky",
top: "0"
},
maxBodyHeight: "400px",
rowStyle: {
fontSize: 14,
}
}}
/>
}
{reportOption === '4' &&
<MaterialTable
title={i18n.t("reports.listTitles.title5_1")}
columns={
[
{ title: 'Unidade', field: 'name', cellStyle: { whiteSpace: 'nowrap' }, },
{
title: 'Fila', field: 'queueName',
cellStyle: (evt, rowData) => {
return {
whiteSpace: 'nowrap',
backgroundColor: rowData?.queueColor || 'inherit',
color: 'white'
}
}
},
{ title: 'Conversas iniciadas', field: 'startedByAgent', },
{ title: 'Conversas recebidas', field: 'startedByClient' },
{ title: `Conversas finalizadas`, field: 'closedChat' },
{ title: `Tempo médio de espera`, field: 'avgChatWaitingTime' },
{ title: 'Aguardando', field: 'pendingChat' }
]
}
data={dataRows}
options={
{
search: true,
selection: false,
paging: false,
padding: 'dense',
sorting: true,
searchFieldStyle: {
width: 300,
},
pageSize: 20,
headerStyle: {
position: "sticky",
top: "0"
},
maxBodyHeight: "400px",
rowStyle: {
fontSize: 14,
}
// rowStyle: rowData => ({
// backgroundColor: rowData.queueColor || 'inherit',
// fontSize: 14
// })
}}
/>
}
</Item> </Item>

View File

@ -241,6 +241,7 @@ const messages = {
}, },
notification: { notification: {
message: "Message from", message: "Message from",
messagePeding: "new ticket in queue",
}, },
tabs: { tabs: {
open: { title: "Inbox" }, open: { title: "Inbox" },

View File

@ -245,6 +245,7 @@ const messages = {
}, },
notification: { notification: {
message: "Mensaje de", message: "Mensaje de",
messagePeding: "Nuevo billete en cola",
}, },
tabs: { tabs: {
open: { title: "Bandeja" }, open: { title: "Bandeja" },

View File

@ -244,6 +244,7 @@ const messages = {
}, },
notification: { notification: {
message: "Mensagem de", message: "Mensagem de",
messagePeding: "Novo ticket na fila",
}, },
tabs: { tabs: {
open: { title: "Inbox" }, open: { title: "Inbox" },
@ -312,7 +313,9 @@ const messages = {
title0_1: "Lembretes/Agendamentos", title0_1: "Lembretes/Agendamentos",
title1_1: "Atendimento por atendentes", title1_1: "Atendimento por atendentes",
title2_1: "Chat do atendimento pelo Whatsapp", title2_1: "Chat do atendimento pelo Whatsapp",
title3_1: "Usuários online/offline" title3_1: "Usuários online/offline",
title4_1: "Relatório de atendimento por números",
title5_1: "Relatório de atendimento por filas"
}, },
listColumns:{ listColumns:{
column0_1: 'Ações', column0_1: 'Ações',

152
package-lock.json generated
View File

@ -2,5 +2,155 @@
"name": "whaticket", "name": "whaticket",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": {} "packages": {
"": {
"dependencies": {
"natural": "^6.10.4"
}
},
"node_modules/afinn-165": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/afinn-165/-/afinn-165-1.0.4.tgz",
"integrity": "sha512-7+Wlx3BImrK0HiG6y3lU4xX7SpBPSSu8T9iguPMlaueRFxjbYwAQrp9lqZUuFikqKbd/en8lVREILvP2J80uJA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/afinn-165-financialmarketnews": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/afinn-165-financialmarketnews/-/afinn-165-financialmarketnews-3.0.0.tgz",
"integrity": "sha512-0g9A1S3ZomFIGDTzZ0t6xmv4AuokBvBmpes8htiyHpH7N4xDmvSQL6UxL/Zcs2ypRb3VwgCscaD8Q3zEawKYhw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/apparatus": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/apparatus/-/apparatus-0.0.10.tgz",
"integrity": "sha512-KLy/ugo33KZA7nugtQ7O0E1c8kQ52N3IvD/XgIh4w/Nr28ypfkwDfA67F1ev4N1m5D+BOk1+b2dEJDfpj/VvZg==",
"dependencies": {
"sylvester": ">= 0.0.8"
},
"engines": {
"node": ">=0.2.6"
}
},
"node_modules/natural": {
"version": "6.10.4",
"resolved": "https://registry.npmjs.org/natural/-/natural-6.10.4.tgz",
"integrity": "sha512-PpqKTyRwNsYrTQ7/2El9X96pt7OCKynwGahl9oIBQEz9d71jxnHQ0KkuWg7xOC7ZvG0gRSkJ56q7Ygr5zfof2w==",
"dependencies": {
"afinn-165": "^1.0.2",
"afinn-165-financialmarketnews": "^3.0.0",
"apparatus": "^0.0.10",
"safe-stable-stringify": "^2.2.0",
"stopwords-iso": "^1.1.0",
"sylvester": "^0.0.12",
"underscore": "^1.9.1",
"wordnet-db": "^3.1.11"
},
"engines": {
"node": ">=0.4.10"
}
},
"node_modules/safe-stable-stringify": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz",
"integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==",
"engines": {
"node": ">=10"
}
},
"node_modules/stopwords-iso": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stopwords-iso/-/stopwords-iso-1.1.0.tgz",
"integrity": "sha512-I6GPS/E0zyieHehMRPQcqkiBMJKGgLta+1hREixhoLPqEA0AlVFiC43dl8uPpmkkeRdDMzYRWFWk5/l9x7nmNg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/sylvester": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/sylvester/-/sylvester-0.0.12.tgz",
"integrity": "sha512-SzRP5LQ6Ts2G5NyAa/jg16s8e3R7rfdFjizy1zeoecYWw+nGL+YA1xZvW/+iJmidBGSdLkuvdwTYEyJEb+EiUw==",
"engines": {
"node": ">=0.2.6"
}
},
"node_modules/underscore": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
},
"node_modules/wordnet-db": {
"version": "3.1.14",
"resolved": "https://registry.npmjs.org/wordnet-db/-/wordnet-db-3.1.14.tgz",
"integrity": "sha512-zVyFsvE+mq9MCmwXUWHIcpfbrHHClZWZiVOzKSxNJruIcFn2RbY55zkhiAMMxM8zCVSmtNiViq8FsAZSFpMYag==",
"engines": {
"node": ">=0.6.0"
}
}
},
"dependencies": {
"afinn-165": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/afinn-165/-/afinn-165-1.0.4.tgz",
"integrity": "sha512-7+Wlx3BImrK0HiG6y3lU4xX7SpBPSSu8T9iguPMlaueRFxjbYwAQrp9lqZUuFikqKbd/en8lVREILvP2J80uJA=="
},
"afinn-165-financialmarketnews": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/afinn-165-financialmarketnews/-/afinn-165-financialmarketnews-3.0.0.tgz",
"integrity": "sha512-0g9A1S3ZomFIGDTzZ0t6xmv4AuokBvBmpes8htiyHpH7N4xDmvSQL6UxL/Zcs2ypRb3VwgCscaD8Q3zEawKYhw=="
},
"apparatus": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/apparatus/-/apparatus-0.0.10.tgz",
"integrity": "sha512-KLy/ugo33KZA7nugtQ7O0E1c8kQ52N3IvD/XgIh4w/Nr28ypfkwDfA67F1ev4N1m5D+BOk1+b2dEJDfpj/VvZg==",
"requires": {
"sylvester": ">= 0.0.8"
}
},
"natural": {
"version": "6.10.4",
"resolved": "https://registry.npmjs.org/natural/-/natural-6.10.4.tgz",
"integrity": "sha512-PpqKTyRwNsYrTQ7/2El9X96pt7OCKynwGahl9oIBQEz9d71jxnHQ0KkuWg7xOC7ZvG0gRSkJ56q7Ygr5zfof2w==",
"requires": {
"afinn-165": "^1.0.2",
"afinn-165-financialmarketnews": "^3.0.0",
"apparatus": "^0.0.10",
"safe-stable-stringify": "^2.2.0",
"stopwords-iso": "^1.1.0",
"sylvester": "^0.0.12",
"underscore": "^1.9.1",
"wordnet-db": "^3.1.11"
}
},
"safe-stable-stringify": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz",
"integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g=="
},
"stopwords-iso": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stopwords-iso/-/stopwords-iso-1.1.0.tgz",
"integrity": "sha512-I6GPS/E0zyieHehMRPQcqkiBMJKGgLta+1hREixhoLPqEA0AlVFiC43dl8uPpmkkeRdDMzYRWFWk5/l9x7nmNg=="
},
"sylvester": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/sylvester/-/sylvester-0.0.12.tgz",
"integrity": "sha512-SzRP5LQ6Ts2G5NyAa/jg16s8e3R7rfdFjizy1zeoecYWw+nGL+YA1xZvW/+iJmidBGSdLkuvdwTYEyJEb+EiUw=="
},
"underscore": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
},
"wordnet-db": {
"version": "3.1.14",
"resolved": "https://registry.npmjs.org/wordnet-db/-/wordnet-db-3.1.14.tgz",
"integrity": "sha512-zVyFsvE+mq9MCmwXUWHIcpfbrHHClZWZiVOzKSxNJruIcFn2RbY55zkhiAMMxM8zCVSmtNiViq8FsAZSFpMYag=="
}
}
} }

5
package.json 100644
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"natural": "^6.10.4"
}
}