Merge pull request #10 from canove/test

added multiple WhatsApp account support
This commit is contained in:
Cassio Santos
2020-09-07 12:01:42 -03:00
committed by GitHub
80 changed files with 2802 additions and 1591 deletions

101
README.md
View File

@@ -4,7 +4,7 @@ A _very simple_ Ticket System based on WhatsApp messages.
Backend uses [whatsapp-web.js](https://github.com/pedroslopez/whatsapp-web.js) to receive and send WhatsApp messages, create tickets from them and store all in a MySQL database.
Frontend is a full-featured multi-user _chat app_ bootstrapped with react-create-app and Material UI, that comunicates with backend using REST API and Websockets. It allows you to interact with contacts, tickets, send and receive WhatsApp messagees.
Frontend is a full-featured multi-user _chat app_ bootstrapped with react-create-app and Material UI, that comunicates with backend using REST API and Websockets. It allows you to interact with contacts, tickets, send and receive WhatsApp messages.
**NOTE**: I can't guarantee you will not be blocked by using this method, although it has worked for me. WhatsApp does not allow bots or unofficial clients on their platform, so this shouldn't be considered totally safe.
@@ -22,13 +22,22 @@ If a contact sent a new message in less than 2 hours interval, and there is no t
## Screenshots
<img src="https://raw.githubusercontent.com/canove/whaticket/master/images/chat2.png" width="350"> <img src="https://raw.githubusercontent.com/canove/whaticket/master/images/chat3.png" width="350">
<img src="https://raw.githubusercontent.com/canove/whaticket/master/images/tickets2.png" width="350"> <img src="https://raw.githubusercontent.com/canove/whaticket/master/images/tickets3.png" width="350"> <img src="https://raw.githubusercontent.com/canove/whaticket/master/images/multiple-whatsapps2.png" width="350">
## Features
- Have multiple users chating in same WhatsApp Number ✅
- Connect to multiple WhatsApp accounts and receive all messages in one place.
- Create and chat with new contacts without touching cellphone ✅
- Send and receive message ✅
- Send media (images/audio/documents) ✅
- Receive media (images/audio/video/documents) ✅
## Demo
**Note**: It's not a good idea to sync your whatsapp account is this demo enviroment, because all your received messages will be stored in database and will be accessible by everyone that access this URL and creates an account.
**Note**: It's not a good idea to sync your WhatsApp account is this demo enviroment, because all your received messages will be stored in database and will be accessible by everyone that access this URL and creates an account.
That said, theres not much to test without syncing an whatsapp account, since adding contacts or tickets simple throws an error if app is not synced with whatassp. I will create a better test enviroment in future.
That said, theres not much to test without syncing an WhatsApp account, since adding contacts or tickets simple throws an error if app is not synced with WhatsApp.
Meanwhile, if you want to test it, remember to disconnect session and delete all tickets and contacts after your tests.
@@ -38,12 +47,12 @@ email: demo@demo.com
password: demo123
It's online thanks to [@ramphy](https://github.com/ramphy), who provided a VPS for me to create these installation instructions.
It's online thanks to [@ramphy](https://github.com/ramphy), who provided a VPS to creation of these installation instructions.
## Installation and Usage (Linux Ubuntu - Development)
Create Mysql Database using docker:
_Note_: change dbname, username password.
_Note_: change MYSQL_DATABASE, MYSQL_PASSWORD, MYSQL_USER and MYSQL_ROOT_PASSWORD.
```bash
docker run --name whaticketdb -e MYSQL_ROOT_PASSWORD=strongpassword -e MYSQL_DATABASE=whaticket -e MYSQL_USER=whaticket -e MYSQL_PASSWORD=whaticket --restart always -p 3306:3306 -d mariadb:latest --character-set-server=utf8mb4 --collation-server=utf8mb4_bin
@@ -57,9 +66,10 @@ sudo apt-get install -y libgbm-dev wget unzip fontconfig locales gconf-service l
- Clone this repo
- On backend folder:
- Copy .env.example to .env and fill it with database details
- Copy .env.example to .env and fill it with database details.
- Install dependecies: `npm install` (Only in the first time)
- Create database tables: `npx sequelize db:migrate` (Only in the first time)
- Fill database with initial values: `npx sequelize db:seed:all`
- Start backend: `npm start`
- In another terminal, on frontend folder:
- Copy .env.example to .env and fill it with backend URL (normally localhost:port)
@@ -67,12 +77,13 @@ sudo apt-get install -y libgbm-dev wget unzip fontconfig locales gconf-service l
- Start frontend: `npm start`
- Go to http://your_server_ip:3000/signup
- Create an user and login with it.
- On the sidebard, go to _Connection_ and read QRCode with your WhatsApp.
- On the sidebard, go to _Connections_ and create your first WhatsApp connection.
- Wait for QR CODE button to appear, click it and read qr code.
- Done. Every message received by your synced WhatsApp number will appear in Tickets List.
## Basic production deployment (Ubuntu 18.04 VPS)
You need two subdomains forwarding to youts VPS ip to follow these instructions. We'll use `myapp.mydomain.com` and `api.mydomain.com` in examples.
You'll need two subdomains forwarding to yours VPS ip to follow these instructions. We'll use `myapp.mydomain.com` to frontend and `api.mydomain.com` to backend in the following example. We'll also use an dedicated user with sudo privileges no deploy it (not root).
Update all system packages:
@@ -89,7 +100,7 @@ node -v
npm -v
```
Install docker:
Install docker and add you user to docker group:
```bash
sudo apt install apt-transport-https ca-certificates curl software-properties-common
@@ -98,20 +109,21 @@ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubun
sudo apt update
sudo apt install docker-ce
sudo systemctl status docker
```
Add current user to docker group:
```bash
sudo usermod -aG docker \${USER}
su - \${USER}
```
Create database container (Instructions in installation)
Create Mysql Database using docker:
_Note_: change MYSQL_DATABASE, MYSQL_PASSWORD, MYSQL_USER and MYSQL_ROOT_PASSWORD.
```bash
docker run --name whaticketdb -e MYSQL_ROOT_PASSWORD=strongpassword -e MYSQL_DATABASE=whaticket -e MYSQL_USER=whaticket -e MYSQL_PASSWORD=whaticket --restart always -p 3306:3306 -d mariadb:latest --character-set-server=utf8mb4 --collation-server=utf8mb4_bin
```
Clone this repository:
```bash
cd ~
git clone https://github.com/canove/whaticket whaticket
```
@@ -124,44 +136,44 @@ nano whaticket/backend/.env
```bash
NODE_ENV=
BACKEND_URL=https://api.mydomain.com
PROXY_PORT=443
BACKEND_URL=https://api.mydomain.com #USE HTTPS HERE, WE WILL ADD SSL LATTER
PROXY_PORT=443 #USE NGINX REVERSE PROXY PORT HERE, WE WILL CONFIGURE IT LATTER
PORT=8080
DB_HOST=
DB_HOST=localhost
DB_USER=
DB_PASS=
DB_NAME=
```
Install puppeteer dependencies(Instructions in installation)
Install puppeteer dependencies:
Install backend dependencies and run migrations:
```bash
sudo apt-get install -y libgbm-dev wget unzip fontconfig locales gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils
```
Install backend dependencies, run migrations and seeds:
```bash
cd whaticket/backend
npm install
npx sequelize db:migrate
npx sequelize db:seed:all
```
Start backend to confirm its working, you should see: `Server started on port...` on console.
Start it with `npm start`, you should see: `Server started on port...` on console. Hit `CTRL + C` to exit.
Install pm2 **with sudo**:
Install pm2 **with sudo**, and start backend with it:
```bash
sudo npm install -g pm2
```
Start backend with pm2:
```bash
pm2 start src/app.js --name whaticket-backend
```
Make pm2 auto start afeter reboot:
```bash
pm2 startup ubuntu -u YOUR_USERNAME
pm2 startup ubuntu -u `YOUR_USERNAME`
```
Copy the last line outputed from previus command and run it, its something like:
@@ -170,10 +182,10 @@ Copy the last line outputed from previus command and run it, its something like:
sudo env PATH=\$PATH:/usr/bin pm2 startup ubuntu -u YOUR_USERNAME --hp /home/YOUR_USERNAM
```
Now, lets prepare frontend:
Go to frontend folder and install dependencies:
```bash
cd ../whaticket/frontend
cd ../frontend
npm install
```
@@ -189,16 +201,24 @@ Build frontend app:
npm run build
```
Start frotend with pm2:
Start frontend with pm2, and save pm2 process list to start automatically after reboot:
```bash
pm2 start server.js --name whaticket-frontend
pm2 save
```
Save pm2 process list to start automatically after reboot:
To check if it's running, run `pm2 list`, it should look like:
```bash
pm2 save
deploy@ubuntu-whats:~$ pm2 list
┌─────┬─────────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ . │ status │ cpu │ mem │ user │ watching │
├─────┼─────────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
1 │ whaticket-frontend │ default │ 0.1.0 │ fork │ 179249 │ 12D │ 0 │ online │ 0.3% │ 50.2mb │ deploy │ disabled │
6 │ whaticket-backend │ default │ 1.0.0 │ fork │ 179253 │ 12D │ 15 │ online │ 0.3% │ 118.5mb │ deploy │ disabled │
└─────┴─────────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
```
Install nginx:
@@ -211,7 +231,6 @@ Remove nginx default site:
```bash
sudo rm /etc/nginx/sites-enabled/default
sudo service nginx restart
```
Create a new nginx site to frontend app:
@@ -240,7 +259,7 @@ server {
}
```
Create another one to backend api, changing `server_name` to yours equivalent to `api.mydomain.com`, and `proxy_pass` to you localhost backend node server URL:
Create another one to backend api, changing `server_name` to yours equivalent to `api.mydomain.com`, and `proxy_pass` to your localhost backend node server URL:
```bash
sudo cp /etc/nginx/sites-available/whaticket-frontend /etc/nginx/sites-available/whaticket-backend
@@ -257,7 +276,7 @@ server {
}
```
Create a symbolic link to enalbe nginx site:
Create a symbolic links to enalbe nginx sites:
```bash
sudo ln -s /etc/nginx/sites-available/whaticket-frontend /etc/nginx/sites-enabled
@@ -285,14 +304,6 @@ Enable SSL on nginx (Accept all information asked):
sudo certbot --nginx
```
## Features
- Have multiple users chating in same WhatsApp Number ✅
- Create and chat with new contacts without touching cellphone ✅
- Send and receive message ✅
- Send media (images/audio/documents) ✅
- Receive media (images/audio/video/documents) ✅
## Contributing
Any help and suggestions are welcome!

View File

@@ -22,7 +22,6 @@
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-async-errors": "^3.1.1",
"express-validator": "^6.5.0",
"jsonwebtoken": "^8.5.1",
"multer": "^1.4.2",
"mysql2": "^2.1.0",
@@ -30,7 +29,8 @@
"sequelize": "^6.3.4",
"socket.io": "^2.3.0",
"whatsapp-web.js": "^1.8.0",
"youch": "^2.0.10"
"youch": "^2.0.10",
"yup": "^0.29.3"
},
"devDependencies": {
"nodemon": "^2.0.4",

View File

@@ -7,16 +7,12 @@ const cors = require("cors");
const multer = require("multer");
const Sentry = require("@sentry/node");
const wBot = require("./libs/wbot");
const { initWbot } = require("./libs/wbot");
const wbotMessageListener = require("./services/wbotMessageListener");
const wbotMonitor = require("./services/wbotMonitor");
const Whatsapp = require("./models/Whatsapp");
const MessagesRoutes = require("./routes/messages");
const ContactsRoutes = require("./routes/contacts");
const AuthRoutes = require("./routes/auth");
const TicketsRoutes = require("./routes/tickets");
const WhatsRoutes = require("./routes/whatsapp");
const UsersRoutes = require("./routes/users");
const Router = require("./router");
const app = express();
@@ -40,13 +36,7 @@ app.use(cors());
app.use(express.json());
app.use(multer({ storage: fileStorage }).single("media"));
app.use("/public", express.static(path.join(__dirname, "..", "public")));
app.use("/auth", AuthRoutes);
app.use(ContactsRoutes);
app.use(TicketsRoutes);
app.use(MessagesRoutes);
app.use(WhatsRoutes);
app.use(UsersRoutes);
app.use(Router);
const io = require("./libs/socket").init(server);
io.on("connection", socket => {
@@ -66,13 +56,21 @@ io.on("connection", socket => {
});
});
wBot
.init()
.then(({ dbSession }) => {
wbotMessageListener();
wbotMonitor(dbSession);
})
.catch(err => console.log(err));
const startWhatsAppSessions = async () => {
const whatsapps = await Whatsapp.findAll();
if (whatsapps.length > 0) {
whatsapps.forEach(whatsapp => {
initWbot(whatsapp)
.then(() => {
wbotMessageListener(whatsapp);
wbotMonitor(whatsapp);
})
.catch(err => console.log(err));
});
}
};
startWhatsAppSessions();
app.use(Sentry.Handlers.errorHandler());
@@ -82,6 +80,6 @@ app.use(async (err, req, res, next) => {
console.log(err);
return res.status(500).json(errors);
}
console.log(err);
return res.status(500).json({ error: "Internal server error" });
});

View File

@@ -12,4 +12,5 @@ module.exports = {
username: process.env.DB_USER,
password: process.env.DB_PASS,
logging: false,
seederStorage: "sequelize",
};

View File

@@ -2,13 +2,14 @@ const Sequelize = require("sequelize");
const { Op } = require("sequelize");
const Contact = require("../models/Contact");
const Whatsapp = require("../models/Whatsapp");
const ContactCustomField = require("../models/ContactCustomField");
const { getIO } = require("../libs/socket");
const { getWbot } = require("../libs/wbot");
exports.index = async (req, res) => {
const { searchParam = "", pageNumber = 1, rowsPerPage = 10 } = req.query;
const { searchParam = "", pageNumber = 1 } = req.query;
const whereCondition = {
[Op.or]: [
@@ -23,7 +24,7 @@ exports.index = async (req, res) => {
],
};
let limit = +rowsPerPage;
let limit = 20;
let offset = limit * (pageNumber - 1);
const { count, rows: contacts } = await Contact.findAndCountAll({
@@ -33,31 +34,42 @@ exports.index = async (req, res) => {
order: [["createdAt", "DESC"]],
});
return res.json({ contacts, count });
const hasMore = count > offset + contacts.length;
return res.json({ contacts, count, hasMore });
};
exports.store = async (req, res) => {
const wbot = getWbot();
const defaultWhatsapp = await Whatsapp.findOne({
where: { default: true },
});
if (!defaultWhatsapp) {
return res
.status(404)
.json({ error: "No default WhatsApp found. Check Connection page." });
}
const wbot = getWbot(defaultWhatsapp);
const io = getIO();
const newContact = req.body;
let isValidNumber;
try {
isValidNumber = await wbot.isRegisteredUser(`${newContact.number}@c.us`);
const isValidNumber = await wbot.isRegisteredUser(
`${newContact.number}@c.us`
);
if (!isValidNumber) {
return res
.status(400)
.json({ error: "The suplied number is not a valid Whatsapp number" });
}
} catch (err) {
console.log("Could not check whatsapp contact. Is session details valid?");
console.log(err);
return res.status(500).json({
error: "Could not check whatsapp contact. Check connection page.",
});
}
if (!isValidNumber) {
return res
.status(400)
.json({ error: "The suplied number is not a valid Whatsapp number" });
}
const profilePicUrl = await wbot.getProfilePicUrl(
`${newContact.number}@c.us`
);
@@ -74,26 +86,22 @@ exports.store = async (req, res) => {
contact: contact,
});
res.status(200).json(contact);
return res.status(200).json(contact);
};
exports.show = async (req, res) => {
const { contactId } = req.params;
const { id, name, number, email, extraInfo } = await Contact.findByPk(
contactId,
{
include: "extraInfo",
}
);
res.status(200).json({
id,
name,
number,
email,
extraInfo,
const contact = await Contact.findByPk(contactId, {
include: "extraInfo",
attributes: ["id", "name", "number", "email"],
});
if (!contact) {
return res.status(404).json({ error: "No contact found with this id." });
}
return res.status(200).json(contact);
};
exports.update = async (req, res) => {
@@ -108,7 +116,7 @@ exports.update = async (req, res) => {
});
if (!contact) {
return res.status(400).json({ error: "No contact found with this ID" });
return res.status(404).json({ error: "No contact found with this ID" });
}
if (updatedContact.extraInfo) {
@@ -138,7 +146,7 @@ exports.update = async (req, res) => {
contact: contact,
});
res.status(200).json(contact);
return res.status(200).json(contact);
};
exports.delete = async (req, res) => {
@@ -148,7 +156,7 @@ exports.delete = async (req, res) => {
const contact = await Contact.findByPk(contactId);
if (!contact) {
return res.status(400).json({ error: "No contact found with this ID" });
return res.status(404).json({ error: "No contact found with this ID" });
}
await contact.destroy();
@@ -158,5 +166,5 @@ exports.delete = async (req, res) => {
contactId: contactId,
});
res.status(200).json({ message: "Contact deleted" });
return res.status(200).json({ message: "Contact deleted" });
};

View File

@@ -1,12 +1,32 @@
const Contact = require("../models/Contact");
const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
const { getWbot, init } = require("../libs/wbot");
const { getWbot, initWbot } = require("../libs/wbot");
exports.store = async (req, res, next) => {
const io = getIO();
const wbot = getWbot();
const defaultWhatsapp = await Whatsapp.findOne({
where: { default: true },
});
const phoneContacts = await wbot.getContacts();
if (!defaultWhatsapp) {
return res
.status(404)
.json({ error: "No default WhatsApp found. Check Connection page." });
}
const io = getIO();
const wbot = getWbot(defaultWhatsapp);
let phoneContacts;
try {
phoneContacts = await wbot.getContacts();
} catch (err) {
console.log(err);
return res.status(500).json({
error: "Could not check whatsapp contact. Check connection page.",
});
}
await Promise.all(
phoneContacts.map(async ({ number, name }) => {

View File

@@ -1,6 +1,7 @@
const Message = require("../models/Message");
const Contact = require("../models/Contact");
const User = require("../models/User");
const Whatsapp = require("../models/Whatsapp");
const Ticket = require("../models/Ticket");
const { getIO } = require("../libs/socket");
@@ -11,7 +12,7 @@ const { MessageMedia } = require("whatsapp-web.js");
const setMessagesAsRead = async ticket => {
const io = getIO();
const wbot = getWbot();
const wbot = getWbot(ticket.whatsappId);
await Message.update(
{ read: true },
@@ -38,9 +39,6 @@ const setMessagesAsRead = async ticket => {
};
exports.index = async (req, res, next) => {
// const wbot = getWbot();
// const io = getIO();
const { ticketId } = req.params;
const { searchParam = "", pageNumber = 1 } = req.query;
@@ -107,7 +105,6 @@ exports.index = async (req, res, next) => {
};
exports.store = async (req, res, next) => {
const wbot = getWbot();
const io = getIO();
const { ticketId } = req.params;
@@ -125,6 +122,26 @@ exports.store = async (req, res, next) => {
],
});
if (!ticket) {
return res.status(404).json({ error: "No ticket found with this ID" });
}
if (!ticket.whatsappId) {
const defaultWhatsapp = await Whatsapp.findOne({
where: { default: true },
});
if (!defaultWhatsapp) {
return res
.status(404)
.json({ error: "No default WhatsApp found. Check Connection page." });
}
await ticket.setWhatsapp(defaultWhatsapp);
}
const wbot = getWbot(ticket.whatsappId);
try {
if (media) {
const newMedia = MessageMedia.fromFilePath(req.file.path);
@@ -177,7 +194,7 @@ exports.store = async (req, res, next) => {
await setMessagesAsRead(ticket);
return res.json({ newMessage, ticket });
return res.status(200).json({ newMessage, ticket });
}
return res

View File

@@ -8,7 +8,7 @@ exports.store = async (req, res, next) => {
const user = await User.findOne({ where: { email: email } });
if (!user) {
return res.status(401).json({ error: "No user found with this email" });
return res.status(404).json({ error: "No user found with this email" });
}
if (!(await user.checkPassword(password))) {
@@ -23,7 +23,10 @@ exports.store = async (req, res, next) => {
}
);
return res
.status(200)
.json({ token: token, username: user.name, userId: user.id });
return res.status(200).json({
token: token,
username: user.name,
profile: user.profile,
userId: user.id,
});
};

View File

@@ -0,0 +1,39 @@
const Setting = require("../models/Setting");
const { getIO } = require("../libs/socket");
exports.index = async (req, res) => {
if (req.user.profile !== "admin") {
return res
.status(403)
.json({ error: "Only administrators can access this route." });
}
const settings = await Setting.findAll();
return res.status(200).json(settings);
};
exports.update = async (req, res) => {
if (req.user.profile !== "admin") {
return res
.status(403)
.json({ error: "Only administrators can access this route." });
}
const io = getIO();
const { settingKey } = req.params;
const setting = await Setting.findByPk(settingKey);
if (!setting) {
return res.status(404).json({ error: "No setting found with this ID" });
}
await setting.update(req.body);
io.emit("settings", {
action: "update",
setting,
});
return res.status(200).json(setting);
};

View File

@@ -4,6 +4,7 @@ const { startOfDay, endOfDay, parseISO } = require("date-fns");
const Ticket = require("../models/Ticket");
const Contact = require("../models/Contact");
const Message = require("../models/Message");
const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
@@ -16,7 +17,7 @@ exports.index = async (req, res) => {
showAll,
} = req.query;
const userId = req.userId;
const userId = req.user.id;
const limit = 20;
const offset = limit * (pageNumber - 1);
@@ -105,12 +106,23 @@ exports.index = async (req, res) => {
const hasMore = count > offset + tickets.length;
return res.json({ count, tickets, hasMore });
return res.status(200).json({ count, tickets, hasMore });
};
exports.store = async (req, res) => {
const io = getIO();
const ticket = await Ticket.create(req.body);
const defaultWhatsapp = await Whatsapp.findOne({
where: { default: true },
});
if (!defaultWhatsapp) {
return res
.status(404)
.json({ error: "No default WhatsApp found. Check Connection page." });
}
const ticket = await defaultWhatsapp.createTicket(req.body);
const contact = await ticket.getContact();
@@ -121,7 +133,7 @@ exports.store = async (req, res) => {
ticket: serializaedTicket,
});
res.status(200).json(ticket);
return res.status(200).json(ticket);
};
exports.update = async (req, res) => {
@@ -139,7 +151,7 @@ exports.update = async (req, res) => {
});
if (!ticket) {
return res.status(400).json({ error: "No ticket found with this ID" });
return res.status(404).json({ error: "No ticket found with this ID" });
}
await ticket.update(req.body);
@@ -149,7 +161,7 @@ exports.update = async (req, res) => {
ticket: ticket,
});
res.status(200).json(ticket);
return res.status(200).json(ticket);
};
exports.delete = async (req, res) => {
@@ -169,5 +181,5 @@ exports.delete = async (req, res) => {
ticketId: ticket.id,
});
res.status(200).json({ message: "ticket deleted" });
return res.status(200).json({ message: "ticket deleted" });
};

View File

@@ -1,48 +1,161 @@
const { validationResult } = require("express-validator");
const Sequelize = require("sequelize");
const Yup = require("yup");
const { Op } = require("sequelize");
const User = require("../models/User");
const Setting = require("../models/Setting");
const { getIO } = require("../libs/socket");
exports.index = async (req, res) => {
// const { searchParam = "", pageNumber = 1 } = req.query;
if (req.user.profile !== "admin") {
return res
.status(403)
.json({ error: "Only administrators can access this route." });
}
const users = await User.findAll({ attributes: ["name", "id", "email"] });
const { searchParam = "", pageNumber = 1 } = req.query;
return res.status(200).json(users);
const whereCondition = {
[Op.or]: [
{
name: Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("name")),
"LIKE",
"%" + searchParam.toLowerCase() + "%"
),
},
{ email: { [Op.like]: `%${searchParam.toLowerCase()}%` } },
],
};
let limit = 20;
let offset = limit * (pageNumber - 1);
const { count, rows: users } = await User.findAndCountAll({
attributes: ["name", "id", "email", "profile"],
where: whereCondition,
limit,
offset,
order: [["createdAt", "DESC"]],
});
const hasMore = count > offset + users.length;
return res.status(200).json({ users, count, hasMore });
};
exports.store = async (req, res, next) => {
const errors = validationResult(req);
console.log(req.url);
const schema = Yup.object().shape({
name: Yup.string().required().min(2),
email: Yup.string()
.email()
.required()
.test(
"Check-email",
"An user with this email already exists",
async value => {
const userFound = await User.findOne({ where: { email: value } });
return !Boolean(userFound);
}
),
password: Yup.string().required().min(5),
});
if (!errors.isEmpty()) {
if (req.url === "/signup") {
const { value: userCreation } = await Setting.findByPk("userCreation");
if (userCreation === "disabled") {
return res
.status(403)
.json({ error: "User creation is disabled by administrator." });
}
} else if (req.user.profile !== "admin") {
return res
.status(400)
.json({ error: "Validation failed", data: errors.array() });
.status(403)
.json({ error: "Only administrators can create users." });
}
const { name, id, email } = await User.create(req.body);
try {
await schema.validate(req.body);
} catch (err) {
return res.status(400).json({ error: err.message });
}
res.status(201).json({ message: "User created!", userId: id });
const io = getIO();
const { name, id, email, profile } = await User.create(req.body);
io.emit("user", {
action: "create",
user: { name, id, email, profile },
});
return res.status(201).json({ message: "User created!", userId: id });
};
exports.update = async (req, res) => {
exports.show = async (req, res) => {
const { userId } = req.params;
const user = await User.findByPk(userId, {
attributes: ["name", "id", "email"],
attributes: ["id", "name", "email", "profile"],
});
if (!user) {
res.status(400).json({ error: "No user found with this id." });
}
return res.status(200).json(user);
};
exports.update = async (req, res) => {
const schema = Yup.object().shape({
name: Yup.string().min(2),
email: Yup.string().email(),
password: Yup.string(),
});
if (req.user.profile !== "admin") {
return res
.status(403)
.json({ error: "Only administrators can edit users." });
}
await schema.validate(req.body);
const io = getIO();
const { userId } = req.params;
const user = await User.findByPk(userId, {
attributes: ["name", "id", "email", "profile"],
});
if (!user) {
res.status(404).json({ error: "No user found with this id." });
}
if (user.profile === "admin" && req.body.profile === "user") {
const adminUsers = await User.count({ where: { profile: "admin" } });
if (adminUsers <= 1) {
return res
.status(403)
.json({ error: "There must be at leat one admin user." });
}
}
await user.update(req.body);
//todo, send socket IO to users channel.
io.emit("user", {
action: "update",
user: user,
});
res.status(200).json(user);
return res.status(200).json(user);
};
exports.delete = async (req, res) => {
const io = getIO();
const { userId } = req.params;
const user = await User.findByPk(userId);
@@ -51,7 +164,18 @@ exports.delete = async (req, res) => {
res.status(400).json({ error: "No user found with this id." });
}
if (req.user.profile !== "admin") {
return res
.status(403)
.json({ error: "Only administrators can edit users." });
}
await user.destroy();
res.status(200).json({ message: "User deleted" });
io.emit("user", {
action: "delete",
userId: userId,
});
return res.status(200).json({ message: "User deleted" });
};

View File

@@ -0,0 +1,141 @@
const Yup = require("yup");
const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
const { getWbot, initWbot, removeWbot } = require("../libs/wbot");
const wbotMessageListener = require("../services/wbotMessageListener");
const wbotMonitor = require("../services/wbotMonitor");
exports.index = async (req, res) => {
const whatsapps = await Whatsapp.findAll();
return res.status(200).json(whatsapps);
};
exports.store = async (req, res) => {
const schema = Yup.object().shape({
name: Yup.string().required().min(2),
default: Yup.boolean()
.required()
.test(
"Check-default",
"Only one default whatsapp is permited",
async value => {
if (value === true) {
const whatsappFound = await Whatsapp.findOne({
where: { default: true },
});
return !Boolean(whatsappFound);
} else return true;
}
),
});
try {
await schema.validate(req.body);
} catch (err) {
return res.status(400).json({ error: err.message });
}
const io = getIO();
const whatsapp = await Whatsapp.create(req.body);
if (!whatsapp) {
return res.status(400).json({ error: "Cannot create whatsapp session." });
}
initWbot(whatsapp)
.then(() => {
wbotMessageListener(whatsapp);
wbotMonitor(whatsapp);
})
.catch(err => console.log(err));
io.emit("whatsapp", {
action: "update",
whatsapp: whatsapp,
});
return res.status(200).json(whatsapp);
};
exports.show = async (req, res) => {
const { whatsappId } = req.params;
const whatsapp = await Whatsapp.findByPk(whatsappId);
if (!whatsapp) {
return res.status(200).json({ message: "Session not found" });
}
return res.status(200).json(whatsapp);
};
exports.update = async (req, res) => {
const { whatsappId } = req.params;
const schema = Yup.object().shape({
name: Yup.string().required().min(2),
default: Yup.boolean()
.required()
.test(
"Check-default",
"Only one default whatsapp is permited",
async value => {
if (value === true) {
const whatsappFound = await Whatsapp.findOne({
where: { default: true },
});
if (whatsappFound) {
return !(whatsappFound.id !== +whatsappId);
} else {
return true;
}
} else return true;
}
),
});
try {
await schema.validate(req.body);
} catch (err) {
return res.status(400).json({ error: err.message });
}
const io = getIO();
const whatsapp = await Whatsapp.findByPk(whatsappId);
if (!whatsapp) {
return res.status(404).json({ message: "Whatsapp not found" });
}
await whatsapp.update(req.body);
io.emit("whatsapp", {
action: "update",
whatsapp: whatsapp,
});
return res.status(200).json({ message: "Whatsapp updated" });
};
exports.delete = async (req, res) => {
const io = getIO();
const { whatsappId } = req.params;
const whatsapp = await Whatsapp.findByPk(whatsappId);
if (!whatsapp) {
return res.status(404).json({ message: "Whatsapp not found" });
}
await whatsapp.destroy();
removeWbot(whatsapp.id);
io.emit("whatsapp", {
action: "delete",
whatsappId: whatsapp.id,
});
return res.status(200).json({ message: "Whatsapp deleted." });
};

View File

@@ -1,45 +1,32 @@
const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
const { getWbot } = require("../libs/wbot");
// const Whatsapp = require("../models/Whatsapp");
// const { getIO } = require("../libs/socket");
// const { getWbot, initWbot, removeWbot } = require("../libs/wbot");
// const wbotMessageListener = require("../services/wbotMessageListener");
// const wbotMonitor = require("../services/wbotMonitor");
exports.show = async (req, res) => {
const { sessionId } = req.params;
const dbSession = await Whatsapp.findByPk(sessionId);
// exports.show = async (req, res) => {
// const { whatsappId } = req.params;
// const dbSession = await Whatsapp.findByPk(whatsappId);
if (!dbSession) {
return res.status(200).json({ message: "Session not found" });
}
// if (!dbSession) {
// return res.status(200).json({ message: "Session not found" });
// }
return res.status(200).json(dbSession);
};
exports.delete = async (req, res) => {
const wbot = getWbot();
const io = getIO();
const { sessionId } = req.params;
const dbSession = await Whatsapp.findByPk(sessionId);
if (!dbSession) {
return res.status(200).json({ message: "Session not found" });
}
await dbSession.update({ session: "", status: "pending" });
wbot.logout();
io.emit("session", {
action: "logout",
session: dbSession,
});
return res.status(200).json({ message: "session disconnected" });
};
// exports.getContacts = async (req, res, next) => {
// const io = getIO();
// const wbot = getWbot();
// const phoneContacts = await wbot.getContacts();
// return res.status(200).json(phoneContacts);
// return res.status(200).json(dbSession);
// };
// exports.delete = async (req, res) => {
// const { whatsappId } = req.params;
// const dbSession = await Whatsapp.findByPk(whatsappId);
// if (!dbSession) {
// return res.status(404).json({ message: "Session not found" });
// }
// const wbot = getWbot(dbSession.id);
// wbot.logout();
// return res.status(200).json({ message: "Session disconnected." });
// };

View File

@@ -7,8 +7,17 @@ const Ticket = require("../models/Ticket");
const Message = require("../models/Message");
const Whatsapp = require("../models/Whatsapp");
const ContactCustomField = require("../models/ContactCustomField");
const Setting = require("../models/Setting");
const models = [User, Contact, Ticket, Message, Whatsapp, ContactCustomField];
const models = [
User,
Contact,
Ticket,
Message,
Whatsapp,
ContactCustomField,
Setting,
];
class Database {
constructor() {

View File

@@ -0,0 +1,15 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn("Users", "profile", {
type: Sequelize.STRING,
allowNull: false,
defaultValue: "admin",
});
},
down: queryInterface => {
return queryInterface.removeColumn("Users", "profile");
},
};

View File

@@ -0,0 +1,29 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("Settings", {
key: {
type: Sequelize.STRING,
primaryKey: true,
allowNull: false,
},
value: {
type: Sequelize.TEXT,
allowNull: false,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
},
});
},
down: queryInterface => {
return queryInterface.dropTable("Settings");
},
};

View File

@@ -0,0 +1,15 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn("Whatsapps", "name", {
type: Sequelize.STRING,
allowNull: false,
unique: true,
});
},
down: queryInterface => {
return queryInterface.removeColumn("Whatsapps", "name");
},
};

View File

@@ -0,0 +1,15 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn("Whatsapps", "default", {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
});
},
down: queryInterface => {
return queryInterface.removeColumn("Whatsapps", "default");
},
};

View File

@@ -0,0 +1,16 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn("Tickets", "whatsappId", {
type: Sequelize.INTEGER,
references: { model: "Whatsapps", key: "id" },
onUpdate: "CASCADE",
onDelete: "SET NULL",
});
},
down: queryInterface => {
return queryInterface.removeColumn("Tickets", "whatsappId");
},
};

View File

@@ -1,48 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert(
"Contacts",
[
{
name: "Joana Doe",
profilePicUrl:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1834&q=80",
number: 5512345678,
createdAt: new Date(),
updatedAt: new Date(),
},
{
name: "John Rulles",
profilePicUrl:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80",
number: 5512345679,
createdAt: new Date(),
updatedAt: new Date(),
},
{
name: "Jonas Jhones",
profilePicUrl:
"https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80",
number: 5512345680,
createdAt: new Date(),
updatedAt: new Date(),
},
{
name: "Julie June",
profilePicUrl:
"https://images.unsplash.com/photo-1493666438817-866a91353ca9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1049&q=80",
number: 5512345681,
createdAt: new Date(),
updatedAt: new Date(),
},
],
{}
);
},
down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete("Contacts", null, {});
},
};

View File

@@ -1,261 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert(
"Tickets",
[
{
status: "pending",
lastMessage: "hello!",
contactId: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 2,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 2,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 2,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 2,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 2,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 2,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 3,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 3,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 3,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 3,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 3,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 3,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 3,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 4,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 4,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 4,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 4,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 4,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 4,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 4,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 4,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 2,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 2,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 2,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 2,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 2,
createdAt: new Date(),
updatedAt: new Date(),
},
{
status: "pending",
lastMessage: "hello!",
contactId: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
],
{}
);
},
down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete("Tickets", null, {});
},
};

View File

@@ -1,148 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert(
"Messages",
[
{
id: "12312321342",
body:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
ack: 0,
ticketId: 1,
fromMe: false,
read: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "12312321313",
body:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
ack: 3,
ticketId: 1,
fromMe: true,
read: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "12312321314",
body:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
ack: 3,
ticketId: 1,
fromMe: true,
read: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "12312321315",
body:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
ack: 0,
ticketId: 1,
fromMe: false,
read: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "12312321316",
body:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
ack: 0,
ticketId: 5,
fromMe: false,
read: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "12312321355",
body:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
ack: 3,
ticketId: 5,
fromMe: true,
read: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "12312321318",
body:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
ack: 3,
ticketId: 5,
fromMe: true,
read: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "12312321319",
body:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
ack: 0,
ticketId: 5,
fromMe: false,
read: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "12312321399",
body:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
ack: 0,
ticketId: 11,
fromMe: false,
read: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "12312321391",
body:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
ack: 3,
ticketId: 11,
fromMe: true,
read: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "12312321392",
body:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
ack: 3,
ticketId: 11,
fromMe: true,
read: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "12312321393",
body:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
ack: 0,
ticketId: 11,
fromMe: false,
read: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
],
{}
);
},
down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete("Messages", null, {});
},
};

View File

@@ -0,0 +1,22 @@
"use strict";
module.exports = {
up: queryInterface => {
return queryInterface.bulkInsert(
"Settings",
[
{
key: "userCreation",
value: "enabled",
createdAt: new Date(),
updatedAt: new Date(),
},
],
{}
);
},
down: queryInterface => {
return queryInterface.bulkDelete("Settings", null, {});
},
};

View File

@@ -3,78 +3,108 @@ const { Client } = require("whatsapp-web.js");
const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
let wbot;
let sessions = [];
module.exports = {
init: async () => {
let sessionCfg;
initWbot: async whatsapp => {
try {
const io = getIO();
const sessionName = whatsapp.name;
let sessionCfg;
const [dbSession] = await Whatsapp.findOrCreate({
where: { id: 1 },
defaults: {
id: 1,
},
});
if (dbSession && dbSession.session) {
sessionCfg = JSON.parse(dbSession.session);
if (whatsapp && whatsapp.session) {
sessionCfg = JSON.parse(whatsapp.session);
}
const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id);
if (sessionIndex !== -1) {
sessions[sessionIndex].destroy();
sessions.splice(sessionIndex, 1);
}
const wbot = new Client({
session: sessionCfg,
restartOnAuthFail: true,
});
wbot.initialize();
wbot.on("qr", async qr => {
console.log("Session:", sessionName);
qrCode.generate(qr, { small: true });
await whatsapp.update({ qrcode: qr, status: "qrcode" });
io.emit("whatsappSession", {
action: "update",
session: whatsapp,
});
});
wbot.on("authenticated", async session => {
console.log("Session:", sessionName, "AUTHENTICATED");
await whatsapp.update({
session: JSON.stringify(session),
status: "authenticated",
});
io.emit("whatsappSession", {
action: "update",
session: whatsapp,
});
});
wbot.on("auth_failure", async msg => {
console.error("Session:", sessionName, "AUTHENTICATION FAILURE", msg);
await whatsapp.update({ session: "" });
});
wbot.on("ready", async () => {
console.log("Session:", sessionName, "READY");
await whatsapp.update({
status: "CONNECTED",
qrcode: "",
});
io.emit("whatsappSession", {
action: "update",
session: whatsapp,
});
wbot.sendPresenceAvailable();
});
wbot.id = whatsapp.id;
sessions.push(wbot);
} catch (err) {
console.log(err);
}
wbot = new Client({
session: sessionCfg,
restartOnAuthFail: true,
});
wbot.initialize();
wbot.on("qr", async qr => {
qrCode.generate(qr, { small: true });
await dbSession.update({ id: 1, qrcode: qr, status: "disconnected" });
getIO().emit("session", {
action: "update",
qr: qr,
session: dbSession,
});
});
wbot.on("authenticated", async session => {
console.log("AUTHENTICATED");
await dbSession.update({
id: 1,
session: JSON.stringify(session),
status: "authenticated",
});
getIO().emit("session", {
action: "authentication",
session: dbSession,
});
});
wbot.on("auth_failure", async msg => {
console.error("AUTHENTICATION FAILURE", msg);
await Whatsapp.update({ session: "" }, { where: { id: 1 } });
});
wbot.on("ready", async () => {
console.log("READY");
await dbSession.update(
{ status: "CONNECTED", qrcode: "" },
{ where: { id: 1 } }
);
// const chats = await wbot.getChats(); // pega as mensagens nao lidas (recebidas quando o bot estava offline)
// let unreadMessages; // todo > salvar isso no DB pra mostrar no frontend
// for (let chat of chats) {
// if (chat.unreadCount > 0) {
// unreadMessages = await chat.fetchMessages({
// limit: chat.unreadCount,
// });
// }
// }
// console.log(unreadMessages);
wbot.sendPresenceAvailable();
});
return { wbot, dbSession };
return null;
},
getWbot: () => {
if (!wbot) {
throw new Error("Wbot not initialized");
getWbot: whatsappId => {
const sessionIndex = sessions.findIndex(s => s.id === whatsappId);
if (sessionIndex === -1) {
console.log("This Wbot session is not initialized");
return null;
}
return sessions[sessionIndex];
},
removeWbot: whatsappId => {
try {
const sessionIndex = sessions.findIndex(s => s.id === whatsappId);
if (sessionIndex !== -1) {
sessions[sessionIndex].destroy();
sessions.splice(sessionIndex, 1);
}
} catch (err) {
console.log(err);
}
return wbot;
},
};

View File

@@ -1,5 +1,7 @@
const jwt = require("jsonwebtoken");
const util = require("util");
const User = require("../models/User");
const authConfig = require("../config/auth");
module.exports = async (req, res, next) => {
@@ -11,12 +13,24 @@ module.exports = async (req, res, next) => {
const [, token] = authHeader.split(" ");
jwt.verify(token, authConfig.secret, (error, result) => {
if (error) {
return res.status(401).json({ error: "Invalid token" });
try {
const decoded = await util.promisify(jwt.verify)(token, authConfig.secret);
const user = await User.findByPk(decoded.userId, {
attributes: ["id", "name", "profile", "email"],
});
if (!user) {
return res
.status(401)
.json({ error: "The token corresponding user does not exists." });
}
req.userId = result.userId;
// todo >> find user in DB and store in req.user to use latter, or throw an error if user not exists anymore
next();
});
req.user = user;
return next();
} catch (err) {
console.log(err);
return res.status(401).json({ error: "Invalid Token" });
}
};

View File

@@ -0,0 +1,24 @@
const Sequelize = require("sequelize");
class Setting extends Sequelize.Model {
static init(sequelize) {
super.init(
{
key: {
type: Sequelize.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
value: { type: Sequelize.TEXT, allowNull: false },
},
{
sequelize,
}
);
return this;
}
}
module.exports = Setting;

View File

@@ -39,6 +39,10 @@ class Ticket extends Sequelize.Model {
static associate(models) {
this.belongsTo(models.Contact, { foreignKey: "contactId", as: "contact" });
this.belongsTo(models.User, { foreignKey: "userId", as: "user" });
this.belongsTo(models.Whatsapp, {
foreignKey: "whatsappId",
as: "whatsapp",
});
this.hasMany(models.Message, { foreignKey: "ticketId", as: "messages" });
}
}

View File

@@ -7,6 +7,7 @@ class User extends Sequelize.Model {
{
name: { type: Sequelize.STRING },
password: { type: Sequelize.VIRTUAL },
profile: { type: Sequelize.STRING, defaultValue: "admin" },
passwordHash: { type: Sequelize.STRING },
email: { type: Sequelize.STRING },
},

View File

@@ -6,9 +6,15 @@ class Whatsapp extends Sequelize.Model {
{
session: { type: Sequelize.TEXT },
qrcode: { type: Sequelize.TEXT },
name: { type: Sequelize.STRING, unique: true, allowNull: false },
status: { type: Sequelize.STRING },
battery: { type: Sequelize.STRING },
plugged: { type: Sequelize.BOOLEAN },
default: {
type: Sequelize.BOOLEAN,
defaultValue: false,
allowNull: false,
},
},
{
sequelize,
@@ -17,6 +23,10 @@ class Whatsapp extends Sequelize.Model {
return this;
}
static associate(models) {
this.hasMany(models.Ticket, { foreignKey: "whatsappId", as: "tickets" });
}
}
module.exports = Whatsapp;

View File

@@ -0,0 +1,21 @@
const express = require("express");
const AuthRoutes = require("./routes/auth");
const TicketsRoutes = require("./routes/tickets");
const MessagesRoutes = require("./routes/messages");
const ContactsRoutes = require("./routes/contacts");
const WhatsRoutes = require("./routes/whatsapp");
const UsersRoutes = require("./routes/users");
const SettingsRoutes = require("./routes/settings");
const routes = express.Router();
routes.use("/auth", AuthRoutes);
routes.use(TicketsRoutes);
routes.use(MessagesRoutes);
routes.use(ContactsRoutes);
routes.use(WhatsRoutes);
routes.use(UsersRoutes);
routes.use(SettingsRoutes);
module.exports = routes;

View File

@@ -0,0 +1,16 @@
const express = require("express");
const SessionController = require("../../controllers/SessionController");
const UserController = require("../../controllers/UserController");
const isAuth = require("../../middleware/is-auth");
const routes = express.Router();
routes.post("/signup", UserController.store);
routes.post("/login", SessionController.store);
routes.get("/check", isAuth, (req, res) => {
res.status(200).json({ authenticated: true });
});
module.exports = routes;

View File

@@ -1,8 +1,8 @@
const express = require("express");
const isAuth = require("../middleware/is-auth");
const isAuth = require("../../middleware/is-auth");
const ContactController = require("../controllers/ContactController");
const ImportPhoneContactsController = require("../controllers/ImportPhoneContactsController");
const ContactController = require("../../controllers/ContactController");
const ImportPhoneContactsController = require("../../controllers/ImportPhoneContactsController");
const routes = express.Router();

View File

@@ -1,7 +1,7 @@
const express = require("express");
const isAuth = require("../middleware/is-auth");
const isAuth = require("../../middleware/is-auth");
const MessageController = require("../controllers/MessageController");
const MessageController = require("../../controllers/MessageController");
const routes = express.Router();

View File

@@ -0,0 +1,14 @@
const express = require("express");
const isAuth = require("../../middleware/is-auth");
const SettingController = require("../../controllers/SettingController");
const routes = express.Router();
routes.get("/settings", isAuth, SettingController.index);
// routes.get("/settings/:settingKey", isAuth, SettingsController.show);
routes.put("/settings/:settingKey", isAuth, SettingController.update);
module.exports = routes;

View File

@@ -1,7 +1,7 @@
const express = require("express");
const isAuth = require("../middleware/is-auth");
const isAuth = require("../../middleware/is-auth");
const TicketController = require("../controllers/TicketController");
const TicketController = require("../../controllers/TicketController");
const routes = express.Router();

View File

@@ -0,0 +1,18 @@
const express = require("express");
const isAuth = require("../../middleware/is-auth");
const UserController = require("../../controllers/UserController");
const routes = express.Router();
routes.get("/users", isAuth, UserController.index);
routes.post("/users", isAuth, UserController.store);
routes.put("/users/:userId", isAuth, UserController.update);
routes.get("/users/:userId", isAuth, UserController.show);
routes.delete("/users/:userId", isAuth, UserController.delete);
module.exports = routes;

View File

@@ -0,0 +1,18 @@
const express = require("express");
const isAuth = require("../../middleware/is-auth");
const WhatsAppController = require("../../controllers/WhatsAppController");
const routes = express.Router();
routes.get("/whatsapp/", isAuth, WhatsAppController.index);
routes.post("/whatsapp/", isAuth, WhatsAppController.store);
routes.get("/whatsapp/:whatsappId", isAuth, WhatsAppController.show);
routes.put("/whatsapp/:whatsappId", isAuth, WhatsAppController.update);
routes.delete("/whatsapp/:whatsappId", isAuth, WhatsAppController.delete);
module.exports = routes;

View File

@@ -0,0 +1,20 @@
const express = require("express");
const isAuth = require("../../middleware/is-auth");
const WhatsAppSessionController = require("../../controllers/WhatsAppSessionController");
const routes = express.Router();
routes.get(
"/whatsappsession/:whatsappId",
isAuth,
WhatsAppSessionController.show
);
routes.delete(
"/whatsappsession/:whatsappId",
isAuth,
WhatsAppSessionController.delete
);
module.exports = routes;

View File

@@ -1,13 +0,0 @@
const express = require("express");
const SessionController = require("../controllers/SessionController");
const isAuth = require("../middleware/is-auth");
const routes = express.Router();
routes.post("/login", SessionController.store);
routes.get("/check", isAuth, (req, res) => {
res.status(200).json({ authenticated: true });
});
module.exports = routes;

View File

@@ -1,36 +0,0 @@
const express = require("express");
const { body } = require("express-validator");
const User = require("../models/User");
const isAuth = require("../middleware/is-auth");
const UserController = require("../controllers/UserController");
const routes = express.Router();
routes.get("/users", isAuth, UserController.index);
routes.post(
"/users",
[
body("email")
.isEmail()
.withMessage("Email inválido")
.custom((value, { req }) => {
return User.findOne({ where: { email: value } }).then(user => {
if (user) {
return Promise.reject("An user with this email already exists!");
}
});
})
.normalizeEmail(),
body("password").trim().isLength({ min: 5 }),
body("name").trim().not().isEmpty(),
],
UserController.store
);
routes.put("/users/:userId", isAuth, UserController.update);
routes.delete("/users/:userId", isAuth, UserController.delete);
module.exports = routes;

View File

@@ -1,23 +0,0 @@
const express = require("express");
const isAuth = require("../middleware/is-auth");
const WhatsAppSessionController = require("../controllers/WhatsAppSessionController");
const routes = express.Router();
routes.get(
"/whatsapp/session/:sessionId",
isAuth,
WhatsAppSessionController.show
);
routes.delete(
"/whatsapp/session/:sessionId",
isAuth,
WhatsAppSessionController.delete
);
// fetch contacts in user cellphone, not in use
// routes.get("/whatsapp/contacts", isAuth, WhatsappController.getContacts);
module.exports = routes;

View File

@@ -7,9 +7,10 @@ const Sentry = require("@sentry/node");
const Contact = require("../models/Contact");
const Ticket = require("../models/Ticket");
const Message = require("../models/Message");
const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
const { getWbot, init } = require("../libs/wbot");
const { getWbot, initWbot } = require("../libs/wbot");
const verifyContact = async (msgContact, profilePicUrl) => {
let contact = await Contact.findOne({
@@ -29,7 +30,7 @@ const verifyContact = async (msgContact, profilePicUrl) => {
return contact;
};
const verifyTicket = async contact => {
const verifyTicket = async (contact, whatsappId) => {
let ticket = await Ticket.findOne({
where: {
status: {
@@ -57,6 +58,7 @@ const verifyTicket = async contact => {
ticket = await Ticket.create({
contactId: contact.id,
status: "pending",
whatsappId,
});
}
@@ -132,12 +134,14 @@ const handleMessage = async (msg, ticket, contact) => {
});
};
const wbotMessageListener = () => {
const wbot = getWbot();
const wbotMessageListener = whatsapp => {
const whatsappId = whatsapp.id;
const wbot = getWbot(whatsappId);
const io = getIO();
wbot.on("message_create", async msg => {
// console.log(msg);
if (
msg.from === "status@broadcast" ||
msg.type === "location" ||
@@ -158,7 +162,7 @@ const wbotMessageListener = () => {
const profilePicUrl = await msgContact.getProfilePicUrl();
const contact = await verifyContact(msgContact, profilePicUrl);
const ticket = await verifyTicket(contact);
const ticket = await verifyTicket(contact, whatsappId);
//return if message was already created by messageController
if (msg.fromMe) {

View File

@@ -3,17 +3,18 @@ const Sentry = require("@sentry/node");
const wbotMessageListener = require("./wbotMessageListener");
const { getIO } = require("../libs/socket");
const { getWbot, init } = require("../libs/wbot");
const { getWbot, initWbot } = require("../libs/wbot");
const wbotMonitor = dbSession => {
const wbotMonitor = whatsapp => {
const io = getIO();
const wbot = getWbot();
const sessionName = whatsapp.name;
const wbot = getWbot(whatsapp.id);
try {
wbot.on("change_state", async newState => {
console.log("monitor", newState);
console.log("Monitor session:", sessionName, newState);
try {
await dbSession.update({ status: newState });
await whatsapp.update({ status: newState });
} catch (err) {
Sentry.captureException(err);
console.log(err);
@@ -21,16 +22,18 @@ const wbotMonitor = dbSession => {
io.emit("session", {
action: "update",
session: dbSession,
session: whatsapp,
});
});
wbot.on("change_battery", async batteryInfo => {
const { battery, plugged } = batteryInfo;
console.log(`Battery: ${battery}% - Charging? ${plugged}`);
console.log(
`Battery session: ${sessionName} ${battery}% - Charging? ${plugged}`
);
try {
await dbSession.update({ battery, plugged });
await whatsapp.update({ battery, plugged });
} catch (err) {
Sentry.captureException(err);
console.log(err);
@@ -38,30 +41,30 @@ const wbotMonitor = dbSession => {
io.emit("session", {
action: "update",
session: dbSession,
session: whatsapp,
});
});
wbot.on("disconnected", async reason => {
console.log("disconnected", reason);
console.log("Disconnected session:", sessionName, reason);
try {
await dbSession.update({ status: "disconnected" });
await whatsapp.update({ status: "disconnected" });
} catch (err) {
Sentry.captureException(err);
console.log(err);
}
io.emit("session", {
action: "logout",
session: dbSession,
action: "update",
session: whatsapp,
});
setTimeout(
() =>
init()
.then(({ dbSession }) => {
wbotMessageListener();
wbotMonitor(dbSession);
initWbot(whatsapp)
.then(() => {
wbotMessageListener(whatsapp);
wbotMonitor(whatsapp);
})
.catch(err => {
Sentry.captureException(err);

View File

@@ -2,6 +2,13 @@
# yarn lockfile v1
"@babel/runtime@^7.10.5":
version "7.11.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
dependencies:
regenerator-runtime "^0.13.4"
"@pedroslopez/moduleraid@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@pedroslopez/moduleraid/-/moduleraid-4.1.0.tgz#468f7195fddc9f367e672ace9269f0698cf4c404"
@@ -854,14 +861,6 @@ express-async-errors@^3.1.1:
resolved "https://registry.yarnpkg.com/express-async-errors/-/express-async-errors-3.1.1.tgz#6053236d61d21ddef4892d6bd1d736889fc9da41"
integrity sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==
express-validator@^6.5.0:
version "6.6.1"
resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-6.6.1.tgz#c53046f615d27fcb78be786e018dcd60bd9c6c5c"
integrity sha512-+MrZKJ3eGYXkNF9p9Zf7MS7NkPJFg9MDYATU5c80Cf4F62JdLBIjWxy6481tRC0y1NnC9cgOw8FuN364bWaGhA==
dependencies:
lodash "^4.17.19"
validator "^13.1.1"
express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@@ -950,6 +949,11 @@ find-up@^3.0.0:
dependencies:
locate-path "^3.0.0"
fn-name@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c"
integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@@ -1382,6 +1386,11 @@ locate-path@^3.0.0:
p-locate "^3.0.0"
path-exists "^3.0.0"
lodash-es@^4.17.11:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
@@ -1417,7 +1426,7 @@ lodash.once@^4.0.0:
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.5:
lodash@^4.17.15, lodash@^4.17.5:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
@@ -1801,6 +1810,11 @@ progress@^2.0.1:
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
property-expr@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910"
integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -1941,6 +1955,11 @@ redeyed@~2.1.0:
dependencies:
esprima "~4.0.0"
regenerator-runtime@^0.13.4:
version "0.13.7"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
registry-auth-token@^4.0.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da"
@@ -2266,6 +2285,11 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
synchronous-promise@^2.0.13:
version "2.0.13"
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.13.tgz#9d8c165ddee69c5a6542862b405bc50095926702"
integrity sha512-R9N6uDkVsghHePKh1TEqbnLddO2IY25OcsksyFp/qBe7XYd0PVbKEWxhcdMhpLzE1I6skj5l4aEZ3CRxcbArlA==
tar-fs@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5"
@@ -2332,6 +2356,11 @@ toposort-class@^1.0.1:
resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988"
integrity sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=
toposort@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
@@ -2464,11 +2493,6 @@ validator@^10.11.0:
resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228"
integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==
validator@^13.1.1:
version "13.1.1"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.1.1.tgz#f8811368473d2173a9d8611572b58c5783f223bf"
integrity sha512-8GfPiwzzRoWTg7OV1zva1KvrSemuMkv07MA9TTl91hfhe+wKrsrgVN4H2QSFd/U/FhiU3iWPYVgvbsOGwhyFWw==
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@@ -2614,3 +2638,16 @@ youch@^2.0.10:
cookie "^0.3.1"
mustache "^3.0.0"
stack-trace "0.0.10"
yup@^0.29.3:
version "0.29.3"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.3.tgz#69a30fd3f1c19f5d9e31b1cf1c2b851ce8045fea"
integrity sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ==
dependencies:
"@babel/runtime" "^7.10.5"
fn-name "~3.0.0"
lodash "^4.17.15"
lodash-es "^4.17.11"
property-expr "^2.0.2"
synchronous-promise "^2.0.13"
toposort "^2.0.2"

View File

@@ -25,7 +25,8 @@
"react-scripts": "3.4.1",
"react-toastify": "^6.0.8",
"recharts": "^1.8.5",
"socket.io-client": "^2.3.0"
"socket.io-client": "^2.3.0",
"yup": "^0.29.3"
},
"scripts": {
"start": "react-scripts start",

View File

@@ -10,8 +10,18 @@ const App = () => {
const theme = createMuiTheme(
{
scrollbarStyles: {
"&::-webkit-scrollbar": {
width: "8px",
height: "8px",
},
"&::-webkit-scrollbar-thumb": {
boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
backgroundColor: "#e8e8e8",
},
},
palette: {
primary: { main: "#1976d2" },
primary: { main: "#2576d2" },
},
},
locale

View File

@@ -48,14 +48,7 @@ const useStyles = makeStyles(theme => ({
padding: "8px 0px 8px 8px",
height: "100%",
overflowY: "scroll",
"&::-webkit-scrollbar": {
width: "8px",
height: "8px",
},
"&::-webkit-scrollbar-thumb": {
boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
backgroundColor: "#e8e8e8",
},
...theme.scrollbarStyles,
},
contactAvatar: {
@@ -80,17 +73,6 @@ const useStyles = makeStyles(theme => ({
padding: 8,
display: "flex",
flexDirection: "column",
// overflowX: "scroll",
// flex: 1,
// "&::-webkit-scrollbar": {
// width: "8px",
// height: "8px",
// },
// "&::-webkit-scrollbar-thumb": {
// // borderRadius: "2px",
// boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
// backgroundColor: "#e8e8e8",
// },
},
contactExtraInfo: {
marginTop: 4,

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect } from "react";
import { Formik, FieldArray } from "formik";
import * as Yup from "yup";
import { Formik, FieldArray, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
@@ -52,6 +54,15 @@ const useStyles = makeStyles(theme => ({
},
}));
const ContactSchema = Yup.object().shape({
name: Yup.string()
.min(2, "Too Short!")
.max(50, "Too Long!")
.required("Required"),
number: Yup.string().min(8, "Too Short!").max(50, "Too Long!"),
email: Yup.string().email("Invalid email"),
});
const ContactModal = ({ open, onClose, contactId }) => {
const classes = useStyles();
@@ -66,8 +77,15 @@ const ContactModal = ({ open, onClose, contactId }) => {
useEffect(() => {
const fetchContact = async () => {
if (!contactId) return;
const res = await api.get(`/contacts/${contactId}`);
setContact(res.data);
try {
const { data } = await api.get(`/contacts/${contactId}`);
setContact(data);
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
fetchContact();
@@ -85,78 +103,70 @@ const ContactModal = ({ open, onClose, contactId }) => {
} else {
await api.post("/contacts", values);
}
toast.success("Contact saved sucessfully!");
} catch (err) {
alert(err.response.data.error);
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
handleClose();
};
return (
<div className={classes.root}>
<Dialog
open={open}
onClose={handleClose}
maxWidth="lg"
scroll="paper"
className={classes.modal}
>
<Dialog open={open} onClose={handleClose} maxWidth="lg" scroll="paper">
<DialogTitle id="form-dialog-title">
{contactId
? `${i18n.t("contactModal.title.edit")}`
: `${i18n.t("contactModal.title.add")}`}
</DialogTitle>
<Formik
initialValues={contact}
enableReinitialize={true}
onSubmit={(values, { setSubmitting }) => {
validationSchema={ContactSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveContact(values);
setSubmitting(false);
actions.setSubmitting(false);
}, 400);
}}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
}) => (
<form onSubmit={handleSubmit}>
<DialogTitle id="form-dialog-title">
{contactId
? `${i18n.t("contactModal.title.edit")}`
: `${i18n.t("contactModal.title.add")}`}
</DialogTitle>
{({ values, errors, touched, isSubmitting }) => (
<Form>
<DialogContent dividers>
<Typography variant="subtitle1" gutterBottom>
{i18n.t("contactModal.form.mainInfo")}
</Typography>
<TextField
<Field
as={TextField}
label={i18n.t("contactModal.form.name")}
name="name"
value={values.name || ""}
onChange={handleChange}
autoFocus
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
required
className={classes.textField}
/>
<TextField
<Field
as={TextField}
label={i18n.t("contactModal.form.number")}
name="number"
value={values.number || ""}
onChange={handleChange}
error={touched.number && Boolean(errors.number)}
helperText={touched.number && errors.number}
placeholder="5513912344321"
variant="outlined"
margin="dense"
required
/>
<div>
<TextField
<Field
as={TextField}
label={i18n.t("contactModal.form.email")}
name="email"
value={values.email || ""}
onChange={handleChange}
placeholder="Endereço de Email"
error={touched.email && Boolean(errors.email)}
helperText={touched.email && errors.email}
placeholder="Email address"
fullWidth
margin="dense"
variant="outlined"
@@ -179,25 +189,21 @@ const ContactModal = ({ open, onClose, contactId }) => {
className={classes.extraAttr}
key={`${index}-info`}
>
<TextField
<Field
as={TextField}
label={i18n.t("contactModal.form.extraName")}
name={`extraInfo[${index}].name`}
value={info.name || ""}
onChange={handleChange}
variant="outlined"
margin="dense"
required
className={classes.textField}
/>
<TextField
<Field
as={TextField}
label={i18n.t("contactModal.form.extraValue")}
name={`extraInfo[${index}].value`}
value={info.value || ""}
onChange={handleChange}
variant="outlined"
margin="dense"
className={classes.textField}
required
/>
<IconButton
size="small"
@@ -248,7 +254,7 @@ const ContactModal = ({ open, onClose, contactId }) => {
)}
</Button>
</DialogActions>
</form>
</Form>
)}
</Formik>
</Dialog>

View File

@@ -1,163 +0,0 @@
import React from "react";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import Skeleton from "@material-ui/lab/Skeleton";
const ContactsSekeleton = () => {
return (
<>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={90} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={55} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={60} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={100} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={90} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={55} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={60} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={100} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={90} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={55} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={60} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={100} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={90} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={55} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={60} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={100} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={90} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={55} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={60} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={100} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</>
);
};
export default ContactsSekeleton;

View File

@@ -0,0 +1,31 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container";
const useStyles = makeStyles(theme => ({
mainContainer: {
flex: 1,
padding: theme.spacing(2),
height: `calc(100% - 48px)`,
},
contentWrapper: {
height: "100%",
overflowY: "hidden",
display: "flex",
flexDirection: "column",
},
}));
const MainContainer = ({ children }) => {
const classes = useStyles();
return (
<Container className={classes.mainContainer}>
<div className={classes.contentWrapper}>{children}</div>
</Container>
);
};
export default MainContainer;

View File

@@ -0,0 +1,19 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
contactsHeader: {
display: "flex",
alignItems: "center",
padding: "0px 6px 6px 6px",
},
}));
const MainHeader = ({ children }) => {
const classes = useStyles();
return <div className={classes.contactsHeader}>{children}</div>;
};
export default MainHeader;

View File

@@ -0,0 +1,21 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
MainHeaderButtonsWrapper: {
flex: "none",
marginLeft: "auto",
"& > *": {
margin: theme.spacing(1),
},
},
}));
const MainHeaderButtonsWrapper = ({ children }) => {
const classes = useStyles();
return <div className={classes.MainHeaderButtonsWrapper}>{children}</div>;
};
export default MainHeaderButtonsWrapper;

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
import "emoji-mart/css/emoji-mart.css";
import { useParams } from "react-router-dom";
import { Picker } from "emoji-mart";
import { toast } from "react-toastify";
import MicRecorder from "mic-recorder-to-mp3";
import { makeStyles } from "@material-ui/core/styles";
@@ -163,8 +164,10 @@ const MessageInput = () => {
try {
await api.post(`/messages/${ticketId}`, formData);
} catch (err) {
alert(err.response.data.error);
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
setLoading(false);
setMedia(mediaInitialState);
@@ -182,8 +185,10 @@ const MessageInput = () => {
try {
await api.post(`/messages/${ticketId}`, message);
} catch (err) {
alert(err.response.data.error);
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
setInputMessage("");
setShowEmoji(false);
@@ -224,8 +229,10 @@ const MessageInput = () => {
try {
await api.post(`/messages/${ticketId}`, formData);
} catch (err) {
alert(err.response.data.error);
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
setRecording(false);
setLoading(false);

View File

@@ -101,15 +101,7 @@ const useStyles = makeStyles(theme => ({
padding: "20px 20px 20px 20px",
// scrollBehavior: "smooth",
overflowY: "scroll",
"&::-webkit-scrollbar": {
width: "8px",
height: "8px",
},
"&::-webkit-scrollbar-thumb": {
// borderRadius: "2px",
boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
backgroundColor: "#e8e8e8",
},
...theme.scrollbarStyles,
},
circleLoading: {
@@ -311,8 +303,12 @@ const MessagesList = () => {
}
} catch (err) {
console.log(err);
toast.error("Ticket não encontrado");
history.push("/chat");
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
if (err.response.status === 404) {
history.push("/tickets");
}
}
}
};
fetchMessages();
@@ -425,8 +421,11 @@ const MessagesList = () => {
});
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
history.push("/chat");
history.push("/tickets");
};
const handleDrawerOpen = () => {

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from "react";
import { useHistory } from "react-router-dom";
import { toast } from "react-toastify";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
@@ -38,7 +39,7 @@ const useStyles = makeStyles(theme => ({
},
}));
const NewTicketModal = ({ modalOpen, onClose, contactId }) => {
const NewTicketModal = ({ modalOpen, onClose }) => {
const history = useHistory();
const classes = useStyles();
const userId = +localStorage.getItem("userId");
@@ -54,18 +55,21 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => {
const delayDebounceFn = setTimeout(() => {
const fetchContacts = async () => {
try {
const res = await api.get("contacts", {
params: { searchParam, rowsPerPage: 20 },
const { data } = await api.get("contacts", {
params: { searchParam },
});
setOptions(res.data.contacts);
setOptions(data.contacts);
setLoading(false);
} catch (err) {
alert(err);
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
fetchContacts();
}, 1000);
}, 500);
return () => clearTimeout(delayDebounceFn);
}, [searchParam, modalOpen]);
@@ -85,9 +89,12 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => {
userId: userId,
status: "open",
});
history.push(`/chat/${ticket.id}`);
history.push(`/tickets/${ticket.id}`);
} catch (err) {
alert(err);
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
setLoading(false);
handleClose();
@@ -107,13 +114,14 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => {
</DialogTitle>
<DialogContent dividers>
<Autocomplete
id="asynchronous-demo"
id="contacts-finder"
style={{ width: 300 }}
getOptionLabel={option => `${option.name} - ${option.number}`}
onChange={(e, newValue) => {
setSelectedContact(newValue);
}}
options={options}
noOptionsText="No contacts found. Try another term."
loading={loading}
renderInput={params => (
<TextField

View File

@@ -24,14 +24,7 @@ const useStyles = makeStyles(theme => ({
tabContainer: {
overflowY: "auto",
maxHeight: 350,
"&::-webkit-scrollbar": {
width: "8px",
height: "8px",
},
"&::-webkit-scrollbar-thumb": {
boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
backgroundColor: "#e8e8e8",
},
...theme.scrollbarStyles,
},
popoverPaper: {
width: "100%",
@@ -123,7 +116,7 @@ const NotificationsPopOver = () => {
notification.onclick = function (event) {
event.preventDefault(); //
window.open(`/chat/${ticket.id}`, "_self");
window.open(`/tickets/${ticket.id}`, "_self");
};
document.addEventListener("visibilitychange", () => {
@@ -144,7 +137,7 @@ const NotificationsPopOver = () => {
}, [setIsOpen]);
const handleSelectTicket = (e, ticket) => {
history.push(`/chat/${ticket.id}`);
history.push(`/tickets/${ticket.id}`);
handleClickAway();
};

View File

@@ -1,71 +0,0 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import FirstPageIcon from "@material-ui/icons/FirstPage";
import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft";
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight";
import LastPageIcon from "@material-ui/icons/LastPage";
import IconButton from "@material-ui/core/IconButton";
const useStyles = makeStyles(theme => ({
root: {
flexShrink: 0,
marginLeft: theme.spacing(2.5),
},
}));
const PaginationActions = ({ count, page, rowsPerPage, onChangePage }) => {
const classes = useStyles();
const handleFirstPageButtonClick = event => {
onChangePage(event, 0);
};
const handleBackButtonClick = event => {
onChangePage(event, page - 1);
};
const handleNextButtonClick = event => {
onChangePage(event, page + 1);
};
const handleLastPageButtonClick = event => {
onChangePage(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};
return (
<div className={classes.root}>
<IconButton
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
{<FirstPageIcon />}
</IconButton>
<IconButton
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
{<KeyboardArrowLeft />}
</IconButton>
<IconButton
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
{<KeyboardArrowRight />}
</IconButton>
<IconButton
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
{<LastPageIcon />}
</IconButton>
</div>
);
};
export default PaginationActions;

View File

@@ -1,18 +0,0 @@
import React from "react";
import QRCode from "qrcode.react";
import Typography from "@material-ui/core/Typography";
import { i18n } from "../../translate/i18n";
const Qrcode = ({ qrCode }) => {
return (
<div>
<Typography color="primary" gutterBottom>
{i18n.t("qrCode.message")}
</Typography>
{qrCode ? <QRCode value={qrCode} size={256} /> : <span>loading</span>}
</div>
);
};
export default Qrcode;

View File

@@ -0,0 +1,65 @@
import React, { useEffect, useState } from "react";
import QRCode from "qrcode.react";
import openSocket from "socket.io-client";
import { toast } from "react-toastify";
import { Dialog, DialogContent, Paper, Typography } from "@material-ui/core";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
const QrcodeModal = ({ open, onClose, whatsAppId }) => {
const [qrCode, setQrCode] = useState("");
useEffect(() => {
const fetchSession = async () => {
try {
const { data } = await api.get(`/whatsapp/${whatsAppId}`);
setQrCode(data.qrcode);
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
fetchSession();
}, [whatsAppId]);
useEffect(() => {
if (!whatsAppId) return;
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("whatsappSession", data => {
if (data.action === "update" && data.session.id === whatsAppId) {
setQrCode(data.session.qrcode);
}
if (data.action === "update" && data.session.qrcode === "") {
onClose();
}
});
return () => {
socket.disconnect();
};
}, [whatsAppId, onClose]);
return (
<Dialog open={open} onClose={onClose} maxWidth="lg" scroll="paper">
<DialogContent>
<Paper elevation={0}>
<Typography color="primary" gutterBottom>
{i18n.t("qrCode.message")}
</Typography>
{qrCode ? (
<QRCode value={qrCode} size={256} />
) : (
<span>Waiting for QR Code</span>
)}
</Paper>
</DialogContent>
</Dialog>
);
};
export default React.memo(QrcodeModal);

View File

@@ -2,6 +2,7 @@ import React from "react";
import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button";
import { format, parseISO } from "date-fns";
import { toast } from "react-toastify";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
@@ -9,9 +10,12 @@ import api from "../../services/api";
const SessionInfo = ({ session }) => {
const handleDisconectSession = async () => {
try {
await api.delete("/whatsapp/session/1");
await api.put(`/whatsapp/session/${session.id}`);
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
@@ -23,7 +27,7 @@ const SessionInfo = ({ session }) => {
<Typography variant="body2" gutterBottom>
{`${i18n.t("sessionInfo.updatedAt")}`}{" "}
{session.updatedAt &&
format(parseISO(session.updatedAt), "dd/mm/yy HH:mm")}
format(parseISO(session.updatedAt), "dd/MM/yy HH:mm")}
</Typography>
<Button
color="primary"

View File

@@ -0,0 +1,78 @@
import React from "react";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import Skeleton from "@material-ui/lab/Skeleton";
const TableRowSkeleton = () => {
return (
<>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell></TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell></TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell></TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell></TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell></TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</>
);
};
export default TableRowSkeleton;

View File

@@ -100,11 +100,11 @@ const TicketListItem = ({ ticket }) => {
} catch (err) {
alert(err);
}
history.push(`/chat/${ticketId}`);
history.push(`/tickets/${ticketId}`);
};
const handleSelectTicket = (e, ticket) => {
history.push(`/chat/${ticket.id}`);
history.push(`/tickets/${ticket.id}`);
};
return (

View File

@@ -18,7 +18,7 @@ const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => {
try {
await api.delete(`/tickets/${ticket.id}`);
toast.success("Ticket deletado com sucesso.");
history.push("/chat");
history.push("/tickets");
} catch (err) {
toast.error("Erro ao deletar o ticket");
}

View File

@@ -23,14 +23,7 @@ const useStyles = makeStyles(theme => ({
ticketsList: {
flex: 1,
overflowY: "scroll",
"&::-webkit-scrollbar": {
width: "8px",
height: "8px",
},
"&::-webkit-scrollbar-thumb": {
boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
backgroundColor: "#e8e8e8",
},
...theme.scrollbarStyles,
borderTop: "2px solid rgba(0, 0, 0, 0.12)",
},

View File

@@ -0,0 +1,10 @@
import React from "react";
import Typography from "@material-ui/core/Typography";
export default function Title(props) {
return (
<Typography variant="h5" color="primary" gutterBottom>
{props.children}
</Typography>
);
}

View File

@@ -0,0 +1,224 @@
import React, { useState, useEffect } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import CircularProgress from "@material-ui/core/CircularProgress";
import Select from "@material-ui/core/Select";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
// import { i18n } from "../../translate/i18n";
import api from "../../services/api";
const useStyles = makeStyles(theme => ({
root: {
display: "flex",
flexWrap: "wrap",
},
textField: {
// marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
// width: "25ch",
flex: 1,
},
btnWrapper: {
// margin: theme.spacing(1),
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
}));
const UserSchema = Yup.object().shape({
name: Yup.string()
.min(2, "Too Short!")
.max(50, "Too Long!")
.required("Required"),
password: Yup.string().min(5, "Too Short!").max(50, "Too Long!"),
email: Yup.string().email("Invalid email").required("Required"),
});
const UserModal = ({ open, onClose, userId }) => {
const classes = useStyles();
const initialState = {
name: "",
email: "",
password: "",
profile: "user",
};
const [user, setUser] = useState(initialState);
useEffect(() => {
const fetchUser = async () => {
if (!userId) return;
try {
const { data } = await api.get(`/users/${userId}`);
setUser(prevState => {
return { ...prevState, ...data };
});
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
fetchUser();
}, [userId, open]);
const handleClose = () => {
onClose();
setUser(initialState);
};
const handleSaveUser = async values => {
try {
if (userId) {
await api.put(`/users/${userId}`, values);
} else {
await api.post("/users", values);
}
toast.success("Success!");
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
handleClose();
};
return (
<div className={classes.root}>
<Dialog open={open} onClose={handleClose} maxWidth="lg" scroll="paper">
<DialogTitle id="form-dialog-title">
{userId ? `Edit User` : `New User`}
</DialogTitle>
<Formik
initialValues={user}
enableReinitialize={true}
validationSchema={UserSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveUser(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting }) => (
<Form>
<DialogContent dividers>
<Field
as={TextField}
label="Name"
autoFocus
name="name"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
className={classes.textField}
/>
<Field
as={TextField}
label="Email"
name="email"
error={touched.email && Boolean(errors.email)}
helperText={touched.email && errors.email}
variant="outlined"
margin="dense"
/>
<div>
<Field
as={TextField}
label="New Password"
type="password"
name="password"
error={touched.password && Boolean(errors.password)}
helperText={touched.password && errors.password}
variant="outlined"
margin="dense"
/>
<FormControl
variant="outlined"
className={classes.formControl}
margin="dense"
>
<InputLabel id="profile-selection-input-label">
Profile
</InputLabel>
<Field
as={Select}
label="Profile"
name="profile"
labelId="profile-selection-label"
id="profile-selection"
required
>
<MenuItem value="admin">Admin</MenuItem>
<MenuItem value="user">User</MenuItem>
</Field>
</FormControl>
</div>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
Cancel
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{"Ok"}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default UserModal;

View File

@@ -0,0 +1,182 @@
import React, { useState, useEffect } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import {
Dialog,
DialogContent,
DialogTitle,
Button,
DialogActions,
CircularProgress,
TextField,
Switch,
FormControlLabel,
} from "@material-ui/core";
// import { i18n } from "../../translate/i18n";
import api from "../../services/api";
const useStyles = makeStyles(theme => ({
form: {
display: "flex",
alignItems: "center",
justifySelf: "center",
"& > *": {
margin: theme.spacing(1),
},
},
textField: {
flex: 1,
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
}));
const SessionSchema = Yup.object().shape({
name: Yup.string()
.min(2, "Too Short!")
.max(50, "Too Long!")
.required("Required"),
});
const WhatsAppModal = ({ open, onClose, whatsAppId }) => {
const classes = useStyles();
const initialState = {
name: "",
default: false,
};
const [whatsApp, setWhatsApp] = useState(initialState);
useEffect(() => {
const fetchSession = async () => {
if (!whatsAppId) return;
try {
const { data } = await api.get(`whatsapp/${whatsAppId}`);
setWhatsApp(data);
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
fetchSession();
}, [whatsAppId]);
const handleSaveWhatsApp = async values => {
try {
if (whatsAppId) {
await api.put(`/whatsapp/${whatsAppId}`, {
name: values.name,
default: values.default,
});
} else {
await api.post("/whatsapp", values);
}
toast.success("Success!");
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
handleClose();
};
const handleClose = () => {
onClose();
setWhatsApp(initialState);
};
return (
<Dialog open={open} onClose={handleClose} maxWidth="lg" scroll="paper">
<DialogTitle>WhatsApp</DialogTitle>
<Formik
initialValues={whatsApp}
enableReinitialize={true}
validationSchema={SessionSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveWhatsApp(values);
// alert(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
}, 400);
}}
>
{({ values, touched, errors, isSubmitting }) => (
<Form>
<DialogContent dividers className={classes.form}>
<Field
as={TextField}
label="Name"
autoFocus
name="name"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
className={classes.textField}
/>
<FormControlLabel
control={
<Field
as={Switch}
color="primary"
name="default"
checked={values.default}
/>
}
label="Default"
/>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
Cancel
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
Save
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
);
};
export default React.memo(WhatsAppModal);

View File

@@ -38,16 +38,17 @@ function ListItemLink(props) {
}
const MainListItems = () => {
const userProfile = localStorage.getItem("profile");
return (
<div>
<ListItemLink to="/" primary="Dashboard" icon={<DashboardIcon />} />
<ListItemLink
to="/whats-auth"
primary={i18n.t("mainDrawer.listItems.connection")}
to="/connections"
primary="Connections"
icon={<SyncAltIcon />}
/>
<ListItemLink
to="/chat"
to="/tickets"
primary={i18n.t("mainDrawer.listItems.tickets")}
icon={<WhatsAppIcon />}
/>
@@ -57,18 +58,22 @@ const MainListItems = () => {
primary={i18n.t("mainDrawer.listItems.contacts")}
icon={<ContactPhoneIcon />}
/>
<Divider />
<ListSubheader inset>Administration</ListSubheader>
<ListItemLink
to="/chat"
primary={i18n.t("mainDrawer.listItems.users")}
icon={<GroupIcon />}
/>
<ListItemLink
to="/chat"
primary={i18n.t("mainDrawer.listItems.settings")}
icon={<SettingsIcon />}
/>
{userProfile === "admin" && (
<>
<Divider />
<ListSubheader inset>Administration</ListSubheader>
<ListItemLink
to="/users"
primary={i18n.t("mainDrawer.listItems.users")}
icon={<GroupIcon />}
/>
<ListItemLink
to="/settings"
primary={i18n.t("mainDrawer.listItems.settings")}
icon={<SettingsIcon />}
/>
</>
)}
</div>
);
};

View File

@@ -35,7 +35,10 @@ const useAuth = () => {
} catch (err) {
setLoading(false);
setIsAuth(false);
toast.error(i18n.t("auth.toasts.fail"));
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
checkAuth();
@@ -44,16 +47,20 @@ const useAuth = () => {
const handleLogin = async (e, user) => {
e.preventDefault();
try {
const res = await api.post("/auth/login", user);
localStorage.setItem("token", JSON.stringify(res.data.token));
localStorage.setItem("username", res.data.username);
localStorage.setItem("userId", res.data.userId);
api.defaults.headers.Authorization = `Bearer ${res.data.token}`;
const { data } = await api.post("/auth/login", user);
localStorage.setItem("token", JSON.stringify(data.token));
localStorage.setItem("username", data.username);
localStorage.setItem("profile", data.profile);
localStorage.setItem("userId", data.userId);
api.defaults.headers.Authorization = `Bearer ${data.token}`;
setIsAuth(true);
toast.success(i18n.t("auth.toasts.success"));
history.push("/chat");
history.push("/tickets");
} catch (err) {
toast.error(i18n.t("auth.toasts.fail"));
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
@@ -62,6 +69,7 @@ const useAuth = () => {
setIsAuth(false);
localStorage.removeItem("token");
localStorage.removeItem("username");
localStorage.removeItem("profile");
localStorage.removeItem("userId");
api.defaults.headers.Authorization = undefined;
history.push("/login");

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useReducer } from "react";
import openSocket from "socket.io-client";
import { toast } from "react-toastify";
import api from "../../services/api";
@@ -97,6 +98,9 @@ const useTickets = ({ searchParam, pageNumber, status, date, showAll }) => {
setLoading(false);
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
fetchTickets();

View File

@@ -0,0 +1,297 @@
import React, { useState, useEffect, useReducer, useCallback } from "react";
import openSocket from "socket.io-client";
import { toast } from "react-toastify";
import { format, parseISO } from "date-fns";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import {
Button,
TableBody,
TableRow,
TableCell,
IconButton,
Table,
TableHead,
Paper,
} from "@material-ui/core";
import { Edit, DeleteOutline, CheckCircle } from "@material-ui/icons";
import MainContainer from "../../components/MainContainer";
import MainHeader from "../../components/MainHeader";
import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
import Title from "../../components/Title";
import TableRowSkeleton from "../../components/TableRowSkeleton";
import api from "../../services/api";
import WhatsAppModal from "../../components/WhatsAppModal";
import ConfirmationModal from "../../components/ConfirmationModal";
import QrcodeModal from "../../components/QrcodeModal";
const reducer = (state, action) => {
if (action.type === "LOAD_WHATSAPPS") {
const whatsApps = action.payload;
return [...whatsApps];
}
if (action.type === "UPDATE_WHATSAPPS") {
const whatsApp = action.payload;
const whatsAppIndex = state.findIndex(s => s.id === whatsApp.id);
if (whatsAppIndex !== -1) {
state[whatsAppIndex] = whatsApp;
return [...state];
} else {
return [whatsApp, ...state];
}
}
if (action.type === "UPDATE_SESSION") {
const whatsApp = action.payload;
const whatsAppIndex = state.findIndex(s => s.id === whatsApp.id);
if (whatsAppIndex !== -1) {
state[whatsAppIndex].status = whatsApp.status;
state[whatsAppIndex].qrcode = whatsApp.qrcode;
return [...state];
} else {
return [...state];
}
}
if (action.type === "DELETE_WHATSAPPS") {
const whatsAppId = action.payload;
const whatsAppIndex = state.findIndex(s => s.id === whatsAppId);
if (whatsAppIndex !== -1) {
state.splice(whatsAppIndex, 1);
}
return [...state];
}
if (action.type === "RESET") {
return [];
}
};
const useStyles = makeStyles(theme => ({
mainPaper: {
flex: 1,
padding: theme.spacing(1),
overflowY: "scroll",
...theme.scrollbarStyles,
},
}));
const Connections = () => {
const classes = useStyles();
const [whatsApps, dispatch] = useReducer(reducer, []);
const [whatsAppModalOpen, setWhatsAppModalOpen] = useState(false);
const [qrModalOpen, setQrModalOpen] = useState(false);
const [selectedWhatsApp, setSelectedWhatsApp] = useState(null);
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
const [deletingWhatsApp, setDeletingWhatsApp] = useState(null);
useEffect(() => {
const fetchSession = async () => {
try {
const { data } = await api.get("/whatsapp/");
dispatch({ type: "LOAD_WHATSAPPS", payload: data });
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
fetchSession();
}, []);
useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("whatsapp", data => {
if (data.action === "update") {
dispatch({ type: "UPDATE_WHATSAPPS", payload: data.whatsapp });
}
});
socket.on("whatsapp", data => {
if (data.action === "delete") {
dispatch({ type: "DELETE_WHATSAPPS", payload: data.whatsappId });
}
});
socket.on("whatsappSession", data => {
if (data.action === "update") {
dispatch({ type: "UPDATE_SESSION", payload: data.session });
}
});
return () => {
socket.disconnect();
};
}, []);
// const handleDisconnectSession = async whatsAppId => {
// try {
// await api.put(`/whatsapp/whatsApp/${whatsAppId}`, {
// whatsApp: "",
// status: "pending",
// });
// } catch (err) {
// console.log(err);
// if (err.response && err.response.data && err.response.data.error) {
// toast.error(err.response.data.error);
// }
// }
// };
const handleOpenWhatsAppModal = () => {
setSelectedWhatsApp(null);
setWhatsAppModalOpen(true);
};
const handleCloseWhatsAppModal = useCallback(() => {
setWhatsAppModalOpen(false);
setSelectedWhatsApp(null);
}, [setSelectedWhatsApp, setWhatsAppModalOpen]);
const handleOpenQrModal = whatsApp => {
setSelectedWhatsApp(whatsApp);
setQrModalOpen(true);
};
const handleCloseQrModal = useCallback(() => {
setQrModalOpen(false);
setSelectedWhatsApp(null);
}, [setQrModalOpen, setSelectedWhatsApp]);
const handleEditWhatsApp = whatsApp => {
setSelectedWhatsApp(whatsApp);
setWhatsAppModalOpen(true);
};
const handleDeleteWhatsApp = async whatsAppId => {
try {
await api.delete(`/whatsapp/${whatsAppId}`);
toast.success("Deleted!");
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
setDeletingWhatsApp(null);
};
return (
<MainContainer>
<ConfirmationModal
title={deletingWhatsApp && `Delete ${deletingWhatsApp.name}?`}
open={confirmModalOpen}
setOpen={setConfirmModalOpen}
onConfirm={() => handleDeleteWhatsApp(deletingWhatsApp.id)}
>
Are you sure? It cannot be reverted.
</ConfirmationModal>
<QrcodeModal
open={qrModalOpen}
onClose={handleCloseQrModal}
whatsAppId={
selectedWhatsApp && !whatsAppModalOpen && selectedWhatsApp.id
}
/>
<WhatsAppModal
open={whatsAppModalOpen}
onClose={handleCloseWhatsAppModal}
whatsAppId={selectedWhatsApp && !qrModalOpen && selectedWhatsApp.id}
/>
<MainHeader>
<Title>Connections</Title>
<MainHeaderButtonsWrapper>
<Button
variant="contained"
color="primary"
onClick={handleOpenWhatsAppModal}
>
Add Whatsapp
</Button>
</MainHeaderButtonsWrapper>
</MainHeader>
<Paper className={classes.mainPaper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
<TableCell align="center">Name</TableCell>
<TableCell align="center">Status</TableCell>
<TableCell align="center">Last update</TableCell>
<TableCell align="center">Default</TableCell>
<TableCell align="center">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{false ? (
<TableRowSkeleton />
) : (
<>
{whatsApps &&
whatsApps.length > 0 &&
whatsApps.map((whatsApp, index) => (
<TableRow key={whatsApp.id}>
<TableCell align="center">{whatsApp.name}</TableCell>
<TableCell align="center">
{whatsApp.status === "qrcode" ? (
<Button
size="small"
variant="contained"
color="primary"
onClick={() => handleOpenQrModal(whatsApp)}
>
QR CODE
</Button>
) : (
whatsApp.status
)}
</TableCell>
<TableCell align="center">
{format(parseISO(whatsApp.updatedAt), "dd/MM/yy HH:mm")}
</TableCell>
<TableCell align="center">
{whatsApp.default && (
<CheckCircle style={{ color: green[500] }} />
)}
</TableCell>
<TableCell align="center">
<IconButton
size="small"
onClick={() => handleEditWhatsApp(whatsApp)}
>
<Edit />
</IconButton>
<IconButton
size="small"
onClick={e => {
setConfirmModalOpen(true);
setDeletingWhatsApp(whatsApp);
}}
>
<DeleteOutline />
</IconButton>
</TableCell>
</TableRow>
))}
</>
)}
</TableBody>
</Table>
</Paper>
</MainContainer>
);
};
export default Connections;

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useReducer } from "react";
import openSocket from "socket.io-client";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
@@ -10,113 +11,129 @@ import TableRow from "@material-ui/core/TableRow";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import Avatar from "@material-ui/core/Avatar";
import TableFooter from "@material-ui/core/TableFooter";
import TablePagination from "@material-ui/core/TablePagination";
import SearchIcon from "@material-ui/icons/Search";
import TextField from "@material-ui/core/TextField";
import Container from "@material-ui/core/Container";
import InputAdornment from "@material-ui/core/InputAdornment";
import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import EditIcon from "@material-ui/icons/Edit";
import PaginationActions from "../../components/PaginationActions";
import api from "../../services/api";
import ContactsSekeleton from "../../components/ContactsSekeleton";
import TableRowSkeleton from "../../components/TableRowSkeleton";
import ContactModal from "../../components/ContactModal";
import ConfirmationModal from "../../components/ConfirmationModal/";
import { i18n } from "../../translate/i18n";
import MainHeader from "../../components/MainHeader";
import Title from "../../components/Title";
import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
import MainContainer from "../../components/MainContainer";
const reducer = (state, action) => {
if (action.type === "LOAD_CONTACTS") {
const contacts = action.payload;
const newContacts = [];
contacts.forEach(contact => {
const contactIndex = state.findIndex(c => c.id === contact.id);
if (contactIndex !== -1) {
state[contactIndex] = contact;
} else {
newContacts.push(contact);
}
});
return [...state, ...newContacts];
}
if (action.type === "UPDATE_CONTACTS") {
const contact = action.payload;
const contactIndex = state.findIndex(c => c.id === contact.id);
if (contactIndex !== -1) {
state[contactIndex] = contact;
return [...state];
} else {
return [contact, ...state];
}
}
if (action.type === "DELETE_CONTACT") {
const contactId = action.payload;
const contactIndex = state.findIndex(c => c.id === contactId);
if (contactIndex !== -1) {
state.splice(contactIndex, 1);
}
return [...state];
}
if (action.type === "RESET") {
return [];
}
};
const useStyles = makeStyles(theme => ({
mainContainer: {
flex: 1,
padding: theme.spacing(2),
height: `calc(100% - 48px)`,
},
contentWrapper: {
height: "100%",
overflowY: "hidden",
display: "flex",
flexDirection: "column",
},
contactsHeader: {
display: "flex",
alignItems: "center",
padding: "0px 6px 6px 6px",
},
actionButtons: {
flex: "none",
marginLeft: "auto",
"& > *": {
margin: theme.spacing(1),
},
},
mainPaper: {
flex: 1,
padding: theme.spacing(2),
padding: theme.spacing(1),
overflowY: "scroll",
"&::-webkit-scrollbar": {
width: "8px",
height: "8px",
},
"&::-webkit-scrollbar-thumb": {
boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
backgroundColor: "#e8e8e8",
},
...theme.scrollbarStyles,
},
}));
const Contacts = () => {
const classes = useStyles();
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(false);
const [pageNumber, setPageNumber] = useState(1);
const [searchParam, setSearchParam] = useState("");
const [contacts, setContacts] = useState([]);
const [contacts, dispatch] = useReducer(reducer, []);
const [selectedContactId, setSelectedContactId] = useState(null);
const [contactModalOpen, setContactModalOpen] = useState(false);
const [deletingContact, setDeletingContact] = useState(null);
const [confirmOpen, setConfirmOpen] = useState(false);
const [hasMore, setHasMore] = useState(false);
useEffect(() => {
dispatch({ type: "RESET" });
setPageNumber(1);
}, [searchParam]);
useEffect(() => {
setLoading(true);
const delayDebounceFn = setTimeout(() => {
const fetchContacts = async () => {
try {
const res = await api.get("/contacts/", {
params: { searchParam, pageNumber: page + 1, rowsPerPage },
const { data } = await api.get("/contacts/", {
params: { searchParam, pageNumber },
});
setContacts(res.data.contacts);
setCount(res.data.count);
dispatch({ type: "LOAD_CONTACTS", payload: data.contacts });
setHasMore(data.hasMore);
setLoading(false);
} catch (err) {
console.log(err);
alert(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
fetchContacts();
}, 500);
return () => clearTimeout(delayDebounceFn);
}, [searchParam, page, rowsPerPage]);
}, [searchParam, pageNumber]);
useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("contact", data => {
if (data.action === "update" || data.action === "create") {
updateContacts(data.contact);
dispatch({ type: "UPDATE_CONTACTS", payload: data.contact });
}
if (data.action === "delete") {
deleteContact(data.contactId);
dispatch({ type: "DELETE_CONTACT", payload: +data.contactId });
}
});
@@ -125,40 +142,6 @@ const Contacts = () => {
};
}, []);
const updateContacts = contact => {
setContacts(prevState => {
const contactIndex = prevState.findIndex(c => c.id === contact.id);
if (contactIndex === -1) {
return [contact, ...prevState];
}
const aux = [...prevState];
aux[contactIndex] = contact;
return aux;
});
};
const deleteContact = contactId => {
setContacts(prevState => {
const contactIndex = prevState.findIndex(c => c.id === +contactId);
if (contactIndex === -1) return prevState;
const aux = [...prevState];
aux.splice(contactIndex, 1);
return aux;
});
};
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = event => {
setRowsPerPage(+event.target.value);
setPage(0);
};
const handleSearch = event => {
setSearchParam(event.target.value.toLowerCase());
};
@@ -181,24 +164,42 @@ const Contacts = () => {
const handleDeleteContact = async contactId => {
try {
await api.delete(`/contacts/${contactId}`);
toast.success("Contact deleted sucessfully!");
} catch (err) {
alert(err);
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
setDeletingContact(null);
setSearchParam("");
setPage(0);
setPageNumber(1);
};
const handleimportContact = async () => {
try {
await api.post("/contacts/import");
window.location.reload(false);
} catch (err) {
console.log(err);
window.location.reload(false);
}
};
const loadMore = () => {
setPageNumber(prevState => prevState + 1);
};
const handleScroll = e => {
if (!hasMore || loading) return;
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
if (scrollHeight - (scrollTop + 100) < clientHeight) {
loadMore();
}
};
return (
<Container className={classes.mainContainer}>
<MainContainer className={classes.mainContainer}>
<ContactModal
open={contactModalOpen}
onClose={handleCloseContactModal}
@@ -225,112 +226,91 @@ const Contacts = () => {
? `${i18n.t("contacts.confirmationModal.deleteMessage")}`
: `${i18n.t("contacts.confirmationModal.importMessage")}`}
</ConfirmationModal>
<div className={classes.contentWrapper}>
<div className={classes.contactsHeader}>
<Typography variant="h5" gutterBottom>
{i18n.t("contacts.title")}
</Typography>
<MainHeader>
<Title>{i18n.t("contacts.title")}</Title>
<MainHeaderButtonsWrapper>
<TextField
placeholder={i18n.t("contacts.searchPlaceholder")}
type="search"
value={searchParam}
onChange={handleSearch}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{ color: "gray" }} />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
onClick={e => setConfirmOpen(true)}
>
{i18n.t("contacts.buttons.import")}
</Button>
<Button
variant="contained"
color="primary"
onClick={handleOpenContactModal}
>
{i18n.t("contacts.buttons.add")}
</Button>
</MainHeaderButtonsWrapper>
</MainHeader>
<Paper
className={classes.mainPaper}
variant="outlined"
onScroll={handleScroll}
>
<Table size="small">
<TableHead>
<TableRow>
<TableCell padding="checkbox" />
<TableCell>{i18n.t("contacts.table.name")}</TableCell>
<TableCell>{i18n.t("contacts.table.whatsapp")}</TableCell>
<TableCell>{i18n.t("contacts.table.email")}</TableCell>
<TableCell align="right">
{i18n.t("contacts.table.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<>
{contacts.map(contact => (
<TableRow key={contact.id}>
<TableCell style={{ paddingRight: 0 }}>
{<Avatar src={contact.profilePicUrl} />}
</TableCell>
<TableCell>{contact.name}</TableCell>
<TableCell>{contact.number}</TableCell>
<TableCell>{contact.email}</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={() => hadleEditContact(contact.id)}
>
<EditIcon />
</IconButton>
<div className={classes.actionButtons}>
<TextField
placeholder={i18n.t("contacts.searchPlaceholder")}
type="search"
value={searchParam}
onChange={handleSearch}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{ color: "gray" }} />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
onClick={e => setConfirmOpen(true)}
>
{i18n.t("contacts.buttons.import")}
</Button>
<Button
variant="contained"
color="primary"
onClick={handleOpenContactModal}
>
{i18n.t("contacts.buttons.add")}
</Button>
</div>
</div>
<Paper className={classes.mainPaper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
<TableCell padding="checkbox" />
<TableCell>{i18n.t("contacts.table.name")}</TableCell>
<TableCell>{i18n.t("contacts.table.whatsapp")}</TableCell>
<TableCell>{i18n.t("contacts.table.email")}</TableCell>
<TableCell align="right">
{i18n.t("contacts.table.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loading ? (
<ContactsSekeleton />
) : (
<>
{contacts.map(contact => (
<TableRow key={contact.id}>
<TableCell style={{ paddingRight: 0 }}>
{<Avatar src={contact.profilePicUrl} />}
</TableCell>
<TableCell>{contact.name}</TableCell>
<TableCell>{contact.number}</TableCell>
<TableCell>{contact.email}</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={() => hadleEditContact(contact.id)}
>
<EditIcon />
</IconButton>
<IconButton
size="small"
onClick={e => {
setConfirmOpen(true);
setDeletingContact(contact);
}}
>
<DeleteOutlineIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</>
)}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
colSpan={5}
count={count}
rowsPerPage={rowsPerPage}
page={page}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
ActionsComponent={PaginationActions}
/>
</TableRow>
</TableFooter>
</Table>
</Paper>
</div>
</Container>
<IconButton
size="small"
onClick={e => {
setConfirmOpen(true);
setDeletingContact(contact);
}}
>
<DeleteOutlineIcon />
</IconButton>
</TableCell>
</TableRow>
))}
{loading && <TableRowSkeleton />}
</>
</TableBody>
</Table>
</Paper>
</MainContainer>
);
};

View File

@@ -1,10 +1,12 @@
import React from "react";
import Typography from "@material-ui/core/Typography";
export default function Title(props) {
const Title = props => {
return (
<Typography component="h2" variant="h6" color="primary" gutterBottom>
{props.children}
</Typography>
);
}
};
export default Title;

View File

@@ -0,0 +1,124 @@
import React, { useState, useEffect } from "react";
import openSocket from "socket.io-client";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import Typography from "@material-ui/core/Typography";
import Container from "@material-ui/core/Container";
import Select from "@material-ui/core/Select";
import { toast } from "react-toastify";
import api from "../../services/api";
const useStyles = makeStyles(theme => ({
root: {
display: "flex",
alignItems: "center",
padding: theme.spacing(4),
},
paper: {
padding: theme.spacing(2),
display: "flex",
alignItems: "center",
},
settingOption: {
marginLeft: "auto",
},
margin: {
margin: theme.spacing(1),
},
}));
const Settings = () => {
const classes = useStyles();
const [settings, setSettings] = useState([]);
useEffect(() => {
const fetchSession = async () => {
try {
const { data } = await api.get("/settings");
setSettings(data);
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
fetchSession();
}, []);
useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("settings", data => {
if (data.action === "update") {
// dispatch({ type: "UPDATE_USERS", payload: data.user });
setSettings(prevState => {
const aux = [...prevState];
const settingIndex = aux.findIndex(s => s.key === data.setting.key);
aux[settingIndex].value = data.setting.value;
return aux;
});
}
});
return () => {
socket.disconnect();
};
}, []);
const handleChangeSetting = async e => {
const selectedValue = e.target.value;
const settingKey = e.target.name;
try {
await api.put(`/settings/${settingKey}`, {
value: selectedValue,
});
toast.success("Setting updated");
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
const getSettingValue = key => {
const { value } = settings.find(s => s.key === key);
return value;
};
return (
<div className={classes.root}>
<Container className={classes.container} maxWidth="sm">
<Typography variant="body2" gutterBottom>
Settings
</Typography>
<Paper className={classes.paper}>
<Typography variant="body1">User creation</Typography>
<Select
margin="dense"
variant="outlined"
native
id="userCreation-setting"
name="userCreation"
value={
settings && settings.length > 0 && getSettingValue("userCreation")
}
className={classes.settingOption}
onChange={handleChangeSetting}
>
<option value="enabled">Enabled</option>
<option value="disabled">Disabled</option>
</Select>
</Paper>
</Container>
</div>
);
};
export default Settings;

View File

@@ -1,8 +1,10 @@
import React, { useState } from "react";
import * as Yup from "yup";
import { useHistory } from "react-router-dom";
import { Link as RouterLink } from "react-router-dom";
import { toast } from "react-toastify";
import { Formik, Form, Field } from "formik";
import Avatar from "@material-ui/core/Avatar";
import Button from "@material-ui/core/Button";
@@ -53,24 +55,33 @@ const useStyles = makeStyles(theme => ({
},
}));
const UserSchema = Yup.object().shape({
name: Yup.string()
.min(2, "Too Short!")
.max(50, "Too Long!")
.required("Required"),
password: Yup.string().min(5, "Too Short!").max(50, "Too Long!"),
email: Yup.string().email("Invalid email").required("Required"),
});
const SignUp = () => {
const classes = useStyles();
const history = useHistory();
const [user, setUser] = useState({ name: "", email: "", password: "" });
const initialState = { name: "", email: "", password: "" };
const handleChangeInput = e => {
setUser({ ...user, [e.target.name]: e.target.value });
};
const [user] = useState(initialState);
const handleSignUp = async e => {
e.preventDefault();
const handleSignUp = async values => {
try {
await api.post("/users", user);
await api.post("/auth/signup", values);
toast.success(i18n.t("signup.toasts.success"));
history.push("/login");
} catch (err) {
toast.error(i18n.t("signup.toasts.fail"));
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
@@ -84,68 +95,88 @@ const SignUp = () => {
<Typography component="h1" variant="h5">
{i18n.t("signup.title")}
</Typography>
<form className={classes.form} noValidate onSubmit={handleSignUp}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
autoComplete="name"
name="name"
variant="outlined"
required
fullWidth
id="name"
label={i18n.t("signup.form.name")}
value={user.name}
onChange={handleChangeInput}
autoFocus
/>
</Grid>
{/* <form className={classes.form} noValidate onSubmit={handleSignUp}> */}
<Formik
initialValues={user}
enableReinitialize={true}
validationSchema={UserSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSignUp(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting }) => (
<Form className={classes.form}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Field
as={TextField}
autoComplete="name"
name="name"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
fullWidth
id="name"
label={i18n.t("signup.form.name")}
autoFocus
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
<Grid item xs={12}>
<Field
as={TextField}
variant="outlined"
fullWidth
id="email"
label={i18n.t("signup.form.email")}
name="email"
error={touched.email && Boolean(errors.email)}
helperText={touched.email && errors.email}
autoComplete="email"
/>
</Grid>
<Grid item xs={12}>
<Field
as={TextField}
variant="outlined"
fullWidth
name="password"
error={touched.password && Boolean(errors.password)}
helperText={touched.password && errors.password}
label={i18n.t("signup.form.password")}
type="password"
id="password"
autoComplete="current-password"
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
id="email"
label={i18n.t("signup.form.email")}
name="email"
autoComplete="email"
value={user.email}
onChange={handleChangeInput}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
name="password"
label={i18n.t("signup.form.password")}
type="password"
id="password"
autoComplete="current-password"
value={user.password}
onChange={handleChangeInput}
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
{i18n.t("signup.buttons.submit")}
</Button>
<Grid container justify="flex-end">
<Grid item>
<Link href="#" variant="body2" component={RouterLink} to="/login">
{i18n.t("signup.buttons.login")}
</Link>
</Grid>
</Grid>
</form>
variant="contained"
color="primary"
className={classes.submit}
>
{i18n.t("signup.buttons.submit")}
</Button>
<Grid container justify="flex-end">
<Grid item>
<Link
href="#"
variant="body2"
component={RouterLink}
to="/login"
>
{i18n.t("signup.buttons.login")}
</Link>
</Grid>
</Grid>
</Form>
)}
</Formik>
</div>
<Box mt={5}>
<Copyright />

View File

@@ -0,0 +1,282 @@
import React, { useState, useEffect, useReducer } from "react";
import { toast } from "react-toastify";
import openSocket from "socket.io-client";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import IconButton from "@material-ui/core/IconButton";
import SearchIcon from "@material-ui/icons/Search";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import EditIcon from "@material-ui/icons/Edit";
import MainContainer from "../../components/MainContainer";
import MainHeader from "../../components/MainHeader";
import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
import Title from "../../components/Title";
import api from "../../services/api";
import TableRowSkeleton from "../../components/TableRowSkeleton";
import UserModal from "../../components/UserModal";
import ConfirmationModal from "../../components/ConfirmationModal";
const reducer = (state, action) => {
if (action.type === "LOAD_USERS") {
const users = action.payload;
const newUsers = [];
users.forEach(user => {
const userIndex = state.findIndex(u => u.id === user.id);
if (userIndex !== -1) {
state[userIndex] = user;
} else {
newUsers.push(user);
}
});
return [...state, ...newUsers];
}
if (action.type === "UPDATE_USERS") {
const user = action.payload;
const userIndex = state.findIndex(u => u.id === user.id);
if (userIndex !== -1) {
state[userIndex] = user;
return [...state];
} else {
return [user, ...state];
}
}
if (action.type === "DELETE_USER") {
const userId = action.payload;
const userIndex = state.findIndex(u => u.id === userId);
if (userIndex !== -1) {
state.splice(userIndex, 1);
}
return [...state];
}
if (action.type === "RESET") {
return [];
}
};
const useStyles = makeStyles(theme => ({
mainPaper: {
flex: 1,
padding: theme.spacing(1),
overflowY: "scroll",
...theme.scrollbarStyles,
},
}));
const Users = () => {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [pageNumber, setPageNumber] = useState(1);
const [hasMore, setHasMore] = useState(false);
const [selectedUser, setSelectedUser] = useState(null);
const [deletingUser, setDeletingUser] = useState(null);
const [userModalOpen, setUserModalOpen] = useState(false);
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
const [searchParam, setSearchParam] = useState("");
const [users, dispatch] = useReducer(reducer, []);
useEffect(() => {
dispatch({ type: "RESET" });
setPageNumber(1);
}, [searchParam]);
useEffect(() => {
setLoading(true);
const delayDebounceFn = setTimeout(() => {
const fetchUsers = async () => {
try {
const { data } = await api.get("/users/", {
params: { searchParam, pageNumber },
});
dispatch({ type: "LOAD_USERS", payload: data.users });
setHasMore(data.hasMore);
setLoading(false);
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
fetchUsers();
}, 500);
return () => clearTimeout(delayDebounceFn);
}, [searchParam, pageNumber]);
useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("user", data => {
if (data.action === "update" || data.action === "create") {
dispatch({ type: "UPDATE_USERS", payload: data.user });
}
if (data.action === "delete") {
dispatch({ type: "DELETE_USER", payload: +data.userId });
}
});
return () => {
socket.disconnect();
};
}, []);
const handleOpenUserModal = () => {
setSelectedUser(null);
setUserModalOpen(true);
};
const handleCloseUserModal = () => {
setSelectedUser(null);
setUserModalOpen(false);
};
const handleSearch = event => {
setSearchParam(event.target.value.toLowerCase());
};
const handleEditUser = user => {
setSelectedUser(user);
setUserModalOpen(true);
};
const handleDeleteUser = async userId => {
try {
await api.delete(`/users/${userId}`);
toast.success("User deleted!");
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
setDeletingUser(null);
setSearchParam("");
setPageNumber(1);
};
const loadMore = () => {
setPageNumber(prevState => prevState + 1);
};
const handleScroll = e => {
if (!hasMore || loading) return;
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
if (scrollHeight - (scrollTop + 100) < clientHeight) {
loadMore();
}
};
return (
<MainContainer>
<ConfirmationModal
title={deletingUser && `Delete ${deletingUser.name}?`}
open={confirmModalOpen}
setOpen={setConfirmModalOpen}
onConfirm={e => handleDeleteUser(deletingUser.id)}
>
Are you sure? It canoot be reverted.
</ConfirmationModal>
<UserModal
open={userModalOpen}
onClose={handleCloseUserModal}
aria-labelledby="form-dialog-title"
userId={selectedUser && selectedUser.id}
/>
<MainHeader>
<Title>Usuários</Title>
<MainHeaderButtonsWrapper>
<TextField
placeholder="Search..."
type="search"
value={searchParam}
onChange={handleSearch}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{ color: "gray" }} />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
onClick={handleOpenUserModal}
>
Novo Usuário
</Button>
</MainHeaderButtonsWrapper>
</MainHeader>
<Paper
className={classes.mainPaper}
variant="outlined"
onScroll={handleScroll}
>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Email</TableCell>
<TableCell>Profile</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loading ? (
<TableRowSkeleton />
) : (
<>
{users.map(user => (
<TableRow key={user.id}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.profile}</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={() => handleEditUser(user)}
>
<EditIcon />
</IconButton>
<IconButton
size="small"
onClick={e => {
setConfirmModalOpen(true);
setDeletingUser(user);
}}
>
<DeleteOutlineIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</>
)}
</TableBody>
</Table>
</Paper>
</MainContainer>
);
};
export default Users;

View File

@@ -1,104 +0,0 @@
import React, { useState, useEffect } from "react";
import { useHistory } from "react-router-dom";
import api from "../../services/api";
import openSocket from "socket.io-client";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import SessionInfo from "../../components/SessionInfo";
import Qrcode from "../../components/Qrcode";
const useStyles = makeStyles(theme => ({
root: {
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: theme.spacing(4),
},
paper: {
padding: theme.spacing(2),
display: "flex",
width: 400,
overflow: "auto",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
},
fixedHeight: {
height: 640,
},
}));
const WhatsAuth = () => {
const classes = useStyles();
const history = useHistory();
const [qrCode, setQrCode] = useState("");
const [session, setSession] = useState({});
useEffect(() => {
const fetchSession = async () => {
try {
const { data } = await api.get("/whatsapp/session/1");
setQrCode(data.qrcode);
setSession(data);
} catch (err) {
console.log(err);
}
};
fetchSession();
}, []);
useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("session", data => {
if (data.action === "update") {
setQrCode(data.qr);
setSession(data.session);
}
});
socket.on("session", data => {
if (data.action === "update") {
setSession(data.session);
}
});
socket.on("session", data => {
if (data.action === "authentication") {
setQrCode("");
setSession(data.session);
history.push("/chat");
}
});
socket.on("session", data => {
if (data.action === "logout") {
setSession(data.session);
}
});
return () => {
socket.disconnect();
};
}, [history]);
return (
<div className={classes.root}>
{session && session.status === "disconnected" ? (
<Paper className={classes.paper}>
<Qrcode qrCode={qrCode} />
</Paper>
) : (
<Paper className={classes.paper}>
<SessionInfo session={session} />
</Paper>
)}
</div>
);
};
export default WhatsAuth;

View File

@@ -4,10 +4,12 @@ import { ToastContainer } from "react-toastify";
import MainDrawer from "../components/_layout";
import Dashboard from "../pages/Dashboard/";
import Chat from "../pages/Chat/";
import Tickets from "../pages/Tickets/";
import Signup from "../pages/Signup/";
import Login from "../pages/Login/";
import WhatsAuth from "../pages/WhatsAuth/WhatsAuth";
import Connections from "../pages/Connections/";
import Settings from "../pages/Settings/";
import Users from "../pages/Users";
import Contacts from "../pages/Contacts/";
import { AuthProvider } from "../context/Auth/AuthContext";
import Route from "./Route";
@@ -21,9 +23,21 @@ const Routes = () => {
<Route exact path="/signup" component={Signup} />
<MainDrawer>
<Route exact path="/" component={Dashboard} isPrivate />
<Route exact path="/chat/:ticketId?" component={Chat} isPrivate />
<Route exact path="/whats-auth" component={WhatsAuth} isPrivate />
<Route
exact
path="/tickets/:ticketId?"
component={Tickets}
isPrivate
/>
<Route
exact
path="/connections"
component={Connections}
isPrivate
/>
<Route exact path="/contacts" component={Contacts} isPrivate />
<Route exact path="/users" component={Users} isPrivate />
<Route exact path="/Settings" component={Settings} isPrivate />
</MainDrawer>
</Switch>
<ToastContainer autoClose={3000} />

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB