mirror of
https://github.com/cheveguerra/whaticket-community.git
synced 2026-04-20 04:39:20 +00:00
feat: added users page
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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" });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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" });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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");
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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" });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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" ||
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
31
frontend/src/components/MainContainer/index.js
Normal file
31
frontend/src/components/MainContainer/index.js
Normal 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;
|
||||||
19
frontend/src/components/MainHeader/index.js
Normal file
19
frontend/src/components/MainHeader/index.js
Normal 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;
|
||||||
21
frontend/src/components/MainHeaderButtonsWrapper/index.js
Normal file
21
frontend/src/components/MainHeaderButtonsWrapper/index.js
Normal 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;
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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%",
|
||||||
|
|||||||
@@ -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;
|
|
||||||
78
frontend/src/components/TableRowSkeleton/index.js
Normal file
78
frontend/src/components/TableRowSkeleton/index.js
Normal 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;
|
||||||
@@ -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)",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
10
frontend/src/components/Title/index.js
Normal file
10
frontend/src/components/Title/index.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
212
frontend/src/components/UserModal/index.js
Normal file
212
frontend/src/components/UserModal/index.js
Normal 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;
|
||||||
@@ -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 />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
234
frontend/src/pages/Users/index.js
Normal file
234
frontend/src/pages/Users/index.js
Normal 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;
|
||||||
@@ -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} />
|
||||||
|
|||||||
Reference in New Issue
Block a user