Modulo campanha basico finalizado
parent
ff0d2c3630
commit
75eb24fe62
|
@ -8,18 +8,19 @@ import ShowContactService from "../services/ContactServices/ShowContactService";
|
||||||
import UpdateContactService from "../services/ContactServices/UpdateContactService";
|
import UpdateContactService from "../services/ContactServices/UpdateContactService";
|
||||||
import DeleteContactService from "../services/ContactServices/DeleteContactService";
|
import DeleteContactService from "../services/ContactServices/DeleteContactService";
|
||||||
|
|
||||||
import CheckContactNumber from "../services/WbotServices/CheckNumber"
|
import CheckContactNumber from "../services/WbotServices/CheckNumber";
|
||||||
import CheckIsValidContact from "../services/WbotServices/CheckIsValidContact";
|
import CheckIsValidContact from "../services/WbotServices/CheckIsValidContact";
|
||||||
import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl";
|
import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl";
|
||||||
import AppError from "../errors/AppError";
|
import AppError from "../errors/AppError";
|
||||||
|
|
||||||
|
import {
|
||||||
import { searchContactCache, insertContactsCache, escapeCharCache } from '../helpers/ContactsCache'
|
searchContactCache,
|
||||||
|
insertContactsCache,
|
||||||
|
escapeCharCache
|
||||||
|
} from "../helpers/ContactsCache";
|
||||||
|
|
||||||
import { off } from "process";
|
import { off } from "process";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type IndexQuery = {
|
type IndexQuery = {
|
||||||
searchParam: string;
|
searchParam: string;
|
||||||
pageNumber: string;
|
pageNumber: string;
|
||||||
|
@ -39,42 +40,46 @@ interface ContactData {
|
||||||
export const index = async (req: Request, res: Response): Promise<Response> => {
|
export const index = async (req: Request, res: Response): Promise<Response> => {
|
||||||
let { searchParam, pageNumber } = req.query as IndexQuery;
|
let { searchParam, pageNumber } = req.query as IndexQuery;
|
||||||
|
|
||||||
console.log('PAGE NUMBER CONTACT: ', pageNumber)
|
console.log("PAGE NUMBER CONTACT: ", pageNumber);
|
||||||
|
|
||||||
if (pageNumber === undefined || pageNumber.trim().length == 0) {
|
if (pageNumber === undefined || pageNumber.trim().length == 0) {
|
||||||
pageNumber = '1'
|
pageNumber = "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
// TEST DEL
|
// TEST DEL
|
||||||
if (searchParam && searchParam.trim().length > 0 && process.env.CACHE) {
|
if (searchParam && searchParam.trim().length > 0 && process.env.CACHE) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const offset = 20 * (+pageNumber - 1);
|
const offset = 20 * (+pageNumber - 1);
|
||||||
|
|
||||||
searchParam = searchParam.replace(/\s+/g, ' ').trim().toLowerCase();
|
searchParam = searchParam.replace(/\s+/g, " ").trim().toLowerCase();
|
||||||
|
|
||||||
const data = await searchContactCache(searchParam, offset, 20)
|
const data = await searchContactCache(searchParam, offset, 20);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
|
console.log("QUERY CONTACTS FROM CACHE SEARCH PARAM: ", searchParam);
|
||||||
|
|
||||||
console.log('QUERY CONTACTS FROM CACHE SEARCH PARAM: ', searchParam)
|
console.log("QUERY CONTACTS FROM CACHE QUERY LENGTH: ", data.length);
|
||||||
|
|
||||||
console.log('QUERY CONTACTS FROM CACHE QUERY LENGTH: ', data.length)
|
return res.json({
|
||||||
|
contacts: data,
|
||||||
return res.json({ contacts: data, count: data.length, hasMore: data.length > 0 ? true : false });
|
count: data.length,
|
||||||
|
hasMore: data.length > 0 ? true : false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('There was an error on search ContactController.ts search cache: ', error)
|
console.log(
|
||||||
|
"There was an error on search ContactController.ts search cache: ",
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('QUERY CONTACTS FROM DATABASE SEARCH PARAM: ', searchParam)
|
console.log("QUERY CONTACTS FROM DATABASE SEARCH PARAM: ", searchParam);
|
||||||
|
|
||||||
const { contacts, count, hasMore } = await ListContactsService({ searchParam, pageNumber });
|
const { contacts, count, hasMore } = await ListContactsService({
|
||||||
|
searchParam,
|
||||||
|
pageNumber
|
||||||
|
});
|
||||||
|
|
||||||
return res.json({ contacts, count, hasMore });
|
return res.json({ contacts, count, hasMore });
|
||||||
};
|
};
|
||||||
|
@ -99,33 +104,32 @@ 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)
|
// 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("xxxxxxxxxxx profilePicUrl: ", profilePicUrl);
|
||||||
|
|
||||||
|
|
||||||
// console.log(`newContact.name: ${newContact.name}\n
|
// console.log(`newContact.name: ${newContact.name}\n
|
||||||
// newContact.number: ${newContact.number}\n
|
// newContact.number: ${newContact.number}\n
|
||||||
// newContact.email: ${newContact.email}\n
|
// newContact.email: ${newContact.email}\n
|
||||||
// newContact.extraInfo: ${newContact.extraInfo}`)
|
// 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;
|
||||||
let extraInfo = newContact.extraInfo
|
let extraInfo = newContact.extraInfo;
|
||||||
|
|
||||||
const contact = await CreateContactService({
|
const contact = await CreateContactService({
|
||||||
name,
|
name,
|
||||||
number,
|
number,
|
||||||
email,
|
email,
|
||||||
profilePicUrl: profilePicUrl,
|
profilePicUrl: profilePicUrl,
|
||||||
extraInfo,
|
extraInfo
|
||||||
});
|
});
|
||||||
|
|
||||||
const io = getIO();
|
const io = getIO();
|
||||||
|
@ -154,8 +158,8 @@ export const update = async (
|
||||||
const schema = Yup.object().shape({
|
const schema = Yup.object().shape({
|
||||||
name: Yup.string(),
|
name: Yup.string(),
|
||||||
number: Yup.string()
|
number: Yup.string()
|
||||||
.matches(/^\d+$/,"Invalid number format. Only numbers is allowed.")
|
.matches(/^\d+$/, "Invalid number format. Only numbers is allowed.")
|
||||||
.matches(/^55\d+$/, "The number must start with 55.")
|
.matches(/^55\d+$/, "The number must start with 55.")
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -196,13 +200,20 @@ export const remove = async (
|
||||||
return res.status(200).json({ message: "Contact deleted" });
|
return res.status(200).json({ message: "Contact deleted" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const contacsBulkInsertOnQueue = async (
|
||||||
|
req: Request,
|
||||||
export const contacsBulkInsertOnQueue = async (req: Request, res: Response): Promise<Response> => {
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
// console.log('THE BODY: ', req.body)
|
// console.log('THE BODY: ', req.body)
|
||||||
|
|
||||||
const { adminId, identifier, queueStatus, file, contacts_inserted } = req.body
|
const {
|
||||||
|
adminId,
|
||||||
|
identifier,
|
||||||
|
queueStatus,
|
||||||
|
file,
|
||||||
|
contacts_inserted,
|
||||||
|
campaign
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
const io = getIO();
|
const io = getIO();
|
||||||
io.emit("contactsBulkInsertOnQueueStatus", {
|
io.emit("contactsBulkInsertOnQueueStatus", {
|
||||||
|
@ -211,17 +222,14 @@ export const contacsBulkInsertOnQueue = async (req: Request, res: Response): Pro
|
||||||
adminId: adminId,
|
adminId: adminId,
|
||||||
identifier: identifier,
|
identifier: identifier,
|
||||||
queueStatus: queueStatus,
|
queueStatus: queueStatus,
|
||||||
file: file
|
file: file,
|
||||||
|
campaign
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (process.env.CACHE && contacts_inserted) {
|
if (process.env.CACHE && contacts_inserted) {
|
||||||
|
await insertContactsCache(contacts_inserted);
|
||||||
await insertContactsCache(contacts_inserted)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json({ message: 'ok' })
|
return res.status(200).json({ message: "ok" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,220 +3,204 @@ import { Server } from "http";
|
||||||
import AppError from "../errors/AppError";
|
import AppError from "../errors/AppError";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import ListUserParamiterService from "../services/UserServices/ListUserParamiterService";
|
import ListUserParamiterService from "../services/UserServices/ListUserParamiterService";
|
||||||
import { addHours, addMinutes, addSeconds, intervalToDuration, add } from "date-fns";
|
import {
|
||||||
|
addHours,
|
||||||
|
addMinutes,
|
||||||
|
addSeconds,
|
||||||
|
intervalToDuration,
|
||||||
|
add
|
||||||
|
} from "date-fns";
|
||||||
|
|
||||||
let io: SocketIO;
|
let io: SocketIO;
|
||||||
|
|
||||||
//test del
|
//test del
|
||||||
import createOrUpdateOnlineUserService from "../services/UserServices/CreateOrUpdateOnlineUserService";
|
import createOrUpdateOnlineUserService from "../services/UserServices/CreateOrUpdateOnlineUserService";
|
||||||
import { splitDateTime } from "../helpers/SplitDateTime";
|
import { splitDateTime } from "../helpers/SplitDateTime";
|
||||||
import format from 'date-fns/format';
|
import format from "date-fns/format";
|
||||||
import ptBR from 'date-fns/locale/pt-BR';
|
import ptBR from "date-fns/locale/pt-BR";
|
||||||
import ListUserOnlineOffline from "../services/UserServices/ListUsersOnlineOfflineService";
|
import ListUserOnlineOffline from "../services/UserServices/ListUsersOnlineOfflineService";
|
||||||
import { handleMessage, handleMsgAck } from "../services/WbotServices/wbotMessageListener";
|
import {
|
||||||
|
handleMessage,
|
||||||
|
handleMsgAck
|
||||||
|
} from "../services/WbotServices/wbotMessageListener";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import Whatsapp from "../models/Whatsapp";
|
import Whatsapp from "../models/Whatsapp";
|
||||||
|
|
||||||
let count: number = 0
|
let count: number = 0;
|
||||||
let listOnline: any[] = []
|
let listOnline: any[] = [];
|
||||||
let listOnlineAux: any[] = []
|
let listOnlineAux: any[] = [];
|
||||||
let countOnline: number = 0
|
let countOnline: number = 0;
|
||||||
let obj: any = { listOnline: [], uuid: null, listOnlineAux: [] }
|
let obj: any = { listOnline: [], uuid: null, listOnlineAux: [] };
|
||||||
|
|
||||||
|
let lstOnline: any[] = [];
|
||||||
|
let lstOnlineAux: any[] = [];
|
||||||
|
let lstTry: any[] = [];
|
||||||
|
|
||||||
|
let dateTime = splitDateTime(
|
||||||
|
new Date(format(new Date(), "yyyy-MM-dd HH:mm:ss", { locale: ptBR }))
|
||||||
let lstOnline: any[] = []
|
);
|
||||||
let lstOnlineAux: any[] = []
|
|
||||||
let lstTry: any[] = []
|
|
||||||
|
|
||||||
let dateTime = splitDateTime(new Date(format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ptBR })))
|
|
||||||
|
|
||||||
|
|
||||||
export const initIO = (httpServer: Server): SocketIO => {
|
export const initIO = (httpServer: Server): SocketIO => {
|
||||||
io = new SocketIO(httpServer, {
|
io = new SocketIO(httpServer, {
|
||||||
cors: {
|
cors: {
|
||||||
origin: process.env.FRONTEND_URL
|
origin: process.env.FRONTEND_URL
|
||||||
},
|
},
|
||||||
maxHttpBufferSize: 1e8
|
maxHttpBufferSize: 1e8
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
io.on("connection", socket => {
|
io.on("connection", socket => {
|
||||||
logger.info("Client Connected");
|
logger.info("Client Connected");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
socket.on("joinWhatsSession", (whatsappId: string) => {
|
socket.on("joinWhatsSession", (whatsappId: string) => {
|
||||||
logger.info(`A client joined a joinWhatsSession channel: ${whatsappId}`);
|
logger.info(`A client joined a joinWhatsSession channel: ${whatsappId}`);
|
||||||
socket.join(`session_${whatsappId}`);
|
socket.join(`session_${whatsappId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("message_from_client", () => {
|
socket.on("message_from_client", () => {
|
||||||
|
socket.emit("message_from_server", "Sent an event from the server!");
|
||||||
socket.emit('message_from_server', 'Sent an event from the server!');
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on("message_create", async (data: any) => {
|
|
||||||
|
|
||||||
handleMessage(data.msg, data);
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("message_create", async (data: any) => {
|
||||||
socket.on("media_uploaded", async (data: any) => {
|
|
||||||
|
|
||||||
handleMessage(data.msg, data);
|
handleMessage(data.msg, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("media_uploaded", async (data: any) => {
|
||||||
|
handleMessage(data.msg, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("message_ack", async (data: any) => {
|
socket.on("message_ack", async (data: any) => {
|
||||||
|
handleMsgAck(data.id, data.ack);
|
||||||
handleMsgAck(data.id, data.ack)
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("campaign_message_sent", async (data: any) => {
|
||||||
|
console.log("campaign_message_sent: ", data);
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
|
||||||
|
let campaign = {};
|
||||||
|
|
||||||
|
if (data?.read) {
|
||||||
|
campaign = {
|
||||||
|
id: data.id,
|
||||||
|
read: data.read
|
||||||
|
};
|
||||||
|
} else if (data?.sent) {
|
||||||
|
campaign = {
|
||||||
|
id: data.id,
|
||||||
|
sent: data.sent
|
||||||
|
};
|
||||||
|
} else if (data?.status) {
|
||||||
|
campaign = {
|
||||||
|
id: data.id,
|
||||||
|
status: data.status
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
io.emit("campaign", {
|
||||||
|
action: "update",
|
||||||
|
campaign
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("online", (userId: any) => {
|
socket.on("online", (userId: any) => {
|
||||||
|
|
||||||
// console.log('userId: ', userId)
|
// console.log('userId: ', userId)
|
||||||
|
|
||||||
obj.uuid = uuidv4()
|
obj.uuid = uuidv4();
|
||||||
|
|
||||||
if (userId.logoutUserId) {
|
if (userId.logoutUserId) {
|
||||||
|
let index = lstOnline.findIndex(
|
||||||
let index = lstOnline.findIndex((x: any) => x.id == userId.logoutUserId)
|
(x: any) => x.id == userId.logoutUserId
|
||||||
|
);
|
||||||
|
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
|
lstOnline.splice(index, 1);
|
||||||
lstOnline.splice(index, 1)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
index = lstOnlineAux.findIndex((x: any) => x.id == userId.logoutUserId);
|
||||||
index = lstOnlineAux.findIndex((x: any) => x.id == userId.logoutUserId)
|
|
||||||
|
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
|
lstOnlineAux.splice(index, 1);
|
||||||
lstOnlineAux.splice(index, 1)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lstOnline.length == 0) {
|
if (lstOnline.length == 0) {
|
||||||
|
const index = listOnlineAux.findIndex((e: any) => e.id == userId);
|
||||||
const index = listOnlineAux.findIndex((e: any) => e.id == userId)
|
|
||||||
|
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
listOnlineAux.push({ 'id': userId })
|
listOnlineAux.push({ id: userId });
|
||||||
}
|
} else {
|
||||||
else {
|
return;
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lstOnline.push({ 'id': userId, 'status': 'online', 'try': 0 })
|
lstOnline.push({ id: userId, status: "online", try: 0 });
|
||||||
|
|
||||||
lstOnlineAux.push({ 'id': userId })
|
lstOnlineAux.push({ id: userId });
|
||||||
console.log(' 1 PUSHED NEW USER ID 1: ', userId)
|
console.log(" 1 PUSHED NEW USER ID 1: ", userId);
|
||||||
|
|
||||||
obj.listOnline = lstOnline
|
obj.listOnline = lstOnline;
|
||||||
|
} else {
|
||||||
}
|
const indexAux = lstOnlineAux.findIndex((e: any) => e.id == userId);
|
||||||
else {
|
|
||||||
|
|
||||||
const indexAux = lstOnlineAux.findIndex((e: any) => e.id == userId)
|
|
||||||
|
|
||||||
if (indexAux == -1) {
|
if (indexAux == -1) {
|
||||||
lstOnlineAux.push({ 'id': userId })
|
lstOnlineAux.push({ id: userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = lstOnline.findIndex((e: any) => e.id == userId)
|
const index = lstOnline.findIndex((e: any) => e.id == userId);
|
||||||
|
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
|
lstOnline.push({ id: userId, status: "online", try: 0 });
|
||||||
|
|
||||||
|
console.log(" 2 PUSHED NEW USER ID: ", userId);
|
||||||
|
|
||||||
lstOnline.push({ 'id': userId, 'status': 'online', 'try': 0 })
|
obj.listOnline = lstOnline;
|
||||||
|
} else {
|
||||||
|
if (countOnline > lstOnline.length - 1) {
|
||||||
console.log(' 2 PUSHED NEW USER ID: ', userId)
|
|
||||||
|
|
||||||
obj.listOnline = lstOnline
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
if (countOnline > (lstOnline.length - 1)) {
|
|
||||||
|
|
||||||
lstOnline.forEach((x: any) => {
|
lstOnline.forEach((x: any) => {
|
||||||
if (lstOnlineAux.map((e: any) => e.id).includes(x.id)) {
|
if (lstOnlineAux.map((e: any) => e.id).includes(x.id)) {
|
||||||
x.try = 0
|
x.try = 0;
|
||||||
x.status = 'online'
|
x.status = "online";
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
var difference = lstOnline.filter((x: any) => !lstOnlineAux.map((e: any) => e.id).includes(x.id));
|
|
||||||
|
|
||||||
|
var difference = lstOnline.filter(
|
||||||
|
(x: any) => !lstOnlineAux.map((e: any) => e.id).includes(x.id)
|
||||||
|
);
|
||||||
|
|
||||||
if (difference.length > 0) {
|
if (difference.length > 0) {
|
||||||
|
difference.forEach(e => {
|
||||||
difference.forEach((e) => {
|
e.try += 1;
|
||||||
|
|
||||||
e.try += 1
|
|
||||||
|
|
||||||
if (e.try > 1) {
|
if (e.try > 1) {
|
||||||
e.status = 'waiting...'
|
e.status = "waiting...";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.try > 3) {
|
if (e.try > 3) {
|
||||||
|
const index = lstOnline.findIndex((x: any) => x.id == e.id);
|
||||||
const index = lstOnline.findIndex((x: any) => x.id == e.id)
|
|
||||||
|
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
|
lstOnline.splice(index, 1);
|
||||||
lstOnline.splice(index, 1)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
obj.listOnline = lstOnline
|
obj.listOnline = lstOnline;
|
||||||
obj.listOnlineAux = lstOnlineAux
|
obj.listOnlineAux = lstOnlineAux;
|
||||||
|
|
||||||
|
lstOnlineAux = [];
|
||||||
|
listOnlineAux = [];
|
||||||
|
|
||||||
lstOnlineAux = []
|
countOnline = -1;
|
||||||
listOnlineAux = []
|
|
||||||
|
|
||||||
countOnline = -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
countOnline++;
|
||||||
countOnline++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.ob = obj
|
exports.ob = obj;
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("joinChatBox", (ticketId: string) => {
|
socket.on("joinChatBox", (ticketId: string) => {
|
||||||
|
@ -239,49 +223,51 @@ export const initIO = (httpServer: Server): SocketIO => {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("disconnecting", async () => {
|
socket.on("disconnecting", async () => {
|
||||||
console.log('socket.rooms: ', socket.rooms); // the Set contains at least the socket ID
|
console.log("socket.rooms: ", socket.rooms); // the Set contains at least the socket ID
|
||||||
|
|
||||||
let rooms = socket.rooms
|
let rooms = socket.rooms;
|
||||||
|
|
||||||
console.log('rooms: ', rooms, ' | rooms.size: ', rooms.size)
|
console.log("rooms: ", rooms, " | rooms.size: ", rooms.size);
|
||||||
|
|
||||||
if(rooms && rooms.size==1) return
|
if (rooms && rooms.size == 1) return;
|
||||||
if(rooms && rooms.size==2 && !([...rooms][1].startsWith('session_'))) return
|
if (rooms && rooms.size == 2 && ![...rooms][1].startsWith("session_"))
|
||||||
|
return;
|
||||||
|
|
||||||
let whatsappIds: any = await Whatsapp.findAll({ attributes: ['id'], raw: true })
|
let whatsappIds: any = await Whatsapp.findAll({
|
||||||
|
attributes: ["id"],
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
|
||||||
if (whatsappIds && whatsappIds.length > 0) {
|
if (whatsappIds && whatsappIds.length > 0) {
|
||||||
|
whatsappIds = whatsappIds.map((e: any) => `${e.id}`);
|
||||||
|
|
||||||
whatsappIds = whatsappIds.map((e: any) => `${e.id}`)
|
console.log(
|
||||||
|
"whatsappIds whatsappIds whatsappIds whatsappIds whatsappIds: ",
|
||||||
|
whatsappIds
|
||||||
|
);
|
||||||
|
|
||||||
console.log('whatsappIds whatsappIds whatsappIds whatsappIds whatsappIds: ',whatsappIds)
|
if (
|
||||||
|
rooms &&
|
||||||
|
rooms.size == 2 &&
|
||||||
|
[...rooms][1].startsWith("session_") &&
|
||||||
|
whatsappIds.includes([...rooms][1].replace("session_", ""))
|
||||||
|
) {
|
||||||
|
console.log([...rooms][1]);
|
||||||
|
|
||||||
if (rooms && rooms.size == 2 &&
|
let whatsappId = [...rooms][1].replace("session_", "");
|
||||||
[...rooms][1].startsWith('session_') &&
|
|
||||||
whatsappIds.includes([...rooms][1].replace('session_', ''))) {
|
|
||||||
|
|
||||||
console.log([...rooms][1])
|
const whatsapp = await Whatsapp.findByPk(whatsappId, {});
|
||||||
|
|
||||||
let whatsappId = [...rooms][1].replace('session_', '')
|
|
||||||
|
|
||||||
const whatsapp = await Whatsapp.findByPk(whatsappId, {})
|
|
||||||
|
|
||||||
if (whatsapp) {
|
if (whatsapp) {
|
||||||
|
await whatsapp.update({ status: "OPENING" });
|
||||||
await whatsapp.update({ status: 'OPENING' });
|
|
||||||
|
|
||||||
io.emit("whatsappSession", {
|
io.emit("whatsappSession", {
|
||||||
action: "update",
|
action: "update",
|
||||||
session: whatsapp
|
session: whatsapp
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return io;
|
return io;
|
||||||
|
@ -294,12 +280,9 @@ export const getIO = (): SocketIO => {
|
||||||
return io;
|
return io;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function writeFileAsync(arg0: any, data: any, arg2: string) {
|
function writeFileAsync(arg0: any, data: any, arg2: string) {
|
||||||
throw new Error("Function not implemented.");
|
throw new Error("Function not implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// exports.listOnlineUsers = listUserId
|
// exports.listOnlineUsers = listUserId
|
||||||
// exports.listUserId
|
// exports.listUserId
|
||||||
|
|
||||||
|
|
|
@ -9,14 +9,14 @@ import { green } from '@material-ui/core/colors'
|
||||||
import { AuthContext } from '../../context/Auth/AuthContext'
|
import { AuthContext } from '../../context/Auth/AuthContext'
|
||||||
import { Can } from '../../components/Can'
|
import { Can } from '../../components/Can'
|
||||||
|
|
||||||
import apiBroker from '../../services/apiBroker'
|
import apiBroker from '../../services/apiBroker'
|
||||||
|
|
||||||
import Select from "@material-ui/core/Select"
|
import Select from "@material-ui/core/Select"
|
||||||
import MenuItem from "@material-ui/core/MenuItem";
|
import MenuItem from "@material-ui/core/MenuItem"
|
||||||
|
|
||||||
import SelectField from "../../components/Report/SelectField"
|
import SelectField from "../../components/Report/SelectField"
|
||||||
|
|
||||||
import { WhatsAppsContext } from "../../context/WhatsApp/WhatsAppsContext";
|
import { WhatsAppsContext } from "../../context/WhatsApp/WhatsAppsContext"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
@ -74,13 +74,14 @@ const CampaignModal = ({ open, onClose, campaignId, dispatch }) => {
|
||||||
const initialState = {
|
const initialState = {
|
||||||
name: '',
|
name: '',
|
||||||
message: '',
|
message: '',
|
||||||
|
status: 'pending',
|
||||||
whatsapp_sender: '',
|
whatsapp_sender: '',
|
||||||
csv_original_file_name: '',
|
csv_original_file_name: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user } = useContext(AuthContext)
|
const { user } = useContext(AuthContext)
|
||||||
|
|
||||||
const { whatsApps } = useContext(WhatsAppsContext);
|
const { whatsApps } = useContext(WhatsAppsContext)
|
||||||
|
|
||||||
console.log('------------> whatsApps: ', whatsApps)
|
console.log('------------> whatsApps: ', whatsApps)
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ const CampaignModal = ({ open, onClose, campaignId, dispatch }) => {
|
||||||
// const [selectedQueueIds, setSelectedQueueIds] = useState([])
|
// const [selectedQueueIds, setSelectedQueueIds] = useState([])
|
||||||
const [file, setFile] = useState()
|
const [file, setFile] = useState()
|
||||||
|
|
||||||
const [selectedNumber, setSelectedNumber] = useState('');
|
const [selectedNumber, setSelectedNumber] = useState('')
|
||||||
|
|
||||||
const [itemHover, setItemHover] = useState(-1)
|
const [itemHover, setItemHover] = useState(-1)
|
||||||
|
|
||||||
|
@ -97,7 +98,7 @@ const CampaignModal = ({ open, onClose, campaignId, dispatch }) => {
|
||||||
}, [selectedNumber])
|
}, [selectedNumber])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSession = async () => {
|
const fetchSession = async () => {
|
||||||
if (!campaignId) return
|
if (!campaignId) return
|
||||||
|
@ -175,6 +176,7 @@ const CampaignModal = ({ open, onClose, campaignId, dispatch }) => {
|
||||||
onClose()
|
onClose()
|
||||||
setCampaign(initialState)
|
setCampaign(initialState)
|
||||||
setSelectedNumber('')
|
setSelectedNumber('')
|
||||||
|
setFile(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleChange(event) {
|
async function handleChange(event) {
|
||||||
|
@ -258,7 +260,7 @@ const CampaignModal = ({ open, onClose, campaignId, dispatch }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={classes.multFieldLine}>
|
<div className={classes.multFieldLine}>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { i18n } from "../translate/i18n";
|
import { i18n } from "../translate/i18n";
|
||||||
|
|
||||||
const toastError = err => {
|
const toastError = err => {
|
||||||
const errorMsg = err.response?.data?.message || err.response.data.error;
|
const errorMsg = err.response?.data?.message || err?.response?.data?.error || `${err?.message}`;
|
||||||
if (errorMsg) {
|
if (errorMsg) {
|
||||||
if (i18n.exists(`backendErrors.${errorMsg}`)) {
|
if (i18n.exists(`backendErrors.${errorMsg}`)) {
|
||||||
toast.error(i18n.t(`backendErrors.${errorMsg}`), {
|
toast.error(i18n.t(`backendErrors.${errorMsg}`), {
|
||||||
|
|
|
@ -69,8 +69,14 @@ const reducer = (state, action) => {
|
||||||
const campaignIndex = state.findIndex((c) => c.id === campaign.id)
|
const campaignIndex = state.findIndex((c) => c.id === campaign.id)
|
||||||
|
|
||||||
if (campaignIndex !== -1) {
|
if (campaignIndex !== -1) {
|
||||||
state[campaignIndex] = campaign
|
|
||||||
|
state[campaignIndex] = { ...state[campaignIndex], ...campaign }
|
||||||
|
|
||||||
return [...state]
|
return [...state]
|
||||||
|
|
||||||
|
// state[campaignIndex] = campaign
|
||||||
|
|
||||||
|
// return [...state]
|
||||||
} else {
|
} else {
|
||||||
return [campaign, ...state]
|
return [campaign, ...state]
|
||||||
}
|
}
|
||||||
|
@ -174,7 +180,7 @@ const Campaign = () => {
|
||||||
title: '',
|
title: '',
|
||||||
message: '',
|
message: '',
|
||||||
campaignId: '',
|
campaignId: '',
|
||||||
csv_original_file_name:'',
|
csv_original_file_name: '',
|
||||||
open: false,
|
open: false,
|
||||||
}
|
}
|
||||||
const [confirmModalInfo, setConfirmModalInfo] = useState(
|
const [confirmModalInfo, setConfirmModalInfo] = useState(
|
||||||
|
@ -203,6 +209,8 @@ const Campaign = () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('data.campaign : ', data.campaign)
|
||||||
|
|
||||||
dispatch({ type: "LOAD_CAMPAIGNS", payload: data.campaign })
|
dispatch({ type: "LOAD_CAMPAIGNS", payload: data.campaign })
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
|
@ -284,6 +292,40 @@ const Campaign = () => {
|
||||||
setQrModalOpen(true)
|
setQrModalOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleStart = async (campaign) => {
|
||||||
|
console.log('start')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await apiBroker.post(`/campaign/start/${campaign.id}`)
|
||||||
|
|
||||||
|
console.log('==============> data.campaign: ', data.campaign)
|
||||||
|
|
||||||
|
dispatch({ type: "UPDATE_CAMPAIGNS", payload: data.campaign })
|
||||||
|
|
||||||
|
toast.success('Campanha iniciada com sucesso')
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
toastError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStop = async (campaign) => {
|
||||||
|
console.log('stop')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await apiBroker.post(`/campaign/stop/${campaign.id}`)
|
||||||
|
|
||||||
|
console.log('==============> data.campaign: ', data.campaign)
|
||||||
|
|
||||||
|
dispatch({ type: "UPDATE_CAMPAIGNS", payload: data.campaign })
|
||||||
|
|
||||||
|
toast.success('Campanha parada com sucesso')
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
toastError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleCloseQrModal = useCallback(() => {
|
const handleCloseQrModal = useCallback(() => {
|
||||||
setSelectedCampaign(null)
|
setSelectedCampaign(null)
|
||||||
setQrModalOpen(false)
|
setQrModalOpen(false)
|
||||||
|
@ -325,7 +367,7 @@ const Campaign = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (confirmModalInfo.action === 'delete') {
|
if (confirmModalInfo.action === 'delete') {
|
||||||
try {
|
try {
|
||||||
await apiBroker.delete(`/campaign/${confirmModalInfo.campaignId}`, {
|
await apiBroker.delete(`/campaign/${confirmModalInfo.campaignId}`, {
|
||||||
params: {
|
params: {
|
||||||
adminId: user.id,
|
adminId: user.id,
|
||||||
|
@ -334,7 +376,7 @@ const Campaign = () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
dispatch({ type: "DELETE_CAMPAIGN", payload: confirmModalInfo.campaignId })
|
dispatch({ type: "DELETE_CAMPAIGN", payload: confirmModalInfo.campaignId })
|
||||||
|
|
||||||
toast.success('Campanha deletada com sucesso')
|
toast.success('Campanha deletada com sucesso')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -347,63 +389,28 @@ const Campaign = () => {
|
||||||
|
|
||||||
const renderActionButtons = (campaign) => {
|
const renderActionButtons = (campaign) => {
|
||||||
return (
|
return (
|
||||||
<Can
|
<>
|
||||||
role={user.profile}
|
{campaign.status === 'stopped' && (
|
||||||
perform="connection-button:show"
|
<Button
|
||||||
yes={() => (
|
size="small"
|
||||||
<>
|
variant="contained"
|
||||||
{campaign.status === 'qrcode' && (
|
color="primary"
|
||||||
<Button
|
onClick={() => handleStart(campaign)}
|
||||||
size="small"
|
>
|
||||||
variant="contained"
|
Start
|
||||||
color="primary"
|
</Button>
|
||||||
onClick={() => handleOpenQrModal(campaign)}
|
|
||||||
>
|
|
||||||
{i18n.t('connections.buttons.qrcode')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{campaign.status === 'DISCONNECTED' && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => handleStartWhatsAppSession(campaign.id)}
|
|
||||||
>
|
|
||||||
{i18n.t('connections.buttons.tryAgain')}
|
|
||||||
</Button>{' '}
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
color="secondary"
|
|
||||||
onClick={() => handleRequestNewQrCode(campaign.id)}
|
|
||||||
>
|
|
||||||
{i18n.t('connections.buttons.newQr')}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{(campaign.status === 'CONNECTED' ||
|
|
||||||
campaign.status === 'PAIRING' ||
|
|
||||||
campaign.status === 'TIMEOUT') && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
color="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
handleOpenConfirmationModal('disconnect', campaign.id)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{i18n.t('connections.buttons.disconnect')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{campaign.status === 'OPENING' && (
|
|
||||||
<Button size="small" variant="outlined" disabled color="default">
|
|
||||||
{i18n.t('connections.buttons.connecting')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
/>
|
{campaign.status === 'running' && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => handleStop(campaign)}
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -483,6 +490,31 @@ const Campaign = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const socket = openSocket(process.env.REACT_APP_BACKEND_URL)
|
const socket = openSocket(process.env.REACT_APP_BACKEND_URL)
|
||||||
|
|
||||||
|
|
||||||
|
socket.on("contactsBulkInsertOnQueueStatus", (data) => {
|
||||||
|
if (data.action === 'update') {
|
||||||
|
|
||||||
|
if (String(data.insertOnQueue.adminId) === String(user.id)) {
|
||||||
|
|
||||||
|
if (data?.insertOnQueue?.campaign?.status === "stopped") {
|
||||||
|
|
||||||
|
dispatch({ type: "UPDATE_CAMPAIGNS", payload: data.insertOnQueue.campaign })
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('campaign', (data) => {
|
||||||
|
|
||||||
|
console.log('------------> CAMPAIGN: ', data)
|
||||||
|
|
||||||
|
if (data.action === 'update') {
|
||||||
|
dispatch({ type: "UPDATE_CAMPAIGNS", payload: data.campaign })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
socket.on('diskSpaceMonit', (data) => {
|
socket.on('diskSpaceMonit', (data) => {
|
||||||
if (data.action === 'update') {
|
if (data.action === 'update') {
|
||||||
setDiskSpaceInfo(data.diskSpace)
|
setDiskSpaceInfo(data.diskSpace)
|
||||||
|
@ -560,30 +592,32 @@ const Campaign = () => {
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
Nome
|
Name
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
Status
|
Status
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<Can
|
<TableCell align="center">
|
||||||
role={user.profile}
|
Sent
|
||||||
perform="connection-button:show"
|
</TableCell>
|
||||||
yes={() => (
|
|
||||||
<TableCell align="center">
|
|
||||||
{i18n.t('connections.table.session')}
|
|
||||||
</TableCell>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
Whatsapp sender
|
Read
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell align="center">
|
||||||
|
Start/stop
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell align="center">
|
||||||
|
Sender
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
Mensagem
|
Message
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
|
@ -604,18 +638,20 @@ const Campaign = () => {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
Status
|
{campaign.status}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<Can
|
<TableCell align="center">
|
||||||
role={user.profile}
|
{campaign.status === 'stopped' || campaign.status === 'running' || campaign.status === 'success' ? `${campaign.sent}/${campaign.all}` : '0/0'}
|
||||||
perform="connection-button:show"
|
</TableCell>
|
||||||
yes={() => (
|
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
{renderActionButtons(campaign)}
|
{campaign.status === 'stopped' || campaign.status === 'running' || campaign.status === 'success' ? `${campaign.read}` : '0'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
|
||||||
/>
|
<TableCell align="center">
|
||||||
|
{renderActionButtons(campaign)}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
{campaign.whatsapp_sender}
|
{campaign.whatsapp_sender}
|
||||||
|
@ -626,56 +662,29 @@ const Campaign = () => {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
<Can
|
|
||||||
role={user.profile}
|
|
||||||
perform="show-icon-edit-whatsapp"
|
|
||||||
yes={() => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
border: 'none',
|
|
||||||
display: 'inline',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(settings &&
|
|
||||||
settings.length > 0 &&
|
|
||||||
getSettingValue('editURA') &&
|
|
||||||
getSettingValue('editURA') ===
|
|
||||||
'enabled') |
|
|
||||||
(user.profile === 'master') ? (
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={() =>
|
|
||||||
handleEditCampaign(campaign)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Edit />
|
|
||||||
</IconButton>
|
|
||||||
) : (
|
|
||||||
<div></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Can
|
{campaign.status != 'success' && (
|
||||||
role={user.profile}
|
<IconButton
|
||||||
perform="btn-remove-whatsapp"
|
size="small"
|
||||||
yes={() => (
|
onClick={() =>
|
||||||
<IconButton
|
handleEditCampaign(campaign)
|
||||||
size="small"
|
}
|
||||||
onClick={(e) => {
|
>
|
||||||
handleOpenConfirmationModal(
|
<Edit />
|
||||||
'delete',
|
</IconButton>)}
|
||||||
campaign.id
|
|
||||||
)
|
|
||||||
}}
|
<IconButton
|
||||||
>
|
size="small"
|
||||||
<DeleteOutline />
|
onClick={(e) => {
|
||||||
</IconButton>
|
handleOpenConfirmationModal(
|
||||||
)}
|
'delete',
|
||||||
/>
|
campaign.id
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteOutline />
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue