feat: added users page

This commit is contained in:
canove
2020-09-02 21:01:02 -03:00
parent e0b6e9ce3f
commit dfde175c07
33 changed files with 1121 additions and 659 deletions

View File

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

View File

@@ -82,6 +82,5 @@ app.use(async (err, req, res, next) => {
console.log(err); console.log(err);
return res.status(500).json(errors); return res.status(500).json(errors);
} }
console.log(err);
return res.status(500).json({ error: "Internal server error" }); return res.status(500).json({ error: "Internal server error" });
}); });

View File

@@ -8,7 +8,7 @@ const { getIO } = require("../libs/socket");
const { getWbot } = require("../libs/wbot"); const { getWbot } = require("../libs/wbot");
exports.index = async (req, res) => { exports.index = async (req, res) => {
const { searchParam = "", pageNumber = 1, rowsPerPage = 10 } = req.query; const { searchParam = "", pageNumber = 1 } = req.query;
const whereCondition = { const whereCondition = {
[Op.or]: [ [Op.or]: [
@@ -23,7 +23,7 @@ exports.index = async (req, res) => {
], ],
}; };
let limit = +rowsPerPage; let limit = 20;
let offset = limit * (pageNumber - 1); let offset = limit * (pageNumber - 1);
const { count, rows: contacts } = await Contact.findAndCountAll({ const { count, rows: contacts } = await Contact.findAndCountAll({
@@ -33,7 +33,9 @@ exports.index = async (req, res) => {
order: [["createdAt", "DESC"]], 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) => { exports.store = async (req, res) => {

View File

@@ -16,7 +16,7 @@ exports.index = async (req, res) => {
showAll, showAll,
} = req.query; } = req.query;
const userId = req.userId; const userId = req.user.id;
const limit = 20; const limit = 20;
const offset = limit * (pageNumber - 1); const offset = limit * (pageNumber - 1);

View File

@@ -1,34 +1,101 @@
const { validationResult } = require("express-validator"); const Sequelize = require("sequelize");
const Yup = require("yup");
const { Op } = require("sequelize");
const User = require("../models/User"); const User = require("../models/User");
const { getIO } = require("../libs/socket");
exports.index = async (req, res) => { exports.index = async (req, res) => {
// const { searchParam = "", pageNumber = 1 } = req.query; const { searchParam = "", pageNumber = 1, rowsPerPage = 10 } = req.query;
const users = await User.findAll({ attributes: ["name", "id", "email"] }); const whereCondition = {
[Op.or]: [
{
name: Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("name")),
"LIKE",
"%" + searchParam.toLowerCase() + "%"
),
},
{ email: { [Op.like]: `%${searchParam.toLowerCase()}%` } },
],
};
return res.status(200).json(users); let limit = +rowsPerPage;
let offset = limit * (pageNumber - 1);
const { count, rows: users } = await User.findAndCountAll({
attributes: ["name", "id", "email", "profile"],
where: whereCondition,
limit,
offset,
order: [["createdAt", "DESC"]],
});
return res.status(200).json({ users, count });
}; };
exports.store = async (req, res, next) => { exports.store = async (req, res, next) => {
const errors = validationResult(req); 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()) { await schema.validate(req.body);
return res
.status(400)
.json({ error: "Validation failed", data: errors.array() });
}
const { name, id, email } = await User.create(req.body); const io = getIO();
res.status(201).json({ message: "User created!", userId: id }); 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.show = async (req, res) => {
const { userId } = req.params;
const { id, name, email, profile } = await User.findByPk(userId);
return res.status(200).json({
id,
name,
email,
profile,
});
}; };
exports.update = async (req, res) => { exports.update = async (req, res) => {
const schema = Yup.object().shape({
name: Yup.string().min(2),
email: Yup.string().email(),
password: Yup.string(),
});
console.log("cai aqui");
await schema.validate(req.body);
const io = getIO();
const { userId } = req.params; const { userId } = req.params;
const user = await User.findByPk(userId, { const user = await User.findByPk(userId, {
attributes: ["name", "id", "email"], attributes: ["name", "id", "email", "profile"],
}); });
if (!user) { if (!user) {
@@ -37,12 +104,16 @@ exports.update = async (req, res) => {
await user.update(req.body); 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) => { exports.delete = async (req, res) => {
const io = getIO();
const { userId } = req.params; const { userId } = req.params;
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
@@ -53,5 +124,10 @@ exports.delete = async (req, res) => {
await user.destroy(); 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,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

@@ -1,5 +1,7 @@
const jwt = require("jsonwebtoken"); const jwt = require("jsonwebtoken");
const util = require("util");
const User = require("../models/User");
const authConfig = require("../config/auth"); const authConfig = require("../config/auth");
module.exports = async (req, res, next) => { module.exports = async (req, res, next) => {
@@ -11,12 +13,24 @@ module.exports = async (req, res, next) => {
const [, token] = authHeader.split(" "); const [, token] = authHeader.split(" ");
jwt.verify(token, authConfig.secret, (error, result) => { try {
if (error) { const decoded = await util.promisify(jwt.verify)(token, authConfig.secret);
return res.status(401).json({ error: "Invalid token" });
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 req.user = user;
next();
}); return next();
} catch (err) {
console.log(err);
return res.status(401).json({ error: "Invalid Token" });
}
}; };

View File

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

View File

@@ -1,9 +1,12 @@
const express = require("express"); const express = require("express");
const SessionController = require("../controllers/SessionController"); const SessionController = require("../controllers/SessionController");
const UserController = require("../controllers/UserController");
const isAuth = require("../middleware/is-auth"); const isAuth = require("../middleware/is-auth");
const routes = express.Router(); const routes = express.Router();
routes.post("/signup", UserController.store);
routes.post("/login", SessionController.store); routes.post("/login", SessionController.store);
routes.get("/check", isAuth, (req, res) => { routes.get("/check", isAuth, (req, res) => {

View File

@@ -1,5 +1,4 @@
const express = require("express"); const express = require("express");
const { body } = require("express-validator");
const User = require("../models/User"); const User = require("../models/User");
const isAuth = require("../middleware/is-auth"); const isAuth = require("../middleware/is-auth");
@@ -9,28 +8,12 @@ const routes = express.Router();
routes.get("/users", isAuth, UserController.index); routes.get("/users", isAuth, UserController.index);
routes.post( routes.post("/users", isAuth, UserController.store);
"/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.put("/users/:userId", isAuth, UserController.update);
routes.get("/users/:userId", isAuth, UserController.show);
routes.delete("/users/:userId", isAuth, UserController.delete); routes.delete("/users/:userId", isAuth, UserController.delete);
module.exports = routes; module.exports = routes;

View File

@@ -137,7 +137,7 @@ const wbotMessageListener = () => {
const io = getIO(); const io = getIO();
wbot.on("message_create", async msg => { wbot.on("message_create", async msg => {
// console.log(msg); console.log(msg);
if ( if (
msg.from === "status@broadcast" || msg.from === "status@broadcast" ||
msg.type === "location" || msg.type === "location" ||

View File

@@ -2,6 +2,13 @@
# yarn lockfile v1 # 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": "@pedroslopez/moduleraid@^4.1.0":
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/@pedroslopez/moduleraid/-/moduleraid-4.1.0.tgz#468f7195fddc9f367e672ace9269f0698cf4c404" 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" resolved "https://registry.yarnpkg.com/express-async-errors/-/express-async-errors-3.1.1.tgz#6053236d61d21ddef4892d6bd1d736889fc9da41"
integrity sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng== 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: express@^4.17.1:
version "4.17.1" version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@@ -950,6 +949,11 @@ find-up@^3.0.0:
dependencies: dependencies:
locate-path "^3.0.0" 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: forwarded@~0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 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" p-locate "^3.0.0"
path-exists "^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: lodash.includes@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" 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" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= 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" version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
@@ -1801,6 +1810,11 @@ progress@^2.0.1:
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== 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: proto-list@~1.2.1:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -1941,6 +1955,11 @@ redeyed@~2.1.0:
dependencies: dependencies:
esprima "~4.0.0" 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: registry-auth-token@^4.0.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da" 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: dependencies:
has-flag "^4.0.0" 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: tar-fs@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" 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" resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988"
integrity sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg= 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: touch@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" 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" resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228"
integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw== 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: vary@^1, vary@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@@ -2614,3 +2638,16 @@ youch@^2.0.10:
cookie "^0.3.1" cookie "^0.3.1"
mustache "^3.0.0" mustache "^3.0.0"
stack-trace "0.0.10" 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-scripts": "3.4.1",
"react-toastify": "^6.0.8", "react-toastify": "^6.0.8",
"recharts": "^1.8.5", "recharts": "^1.8.5",
"socket.io-client": "^2.3.0" "socket.io-client": "^2.3.0",
"yup": "^0.29.3"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",

View File

@@ -10,8 +10,18 @@ const App = () => {
const theme = createMuiTheme( 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: { palette: {
primary: { main: "#1976d2" }, primary: { main: "#2576d2" },
}, },
}, },
locale locale

View File

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

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Formik, FieldArray } from "formik"; import * as Yup from "yup";
import { Formik, FieldArray, Form, Field } from "formik";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors"; import { green } from "@material-ui/core/colors";
@@ -52,6 +53,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 ContactModal = ({ open, onClose, contactId }) => {
const classes = useStyles(); const classes = useStyles();
@@ -86,7 +96,7 @@ const ContactModal = ({ open, onClose, contactId }) => {
await api.post("/contacts", values); await api.post("/contacts", values);
} }
} catch (err) { } catch (err) {
alert(err.response.data.error); alert(JSON.stringify(err.response.data, null, 2));
console.log(err); console.log(err);
} }
handleClose(); handleClose();
@@ -94,69 +104,57 @@ const ContactModal = ({ open, onClose, contactId }) => {
return ( return (
<div className={classes.root}> <div className={classes.root}>
<Dialog <Dialog open={open} onClose={handleClose} maxWidth="lg" scroll="paper">
open={open} <DialogTitle id="form-dialog-title">
onClose={handleClose} {contactId
maxWidth="lg" ? `${i18n.t("contactModal.title.edit")}`
scroll="paper" : `${i18n.t("contactModal.title.add")}`}
className={classes.modal} </DialogTitle>
>
<Formik <Formik
initialValues={contact} initialValues={contact}
enableReinitialize={true} enableReinitialize={true}
onSubmit={(values, { setSubmitting }) => { validationSchema={ContactSchema}
onSubmit={(values, actions) => {
setTimeout(() => { setTimeout(() => {
handleSaveContact(values); handleSaveContact(values);
setSubmitting(false); actions.setSubmitting(false);
}, 400); }, 400);
}} }}
> >
{({ {({ values, errors, touched, isSubmitting }) => (
values, <Form>
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>
<DialogContent dividers> <DialogContent dividers>
<Typography variant="subtitle1" gutterBottom> <Typography variant="subtitle1" gutterBottom>
{i18n.t("contactModal.form.mainInfo")} {i18n.t("contactModal.form.mainInfo")}
</Typography> </Typography>
<TextField <Field
as={TextField}
label={i18n.t("contactModal.form.name")} label={i18n.t("contactModal.form.name")}
name="name" name="name"
value={values.name || ""} error={touched.name && Boolean(errors.name)}
onChange={handleChange} helperText={touched.name && errors.name}
variant="outlined" variant="outlined"
margin="dense" margin="dense"
required
className={classes.textField} className={classes.textField}
/> />
<TextField <Field
as={TextField}
label={i18n.t("contactModal.form.number")} label={i18n.t("contactModal.form.number")}
name="number" name="number"
value={values.number || ""} error={touched.number && Boolean(errors.number)}
onChange={handleChange} helperText={touched.number && errors.number}
placeholder="5513912344321" placeholder="5513912344321"
variant="outlined" variant="outlined"
margin="dense" margin="dense"
required
/> />
<div> <div>
<TextField <Field
as={TextField}
label={i18n.t("contactModal.form.email")} label={i18n.t("contactModal.form.email")}
name="email" name="email"
value={values.email || ""} error={touched.email && Boolean(errors.email)}
onChange={handleChange} helperText={touched.email && errors.email}
placeholder="Endereço de Email" placeholder="Email address"
fullWidth fullWidth
margin="dense" margin="dense"
variant="outlined" variant="outlined"
@@ -179,25 +177,21 @@ const ContactModal = ({ open, onClose, contactId }) => {
className={classes.extraAttr} className={classes.extraAttr}
key={`${index}-info`} key={`${index}-info`}
> >
<TextField <Field
as={TextField}
label={i18n.t("contactModal.form.extraName")} label={i18n.t("contactModal.form.extraName")}
name={`extraInfo[${index}].name`} name={`extraInfo[${index}].name`}
value={info.name || ""}
onChange={handleChange}
variant="outlined" variant="outlined"
margin="dense" margin="dense"
required
className={classes.textField} className={classes.textField}
/> />
<TextField <Field
as={TextField}
label={i18n.t("contactModal.form.extraValue")} label={i18n.t("contactModal.form.extraValue")}
name={`extraInfo[${index}].value`} name={`extraInfo[${index}].value`}
value={info.value || ""}
onChange={handleChange}
variant="outlined" variant="outlined"
margin="dense" margin="dense"
className={classes.textField} className={classes.textField}
required
/> />
<IconButton <IconButton
size="small" size="small"
@@ -248,7 +242,7 @@ const ContactModal = ({ open, onClose, contactId }) => {
)} )}
</Button> </Button>
</DialogActions> </DialogActions>
</form> </Form>
)} )}
</Formik> </Formik>
</Dialog> </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

@@ -101,15 +101,7 @@ const useStyles = makeStyles(theme => ({
padding: "20px 20px 20px 20px", padding: "20px 20px 20px 20px",
// scrollBehavior: "smooth", // scrollBehavior: "smooth",
overflowY: "scroll", overflowY: "scroll",
"&::-webkit-scrollbar": { ...theme.scrollbarStyles,
width: "8px",
height: "8px",
},
"&::-webkit-scrollbar-thumb": {
// borderRadius: "2px",
boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
backgroundColor: "#e8e8e8",
},
}, },
circleLoading: { circleLoading: {

View File

@@ -24,14 +24,7 @@ const useStyles = makeStyles(theme => ({
tabContainer: { tabContainer: {
overflowY: "auto", overflowY: "auto",
maxHeight: 350, maxHeight: 350,
"&::-webkit-scrollbar": { ...theme.scrollbarStyles,
width: "8px",
height: "8px",
},
"&::-webkit-scrollbar-thumb": {
boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
backgroundColor: "#e8e8e8",
},
}, },
popoverPaper: { popoverPaper: {
width: "100%", width: "100%",

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

@@ -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

@@ -23,14 +23,7 @@ const useStyles = makeStyles(theme => ({
ticketsList: { ticketsList: {
flex: 1, flex: 1,
overflowY: "scroll", overflowY: "scroll",
"&::-webkit-scrollbar": { ...theme.scrollbarStyles,
width: "8px",
height: "8px",
},
"&::-webkit-scrollbar-thumb": {
boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
backgroundColor: "#e8e8e8",
},
borderTop: "2px solid rgba(0, 0, 0, 0.12)", 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,212 @@
import React, { useState, useEffect } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
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;
const { data } = await api.get(`/users/${userId}`);
setUser(prevState => {
return { ...prevState, ...data };
});
};
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);
}
} catch (err) {
alert(JSON.stringify(err.response.data, null, 2));
console.log(err);
}
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"
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

@@ -60,12 +60,12 @@ const MainListItems = () => {
<Divider /> <Divider />
<ListSubheader inset>Administration</ListSubheader> <ListSubheader inset>Administration</ListSubheader>
<ListItemLink <ListItemLink
to="/chat" to="/users"
primary={i18n.t("mainDrawer.listItems.users")} primary={i18n.t("mainDrawer.listItems.users")}
icon={<GroupIcon />} icon={<GroupIcon />}
/> />
<ListItemLink <ListItemLink
to="/chat" to="/settings"
primary={i18n.t("mainDrawer.listItems.settings")} primary={i18n.t("mainDrawer.listItems.settings")}
icon={<SettingsIcon />} icon={<SettingsIcon />}
/> />

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useReducer } from "react";
import openSocket from "socket.io-client"; import openSocket from "socket.io-client";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
@@ -10,93 +10,100 @@ import TableRow from "@material-ui/core/TableRow";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Avatar from "@material-ui/core/Avatar"; 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 SearchIcon from "@material-ui/icons/Search";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import Container from "@material-ui/core/Container";
import InputAdornment from "@material-ui/core/InputAdornment"; import InputAdornment from "@material-ui/core/InputAdornment";
import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"; import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import EditIcon from "@material-ui/icons/Edit"; import EditIcon from "@material-ui/icons/Edit";
import PaginationActions from "../../components/PaginationActions";
import api from "../../services/api"; import api from "../../services/api";
import ContactsSekeleton from "../../components/ContactsSekeleton"; import TableRowSkeleton from "../../components/TableRowSkeleton";
import ContactModal from "../../components/ContactModal"; import ContactModal from "../../components/ContactModal";
import ConfirmationModal from "../../components/ConfirmationModal/"; import ConfirmationModal from "../../components/ConfirmationModal/";
import { i18n } from "../../translate/i18n"; 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_CONTACT") {
const updatedContact = action.payload;
const contactIndex = state.findIndex(c => c.id === updatedContact.id);
if (contactIndex !== -1) {
state[contactIndex] = updatedContact;
}
return [...state];
}
if (action.type === "DELETE_CONTACT") {
const contactId = action.payload;
console.log("cai aqui", contactId);
const contactIndex = state.findIndex(c => c.id === contactId);
if (contactIndex !== -1) {
console.log("cai no if");
state.splice(contactIndex, 1);
}
return [...state];
}
};
const useStyles = makeStyles(theme => ({ 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: { mainPaper: {
flex: 1, flex: 1,
padding: theme.spacing(2), padding: theme.spacing(1),
overflowY: "scroll", overflowY: "scroll",
"&::-webkit-scrollbar": { ...theme.scrollbarStyles,
width: "8px",
height: "8px",
},
"&::-webkit-scrollbar-thumb": {
boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
backgroundColor: "#e8e8e8",
},
}, },
})); }));
const Contacts = () => { const Contacts = () => {
const classes = useStyles(); const classes = useStyles();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(false);
const [page, setPage] = useState(0); const [pageNumber, setPageNumber] = useState(1);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [count, setCount] = useState(0);
const [searchParam, setSearchParam] = useState(""); const [searchParam, setSearchParam] = useState("");
const [contacts, setContacts] = useState([]); const [contacts, dispatch] = useReducer(reducer, []);
const [selectedContactId, setSelectedContactId] = useState(null); const [selectedContactId, setSelectedContactId] = useState(null);
const [contactModalOpen, setContactModalOpen] = useState(false); const [contactModalOpen, setContactModalOpen] = useState(false);
const [deletingContact, setDeletingContact] = useState(null); const [deletingContact, setDeletingContact] = useState(null);
const [confirmOpen, setConfirmOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false);
const [hasMore, setHasMore] = useState(false);
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
const delayDebounceFn = setTimeout(() => { const delayDebounceFn = setTimeout(() => {
const fetchContacts = async () => { const fetchContacts = async () => {
try { try {
const res = await api.get("/contacts/", { const { data } = await api.get("/contacts/", {
params: { searchParam, pageNumber: page + 1, rowsPerPage }, params: { searchParam, pageNumber },
}); });
setContacts(res.data.contacts); dispatch({ type: "LOAD_CONTACTS", payload: data.contacts });
setCount(res.data.count); setHasMore(data.hasMore);
setLoading(false); setLoading(false);
} catch (err) { } catch (err) {
console.log(err); console.log(err);
@@ -106,17 +113,17 @@ const Contacts = () => {
fetchContacts(); fetchContacts();
}, 500); }, 500);
return () => clearTimeout(delayDebounceFn); return () => clearTimeout(delayDebounceFn);
}, [searchParam, page, rowsPerPage]); }, [searchParam, pageNumber]);
useEffect(() => { useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL); const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("contact", data => { socket.on("contact", data => {
if (data.action === "update" || data.action === "create") { if (data.action === "update" || data.action === "create") {
updateContacts(data.contact); dispatch({ type: "UPDATE_CONTACT", payload: data.contact });
} }
if (data.action === "delete") { if (data.action === "delete") {
deleteContact(data.contactId); dispatch({ type: "DELETE_CONTACT", payload: +data.contactId });
} }
}); });
@@ -125,40 +132,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 => { const handleSearch = event => {
setSearchParam(event.target.value.toLowerCase()); setSearchParam(event.target.value.toLowerCase());
}; };
@@ -186,7 +159,7 @@ const Contacts = () => {
} }
setDeletingContact(null); setDeletingContact(null);
setSearchParam(""); setSearchParam("");
setPage(0); setPageNumber(1);
}; };
const handleimportContact = async () => { const handleimportContact = async () => {
@@ -197,8 +170,20 @@ const Contacts = () => {
} }
}; };
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 ( return (
<Container className={classes.mainContainer}> <MainContainer className={classes.mainContainer}>
<ContactModal <ContactModal
open={contactModalOpen} open={contactModalOpen}
onClose={handleCloseContactModal} onClose={handleCloseContactModal}
@@ -225,112 +210,91 @@ const Contacts = () => {
? `${i18n.t("contacts.confirmationModal.deleteMessage")}` ? `${i18n.t("contacts.confirmationModal.deleteMessage")}`
: `${i18n.t("contacts.confirmationModal.importMessage")}`} : `${i18n.t("contacts.confirmationModal.importMessage")}`}
</ConfirmationModal> </ConfirmationModal>
<div className={classes.contentWrapper}> <MainHeader>
<div className={classes.contactsHeader}> <Title>{i18n.t("contacts.title")}</Title>
<Typography variant="h5" gutterBottom> <MainHeaderButtonsWrapper>
{i18n.t("contacts.title")} <TextField
</Typography> 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}> <IconButton
<TextField size="small"
placeholder={i18n.t("contacts.searchPlaceholder")} onClick={e => {
type="search" setConfirmOpen(true);
value={searchParam} setDeletingContact(contact);
onChange={handleSearch} }}
InputProps={{ >
startAdornment: ( <DeleteOutlineIcon />
<InputAdornment position="start"> </IconButton>
<SearchIcon style={{ color: "gray" }} /> </TableCell>
</InputAdornment> </TableRow>
), ))}
}} {loading && <TableRowSkeleton />}
/> </>
<Button </TableBody>
variant="contained" </Table>
color="primary" </Paper>
onClick={e => setConfirmOpen(true)} </MainContainer>
>
{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>
); );
}; };

View File

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

View File

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

View File

@@ -0,0 +1,234 @@
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 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 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 [selectedUserId, setSelectedUserId] = useState(null);
const [userModalOpen, setUserModalOpen] = useState(false);
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
const [deletingUser, setDeletingUser] = useState(null);
const [searchParam, setSearchParam] = useState("");
const [users, setUsers] = useState([]);
useEffect(() => {
setLoading(true);
const delayDebounceFn = setTimeout(() => {
const fetchUsers = async () => {
try {
const res = await api.get("/users/", {
params: { searchParam, pageNumber },
});
setUsers(res.data.users);
setLoading(false);
} catch (err) {
console.log(err);
alert(err);
}
};
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") {
updateUsers(data.user);
}
if (data.action === "delete") {
deleteUser(data.userId);
}
});
return () => {
socket.disconnect();
};
}, []);
const updateUsers = user => {
setUsers(prevState => {
const userIndex = prevState.findIndex(c => c.id === user.id);
if (userIndex === -1) {
return [user, ...prevState];
}
const aux = [...prevState];
aux[userIndex] = user;
return aux;
});
};
const deleteUser = userId => {
setUsers(prevState => {
const userIndex = prevState.findIndex(c => c.id === +userId);
if (userIndex === -1) return prevState;
const aux = [...prevState];
aux.splice(userIndex, 1);
return aux;
});
};
const handleOpenUserModal = () => {
setSelectedUserId(null);
setUserModalOpen(true);
};
const handleCloseUserModal = () => {
setSelectedUserId(null);
setUserModalOpen(false);
};
const handleSearch = event => {
setSearchParam(event.target.value.toLowerCase());
};
const handleEditUser = userId => {
setSelectedUserId(userId);
setUserModalOpen(true);
};
const handleDeleteUser = async userId => {
try {
await api.delete(`/users/${userId}`);
} catch (err) {
alert(err);
}
setDeletingUser(null);
setSearchParam("");
setPageNumber(1);
};
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={selectedUserId}
/>
<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">
<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.id)}
>
<EditIcon />
</IconButton>
<IconButton
size="small"
onClick={e => {
setConfirmModalOpen(true);
setDeletingUser(user);
}}
>
<DeleteOutlineIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</>
)}
</TableBody>
</Table>
</Paper>
</MainContainer>
);
};
export default Users;

View File

@@ -8,6 +8,7 @@ import Chat from "../pages/Chat/";
import Signup from "../pages/Signup/"; import Signup from "../pages/Signup/";
import Login from "../pages/Login/"; import Login from "../pages/Login/";
import WhatsAuth from "../pages/WhatsAuth/WhatsAuth"; import WhatsAuth from "../pages/WhatsAuth/WhatsAuth";
import Users from "../pages/Users";
import Contacts from "../pages/Contacts/"; import Contacts from "../pages/Contacts/";
import { AuthProvider } from "../context/Auth/AuthContext"; import { AuthProvider } from "../context/Auth/AuthContext";
import Route from "./Route"; import Route from "./Route";
@@ -24,6 +25,7 @@ const Routes = () => {
<Route exact path="/chat/:ticketId?" component={Chat} isPrivate /> <Route exact path="/chat/:ticketId?" component={Chat} isPrivate />
<Route exact path="/whats-auth" component={WhatsAuth} isPrivate /> <Route exact path="/whats-auth" component={WhatsAuth} isPrivate />
<Route exact path="/contacts" component={Contacts} isPrivate /> <Route exact path="/contacts" component={Contacts} isPrivate />
<Route exact path="/users" component={Users} isPrivate />
</MainDrawer> </MainDrawer>
</Switch> </Switch>
<ToastContainer autoClose={3000} /> <ToastContainer autoClose={3000} />