Merge pull request #233 from ertprs/master

add quick answers
This commit is contained in:
Cassio Santos
2021-09-08 13:41:50 -03:00
committed by GitHub
34 changed files with 5149 additions and 3903 deletions

View File

@@ -0,0 +1,117 @@
import * as Yup from "yup";
import { Request, Response } from "express";
import { getIO } from "../libs/socket";
import ListQuickAnswerService from "../services/QuickAnswerService/ListQuickAnswerService";
import CreateQuickAnswerService from "../services/QuickAnswerService/CreateQuickAnswerService";
import ShowQuickAnswerService from "../services/QuickAnswerService/ShowQuickAnswerService";
import UpdateQuickAnswerService from "../services/QuickAnswerService/UpdateQuickAnswerService";
import DeleteQuickAnswerService from "../services/QuickAnswerService/DeleteQuickAnswerService";
import AppError from "../errors/AppError";
type IndexQuery = {
searchParam: string;
pageNumber: string;
};
interface QuickAnswerData {
shortcut: string;
message: string;
}
export const index = async (req: Request, res: Response): Promise<Response> => {
const { searchParam, pageNumber } = req.query as IndexQuery;
const { quickAnswers, count, hasMore } = await ListQuickAnswerService({
searchParam,
pageNumber
});
return res.json({ quickAnswers, count, hasMore });
};
export const store = async (req: Request, res: Response): Promise<Response> => {
const newQuickAnswer: QuickAnswerData = req.body;
const QuickAnswerSchema = Yup.object().shape({
shortcut: Yup.string().required(),
message: Yup.string().required()
});
try {
await QuickAnswerSchema.validate(newQuickAnswer);
} catch (err) {
throw new AppError(err.message);
}
const quickAnswer = await CreateQuickAnswerService({
...newQuickAnswer
});
const io = getIO();
io.emit("quickAnswer", {
action: "create",
quickAnswer
});
return res.status(200).json(quickAnswer);
};
export const show = async (req: Request, res: Response): Promise<Response> => {
const { quickAnswerId } = req.params;
const quickAnswer = await ShowQuickAnswerService(quickAnswerId);
return res.status(200).json(quickAnswer);
};
export const update = async (
req: Request,
res: Response
): Promise<Response> => {
const quickAnswerData: QuickAnswerData = req.body;
const schema = Yup.object().shape({
shortcut: Yup.string(),
message: Yup.string()
});
try {
await schema.validate(quickAnswerData);
} catch (err) {
throw new AppError(err.message);
}
const { quickAnswerId } = req.params;
const quickAnswer = await UpdateQuickAnswerService({
quickAnswerData,
quickAnswerId
});
const io = getIO();
io.emit("quickAnswer", {
action: "update",
quickAnswer
});
return res.status(200).json(quickAnswer);
};
export const remove = async (
req: Request,
res: Response
): Promise<Response> => {
const { quickAnswerId } = req.params;
await DeleteQuickAnswerService(quickAnswerId);
const io = getIO();
io.emit("quickAnswer", {
action: "delete",
quickAnswerId
});
return res.status(200).json({ message: "Quick Answer deleted" });
};

View File

@@ -9,6 +9,7 @@ import Message from "../models/Message";
import Queue from "../models/Queue"; import Queue from "../models/Queue";
import WhatsappQueue from "../models/WhatsappQueue"; import WhatsappQueue from "../models/WhatsappQueue";
import UserQueue from "../models/UserQueue"; import UserQueue from "../models/UserQueue";
import QuickAnswer from "../models/QuickAnswer";
// eslint-disable-next-line // eslint-disable-next-line
const dbConfig = require("../config/database"); const dbConfig = require("../config/database");
@@ -26,7 +27,8 @@ const models = [
Setting, Setting,
Queue, Queue,
WhatsappQueue, WhatsappQueue,
UserQueue UserQueue,
QuickAnswer
]; ];
sequelize.addModels(models); sequelize.addModels(models);

View File

@@ -0,0 +1,34 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.createTable("QuickAnswers", {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
shortcut: {
type: DataTypes.TEXT,
allowNull: false
},
message: {
type: DataTypes.TEXT,
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false
}
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.dropTable("QuickAnswers");
}
};

View File

@@ -0,0 +1,32 @@
import {
Table,
Column,
DataType,
CreatedAt,
UpdatedAt,
Model,
PrimaryKey,
AutoIncrement
} from "sequelize-typescript";
@Table
class QuickAnswer extends Model<QuickAnswer> {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@Column(DataType.TEXT)
shortcut: string;
@Column(DataType.TEXT)
message: string;
@CreatedAt
createdAt: Date;
@UpdatedAt
updatedAt: Date;
}
export default QuickAnswer;

View File

@@ -9,6 +9,7 @@ import whatsappRoutes from "./whatsappRoutes";
import messageRoutes from "./messageRoutes"; import messageRoutes from "./messageRoutes";
import whatsappSessionRoutes from "./whatsappSessionRoutes"; import whatsappSessionRoutes from "./whatsappSessionRoutes";
import queueRoutes from "./queueRoutes"; import queueRoutes from "./queueRoutes";
import quickAnswerRoutes from "./quickAnswerRoutes";
const routes = Router(); const routes = Router();
@@ -22,5 +23,6 @@ routes.use(messageRoutes);
routes.use(messageRoutes); routes.use(messageRoutes);
routes.use(whatsappSessionRoutes); routes.use(whatsappSessionRoutes);
routes.use(queueRoutes); routes.use(queueRoutes);
routes.use(quickAnswerRoutes);
export default routes; export default routes;

View File

@@ -0,0 +1,30 @@
import express from "express";
import isAuth from "../middleware/isAuth";
import * as QuickAnswerController from "../controllers/QuickAnswerController";
const quickAnswerRoutes = express.Router();
quickAnswerRoutes.get("/quickAnswers", isAuth, QuickAnswerController.index);
quickAnswerRoutes.get(
"/quickAnswers/:quickAnswerId",
isAuth,
QuickAnswerController.show
);
quickAnswerRoutes.post("/quickAnswers", isAuth, QuickAnswerController.store);
quickAnswerRoutes.put(
"/quickAnswers/:quickAnswerId",
isAuth,
QuickAnswerController.update
);
quickAnswerRoutes.delete(
"/quickAnswers/:quickAnswerId",
isAuth,
QuickAnswerController.remove
);
export default quickAnswerRoutes;

View File

@@ -0,0 +1,26 @@
import AppError from "../../errors/AppError";
import QuickAnswer from "../../models/QuickAnswer";
interface Request {
shortcut: string;
message: string;
}
const CreateQuickAnswerService = async ({
shortcut,
message
}: Request): Promise<QuickAnswer> => {
const nameExists = await QuickAnswer.findOne({
where: { shortcut }
});
if (nameExists) {
throw new AppError("ERR__SHORTCUT_DUPLICATED");
}
const quickAnswer = await QuickAnswer.create({ shortcut, message });
return quickAnswer;
};
export default CreateQuickAnswerService;

View File

@@ -0,0 +1,16 @@
import QuickAnswer from "../../models/QuickAnswer";
import AppError from "../../errors/AppError";
const DeleteQuickAnswerService = async (id: string): Promise<void> => {
const quickAnswer = await QuickAnswer.findOne({
where: { id }
});
if (!quickAnswer) {
throw new AppError("ERR_NO_QUICK_ANSWER_FOUND", 404);
}
await quickAnswer.destroy();
};
export default DeleteQuickAnswerService;

View File

@@ -0,0 +1,45 @@
import { Sequelize } from "sequelize";
import QuickAnswer from "../../models/QuickAnswer";
interface Request {
searchParam?: string;
pageNumber?: string;
}
interface Response {
quickAnswers: QuickAnswer[];
count: number;
hasMore: boolean;
}
const ListQuickAnswerService = async ({
searchParam = "",
pageNumber = "1"
}: Request): Promise<Response> => {
const whereCondition = {
message: Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("message")),
"LIKE",
`%${searchParam.toLowerCase().trim()}%`
)
};
const limit = 20;
const offset = limit * (+pageNumber - 1);
const { count, rows: quickAnswers } = await QuickAnswer.findAndCountAll({
where: whereCondition,
limit,
offset,
order: [["message", "ASC"]]
});
const hasMore = count > offset + quickAnswers.length;
return {
quickAnswers,
count,
hasMore
};
};
export default ListQuickAnswerService;

View File

@@ -0,0 +1,14 @@
import QuickAnswer from "../../models/QuickAnswer";
import AppError from "../../errors/AppError";
const ShowQuickAnswerService = async (id: string): Promise<QuickAnswer> => {
const quickAnswer = await QuickAnswer.findByPk(id);
if (!quickAnswer) {
throw new AppError("ERR_NO_QUICK_ANSWERS_FOUND", 404);
}
return quickAnswer;
};
export default ShowQuickAnswerService;

View File

@@ -0,0 +1,40 @@
import QuickAnswer from "../../models/QuickAnswer";
import AppError from "../../errors/AppError";
interface QuickAnswerData {
shortcut?: string;
message?: string;
}
interface Request {
quickAnswerData: QuickAnswerData;
quickAnswerId: string;
}
const UpdateQuickAnswerService = async ({
quickAnswerData,
quickAnswerId
}: Request): Promise<QuickAnswer> => {
const { shortcut, message } = quickAnswerData;
const quickAnswer = await QuickAnswer.findOne({
where: { id: quickAnswerId },
attributes: ["id", "shortcut", "message"]
});
if (!quickAnswer) {
throw new AppError("ERR_NO_QUICK_ANSWERS_FOUND", 404);
}
await quickAnswer.update({
shortcut,
message
});
await quickAnswer.reload({
attributes: ["id", "shortcut", "message"]
});
return quickAnswer;
};
export default UpdateQuickAnswerService;

View File

@@ -2,46 +2,46 @@ import React, { useState, useEffect } from "react";
import Routes from "./routes"; import Routes from "./routes";
import "react-toastify/dist/ReactToastify.css"; import "react-toastify/dist/ReactToastify.css";
import { createMuiTheme, ThemeProvider } from "@material-ui/core/styles"; import { createTheme, ThemeProvider } from "@material-ui/core/styles";
import { ptBR } from "@material-ui/core/locale"; import { ptBR } from "@material-ui/core/locale";
const App = () => { const App = () => {
const [locale, setLocale] = useState(); const [locale, setLocale] = useState();
const theme = createMuiTheme( const theme = createTheme(
{ {
scrollbarStyles: { scrollbarStyles: {
"&::-webkit-scrollbar": { "&::-webkit-scrollbar": {
width: "8px", width: "8px",
height: "8px", height: "8px",
}, },
"&::-webkit-scrollbar-thumb": { "&::-webkit-scrollbar-thumb": {
boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)",
backgroundColor: "#e8e8e8", backgroundColor: "#e8e8e8",
}, },
}, },
palette: { palette: {
primary: { main: "#2576d2" }, primary: { main: "#2576d2" },
}, },
}, },
locale locale
); );
useEffect(() => { useEffect(() => {
const i18nlocale = localStorage.getItem("i18nextLng"); const i18nlocale = localStorage.getItem("i18nextLng");
const browserLocale = const browserLocale =
i18nlocale.substring(0, 2) + i18nlocale.substring(3, 5); i18nlocale.substring(0, 2) + i18nlocale.substring(3, 5);
if (browserLocale === "ptBR") { if (browserLocale === "ptBR") {
setLocale(ptBR); setLocale(ptBR);
} }
}, []); }, []);
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Routes /> <Routes />
</ThemeProvider> </ThemeProvider>
); );
}; };
export default App; export default App;

View File

@@ -4,28 +4,28 @@ import React, { useState } from "react";
import { GithubPicker } from "react-color"; import { GithubPicker } from "react-color";
const ColorPicker = ({ onChange, currentColor, handleClose, open }) => { const ColorPicker = ({ onChange, currentColor, handleClose, open }) => {
const [selectedColor, setSelectedColor] = useState(currentColor); const [selectedColor, setSelectedColor] = useState(currentColor);
const handleChange = color => { const handleChange = (color) => {
setSelectedColor(color.hex); setSelectedColor(color.hex);
handleClose(); handleClose();
}; };
return ( return (
<Dialog <Dialog
onClose={handleClose} onClose={handleClose}
aria-labelledby="simple-dialog-title" aria-labelledby="simple-dialog-title"
open={open} open={open}
> >
<GithubPicker <GithubPicker
width={"100%"} width={"100%"}
triangle="hide" triangle="hide"
color={selectedColor} color={selectedColor}
onChange={handleChange} onChange={handleChange}
onChangeComplete={color => onChange(color.hex)} onChangeComplete={(color) => onChange(color.hex)}
/> />
</Dialog> </Dialog>
); );
}; };
export default ColorPicker; export default ColorPicker;

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -10,62 +10,62 @@ import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessa
import toastError from "../../errors/toastError"; import toastError from "../../errors/toastError";
const MessageOptionsMenu = ({ message, menuOpen, handleClose, anchorEl }) => { const MessageOptionsMenu = ({ message, menuOpen, handleClose, anchorEl }) => {
const { setReplyingMessage } = useContext(ReplyMessageContext); const { setReplyingMessage } = useContext(ReplyMessageContext);
const [confirmationOpen, setConfirmationOpen] = useState(false); const [confirmationOpen, setConfirmationOpen] = useState(false);
const handleDeleteMessage = async () => { const handleDeleteMessage = async () => {
try { try {
await api.delete(`/messages/${message.id}`); await api.delete(`/messages/${message.id}`);
} catch (err) { } catch (err) {
toastError(err); toastError(err);
} }
}; };
const hanldeReplyMessage = () => { const hanldeReplyMessage = () => {
setReplyingMessage(message); setReplyingMessage(message);
handleClose(); handleClose();
}; };
const handleOpenConfirmationModal = e => { const handleOpenConfirmationModal = (e) => {
setConfirmationOpen(true); setConfirmationOpen(true);
handleClose(); handleClose();
}; };
return ( return (
<> <>
<ConfirmationModal <ConfirmationModal
title={i18n.t("messageOptionsMenu.confirmationModal.title")} title={i18n.t("messageOptionsMenu.confirmationModal.title")}
open={confirmationOpen} open={confirmationOpen}
onClose={setConfirmationOpen} onClose={setConfirmationOpen}
onConfirm={handleDeleteMessage} onConfirm={handleDeleteMessage}
> >
{i18n.t("messageOptionsMenu.confirmationModal.message")} {i18n.t("messageOptionsMenu.confirmationModal.message")}
</ConfirmationModal> </ConfirmationModal>
<Menu <Menu
anchorEl={anchorEl} anchorEl={anchorEl}
getContentAnchorEl={null} getContentAnchorEl={null}
anchorOrigin={{ anchorOrigin={{
vertical: "bottom", vertical: "bottom",
horizontal: "right", horizontal: "right",
}} }}
transformOrigin={{ transformOrigin={{
vertical: "top", vertical: "top",
horizontal: "right", horizontal: "right",
}} }}
open={menuOpen} open={menuOpen}
onClose={handleClose} onClose={handleClose}
> >
{message.fromMe && ( {message.fromMe && (
<MenuItem onClick={handleOpenConfirmationModal}> <MenuItem onClick={handleOpenConfirmationModal}>
{i18n.t("messageOptionsMenu.delete")} {i18n.t("messageOptionsMenu.delete")}
</MenuItem> </MenuItem>
)} )}
<MenuItem onClick={hanldeReplyMessage}> <MenuItem onClick={hanldeReplyMessage}>
{i18n.t("messageOptionsMenu.reply")} {i18n.t("messageOptionsMenu.reply")}
</MenuItem> </MenuItem>
</Menu> </Menu>
</> </>
); );
}; };
export default MessageOptionsMenu; export default MessageOptionsMenu;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,222 @@
import React, { useState, useEffect, useRef } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import {
makeStyles,
Button,
TextField,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
CircularProgress,
} from "@material-ui/core";
import { green } from "@material-ui/core/colors";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
const useStyles = makeStyles((theme) => ({
root: {
flexWrap: "wrap",
},
textField: {
marginRight: theme.spacing(1),
width: "100%",
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
textQuickAnswerContainer: {
width: "100%",
},
}));
const QuickAnswerSchema = Yup.object().shape({
shortcut: Yup.string()
.min(2, "Too Short!")
.max(15, "Too Long!")
.required("Required"),
message: Yup.string()
.min(8, "Too Short!")
.max(30000, "Too Long!")
.required("Required"),
});
const QuickAnswersModal = ({
open,
onClose,
quickAnswerId,
initialValues,
onSave,
}) => {
const classes = useStyles();
const isMounted = useRef(true);
const initialState = {
shortcut: "",
message: "",
};
const [quickAnswer, setQuickAnswer] = useState(initialState);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
const fetchQuickAnswer = async () => {
if (initialValues) {
setQuickAnswer((prevState) => {
return { ...prevState, ...initialValues };
});
}
if (!quickAnswerId) return;
try {
const { data } = await api.get(`/quickAnswers/${quickAnswerId}`);
if (isMounted.current) {
setQuickAnswer(data);
}
} catch (err) {
toastError(err);
}
};
fetchQuickAnswer();
}, [quickAnswerId, open, initialValues]);
const handleClose = () => {
onClose();
setQuickAnswer(initialState);
};
const handleSaveQuickAnswer = async (values) => {
try {
if (quickAnswerId) {
await api.put(`/quickAnswers/${quickAnswerId}`, values);
handleClose();
} else {
const { data } = await api.post("/quickAnswers", values);
if (onSave) {
onSave(data);
}
handleClose();
}
toast.success(i18n.t("quickAnswersModal.success"));
} catch (err) {
toastError(err);
}
};
return (
<div className={classes.root}>
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
scroll="paper"
>
<DialogTitle id="form-dialog-title">
{quickAnswerId
? `${i18n.t("quickAnswersModal.title.edit")}`
: `${i18n.t("quickAnswersModal.title.add")}`}
</DialogTitle>
<Formik
initialValues={quickAnswer}
enableReinitialize={true}
validationSchema={QuickAnswerSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveQuickAnswer(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ values, errors, touched, isSubmitting }) => (
<Form>
<DialogContent dividers>
<div className={classes.textQuickAnswerContainer}>
<Field
as={TextField}
label={i18n.t("quickAnswersModal.form.shortcut")}
name="shortcut"
autoFocus
error={touched.shortcut && Boolean(errors.shortcut)}
helperText={touched.shortcut && errors.shortcut}
variant="outlined"
margin="dense"
className={classes.textField}
fullWidth
/>
</div>
<div className={classes.textQuickAnswerContainer}>
<Field
as={TextField}
label={i18n.t("quickAnswersModal.form.message")}
name="message"
error={touched.message && Boolean(errors.message)}
helperText={touched.message && errors.message}
variant="outlined"
margin="dense"
className={classes.textField}
multiline
rows={5}
fullWidth
/>
</div>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("quickAnswersModal.buttons.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{quickAnswerId
? `${i18n.t("quickAnswersModal.buttons.okEdit")}`
: `${i18n.t("quickAnswersModal.buttons.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default QuickAnswersModal;

View File

@@ -19,144 +19,167 @@ import toastError from "../../errors/toastError";
const drawerWidth = 320; const drawerWidth = 320;
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
display: "flex", display: "flex",
height: "100%", height: "100%",
position: "relative", position: "relative",
overflow: "hidden", overflow: "hidden",
}, },
mainWrapper: { ticketInfo: {
flex: 1, maxWidth: "50%",
height: "100%", flexBasis: "50%",
display: "flex", [theme.breakpoints.down("sm")]: {
flexDirection: "column", maxWidth: "80%",
overflow: "hidden", flexBasis: "80%",
borderTopLeftRadius: 0, },
borderBottomLeftRadius: 0, },
borderLeft: "0", ticketActionButtons: {
marginRight: -drawerWidth, maxWidth: "50%",
transition: theme.transitions.create("margin", { flexBasis: "50%",
easing: theme.transitions.easing.sharp, display: "flex",
duration: theme.transitions.duration.leavingScreen, [theme.breakpoints.down("sm")]: {
}), maxWidth: "100%",
}, flexBasis: "100%",
marginBottom: "5px",
},
},
mainWrapperShift: { mainWrapper: {
borderTopRightRadius: 0, flex: 1,
borderBottomRightRadius: 0, height: "100%",
transition: theme.transitions.create("margin", { display: "flex",
easing: theme.transitions.easing.easeOut, flexDirection: "column",
duration: theme.transitions.duration.enteringScreen, overflow: "hidden",
}), borderTopLeftRadius: 0,
marginRight: 0, borderBottomLeftRadius: 0,
}, borderLeft: "0",
marginRight: -drawerWidth,
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
mainWrapperShift: {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
marginRight: 0,
},
})); }));
const Ticket = () => { const Ticket = () => {
const { ticketId } = useParams(); const { ticketId } = useParams();
const history = useHistory(); const history = useHistory();
const classes = useStyles(); const classes = useStyles();
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [contact, setContact] = useState({}); const [contact, setContact] = useState({});
const [ticket, setTicket] = useState({}); const [ticket, setTicket] = useState({});
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
const delayDebounceFn = setTimeout(() => { const delayDebounceFn = setTimeout(() => {
const fetchTicket = async () => { const fetchTicket = async () => {
try { try {
const { data } = await api.get("/tickets/" + ticketId); const { data } = await api.get("/tickets/" + ticketId);
setContact(data.contact); setContact(data.contact);
setTicket(data); setTicket(data);
setLoading(false); setLoading(false);
} catch (err) { } catch (err) {
setLoading(false); setLoading(false);
toastError(err); toastError(err);
} }
}; };
fetchTicket(); fetchTicket();
}, 500); }, 500);
return () => clearTimeout(delayDebounceFn); return () => clearTimeout(delayDebounceFn);
}, [ticketId, history]); }, [ticketId, history]);
useEffect(() => { useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL); const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("connect", () => socket.emit("joinChatBox", ticketId)); socket.on("connect", () => socket.emit("joinChatBox", ticketId));
socket.on("ticket", data => { socket.on("ticket", (data) => {
if (data.action === "update") { if (data.action === "update") {
setTicket(data.ticket); setTicket(data.ticket);
} }
if (data.action === "delete") { if (data.action === "delete") {
toast.success("Ticket deleted sucessfully."); toast.success("Ticket deleted sucessfully.");
history.push("/tickets"); history.push("/tickets");
} }
}); });
socket.on("contact", data => { socket.on("contact", (data) => {
if (data.action === "update") { if (data.action === "update") {
setContact(prevState => { setContact((prevState) => {
if (prevState.id === data.contact?.id) { if (prevState.id === data.contact?.id) {
return { ...prevState, ...data.contact }; return { ...prevState, ...data.contact };
} }
return prevState; return prevState;
}); });
} }
}); });
return () => { return () => {
socket.disconnect(); socket.disconnect();
}; };
}, [ticketId, history]); }, [ticketId, history]);
const handleDrawerOpen = () => { const handleDrawerOpen = () => {
setDrawerOpen(true); setDrawerOpen(true);
}; };
const handleDrawerClose = () => { const handleDrawerClose = () => {
setDrawerOpen(false); setDrawerOpen(false);
}; };
return ( return (
<div className={classes.root} id="drawer-container"> <div className={classes.root} id="drawer-container">
<Paper <Paper
variant="outlined" variant="outlined"
elevation={0} elevation={0}
className={clsx(classes.mainWrapper, { className={clsx(classes.mainWrapper, {
[classes.mainWrapperShift]: drawerOpen, [classes.mainWrapperShift]: drawerOpen,
})} })}
> >
<TicketHeader loading={loading}> <TicketHeader loading={loading}>
<TicketInfo <div className={classes.ticketInfo}>
contact={contact} <TicketInfo
ticket={ticket} contact={contact}
onClick={handleDrawerOpen} ticket={ticket}
/> onClick={handleDrawerOpen}
<TicketActionButtons ticket={ticket} /> />
</TicketHeader> </div>
<ReplyMessageProvider> <div className={classes.ticketActionButtons}>
<MessagesList <TicketActionButtons ticket={ticket} />
ticketId={ticketId} </div>
isGroup={ticket.isGroup} </TicketHeader>
></MessagesList> <ReplyMessageProvider>
<MessageInput ticketStatus={ticket.status} /> <MessagesList
</ReplyMessageProvider> ticketId={ticketId}
</Paper> isGroup={ticket.isGroup}
<ContactDrawer ></MessagesList>
open={drawerOpen} <MessageInput ticketStatus={ticket.status} />
handleDrawerClose={handleDrawerClose} </ReplyMessageProvider>
contact={contact} </Paper>
loading={loading} <ContactDrawer
/> open={drawerOpen}
</div> handleDrawerClose={handleDrawerClose}
); contact={contact}
loading={loading}
/>
</div>
);
}; };
export default Ticket; export default Ticket;

View File

@@ -1,32 +1,44 @@
import React from "react"; import React from "react";
import { Card } from "@material-ui/core"; import { Card, Button } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import TicketHeaderSkeleton from "../TicketHeaderSkeleton"; import TicketHeaderSkeleton from "../TicketHeaderSkeleton";
import ArrowBackIos from "@material-ui/icons/ArrowBackIos";
import { useHistory } from "react-router-dom";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles((theme) => ({
ticketHeader: { ticketHeader: {
display: "flex", display: "flex",
backgroundColor: "#eee", backgroundColor: "#eee",
flex: "none", flex: "none",
borderBottom: "1px solid rgba(0, 0, 0, 0.12)", borderBottom: "1px solid rgba(0, 0, 0, 0.12)",
}, [theme.breakpoints.down("sm")]: {
flexWrap: "wrap",
},
},
})); }));
const TicketHeader = ({ loading, children }) => { const TicketHeader = ({ loading, children }) => {
const classes = useStyles(); const classes = useStyles();
const history = useHistory();
const handleBack = () => {
history.push("/tickets");
};
return ( return (
<> <>
{loading ? ( {loading ? (
<TicketHeaderSkeleton /> <TicketHeaderSkeleton />
) : ( ) : (
<Card square className={classes.ticketHeader}> <Card square className={classes.ticketHeader}>
{children} <Button color="primary" onClick={handleBack}>
</Card> <ArrowBackIos />
)} </Button>
</> {children}
); </Card>
)}
</>
);
}; };
export default TicketHeader; export default TicketHeader;

View File

@@ -22,215 +22,215 @@ import { Can } from "../Can";
import TicketsQueueSelect from "../TicketsQueueSelect"; import TicketsQueueSelect from "../TicketsQueueSelect";
import { Button } from "@material-ui/core"; import { Button } from "@material-ui/core";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles((theme) => ({
ticketsWrapper: { ticketsWrapper: {
position: "relative", position: "relative",
display: "flex", display: "flex",
height: "100%", height: "100%",
flexDirection: "column", flexDirection: "column",
overflow: "hidden", overflow: "hidden",
borderTopRightRadius: 0, borderTopRightRadius: 0,
borderBottomRightRadius: 0, borderBottomRightRadius: 0,
}, },
tabsHeader: { tabsHeader: {
flex: "none", flex: "none",
backgroundColor: "#eee", backgroundColor: "#eee",
}, },
settingsIcon: { settingsIcon: {
alignSelf: "center", alignSelf: "center",
marginLeft: "auto", marginLeft: "auto",
padding: 8, padding: 8,
}, },
tab: { tab: {
minWidth: 120, minWidth: 120,
width: 120, width: 120,
}, },
ticketOptionsBox: { ticketOptionsBox: {
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
background: "#fafafa", background: "#fafafa",
padding: theme.spacing(1), padding: theme.spacing(1),
}, },
serachInputWrapper: { serachInputWrapper: {
flex: 1, flex: 1,
background: "#fff", background: "#fff",
display: "flex", display: "flex",
borderRadius: 40, borderRadius: 40,
padding: 4, padding: 4,
marginRight: theme.spacing(1), marginRight: theme.spacing(1),
}, },
searchIcon: { searchIcon: {
color: "grey", color: "grey",
marginLeft: 6, marginLeft: 6,
marginRight: 6, marginRight: 6,
alignSelf: "center", alignSelf: "center",
}, },
searchInput: { searchInput: {
flex: 1, flex: 1,
border: "none", border: "none",
borderRadius: 30, borderRadius: 30,
}, },
})); }));
const TicketsManager = () => { const TicketsManager = () => {
const classes = useStyles(); const classes = useStyles();
const [searchParam, setSearchParam] = useState(""); const [searchParam, setSearchParam] = useState("");
const [tab, setTab] = useState("open"); const [tab, setTab] = useState("open");
const [newTicketModalOpen, setNewTicketModalOpen] = useState(false); const [newTicketModalOpen, setNewTicketModalOpen] = useState(false);
const [showAllTickets, setShowAllTickets] = useState(false); const [showAllTickets, setShowAllTickets] = useState(false);
const searchInputRef = useRef(); const searchInputRef = useRef();
const { user } = useContext(AuthContext); const { user } = useContext(AuthContext);
const userQueueIds = user.queues.map(q => q.id); const userQueueIds = user.queues.map((q) => q.id);
const [selectedQueueIds, setSelectedQueueIds] = useState(userQueueIds || []); const [selectedQueueIds, setSelectedQueueIds] = useState(userQueueIds || []);
useEffect(() => { useEffect(() => {
if (tab === "search") { if (tab === "search") {
searchInputRef.current.focus(); searchInputRef.current.focus();
} }
}, [tab]); }, [tab]);
let searchTimeout; let searchTimeout;
const handleSearch = e => { const handleSearch = (e) => {
const searchedTerm = e.target.value.toLowerCase(); const searchedTerm = e.target.value.toLowerCase();
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
if (searchedTerm === "") { if (searchedTerm === "") {
setSearchParam(searchedTerm); setSearchParam(searchedTerm);
setTab("open"); setTab("open");
return; return;
} }
searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
setSearchParam(searchedTerm); setSearchParam(searchedTerm);
}, 500); }, 500);
}; };
const handleChangeTab = (e, newValue) => { const handleChangeTab = (e, newValue) => {
setTab(newValue); setTab(newValue);
}; };
return ( return (
<Paper elevation={0} variant="outlined" className={classes.ticketsWrapper}> <Paper elevation={0} variant="outlined" className={classes.ticketsWrapper}>
<NewTicketModal <NewTicketModal
modalOpen={newTicketModalOpen} modalOpen={newTicketModalOpen}
onClose={e => setNewTicketModalOpen(false)} onClose={(e) => setNewTicketModalOpen(false)}
/> />
<Paper elevation={0} square className={classes.tabsHeader}> <Paper elevation={0} square className={classes.tabsHeader}>
<Tabs <Tabs
value={tab} value={tab}
onChange={handleChangeTab} onChange={handleChangeTab}
variant="fullWidth" variant="fullWidth"
indicatorColor="primary" indicatorColor="primary"
textColor="primary" textColor="primary"
aria-label="icon label tabs example" aria-label="icon label tabs example"
> >
<Tab <Tab
value={"open"} value={"open"}
icon={<MoveToInboxIcon />} icon={<MoveToInboxIcon />}
label={i18n.t("tickets.tabs.open.title")} label={i18n.t("tickets.tabs.open.title")}
classes={{ root: classes.tab }} classes={{ root: classes.tab }}
/> />
<Tab <Tab
value={"closed"} value={"closed"}
icon={<CheckBoxIcon />} icon={<CheckBoxIcon />}
label={i18n.t("tickets.tabs.closed.title")} label={i18n.t("tickets.tabs.closed.title")}
classes={{ root: classes.tab }} classes={{ root: classes.tab }}
/> />
<Tab <Tab
value={"search"} value={"search"}
icon={<SearchIcon />} icon={<SearchIcon />}
label={i18n.t("tickets.tabs.search.title")} label={i18n.t("tickets.tabs.search.title")}
classes={{ root: classes.tab }} classes={{ root: classes.tab }}
/> />
</Tabs> </Tabs>
</Paper> </Paper>
<Paper square elevation={0} className={classes.ticketOptionsBox}> <Paper square elevation={0} className={classes.ticketOptionsBox}>
{tab === "search" ? ( {tab === "search" ? (
<div className={classes.serachInputWrapper}> <div className={classes.serachInputWrapper}>
<SearchIcon className={classes.searchIcon} /> <SearchIcon className={classes.searchIcon} />
<InputBase <InputBase
className={classes.searchInput} className={classes.searchInput}
inputRef={searchInputRef} inputRef={searchInputRef}
placeholder={i18n.t("tickets.search.placeholder")} placeholder={i18n.t("tickets.search.placeholder")}
type="search" type="search"
onChange={handleSearch} onChange={handleSearch}
/> />
</div> </div>
) : ( ) : (
<> <>
<Button <Button
variant="outlined" variant="outlined"
color="primary" color="primary"
onClick={() => setNewTicketModalOpen(true)} onClick={() => setNewTicketModalOpen(true)}
> >
{i18n.t("ticketsManager.buttons.newTicket")} {i18n.t("ticketsManager.buttons.newTicket")}
</Button> </Button>
<Can <Can
role={user.profile} role={user.profile}
perform="tickets-manager:showall" perform="tickets-manager:showall"
yes={() => ( yes={() => (
<FormControlLabel <FormControlLabel
label={i18n.t("tickets.buttons.showAll")} label={i18n.t("tickets.buttons.showAll")}
labelPlacement="start" labelPlacement="start"
control={ control={
<Switch <Switch
size="small" size="small"
checked={showAllTickets} checked={showAllTickets}
onChange={() => onChange={() =>
setShowAllTickets(prevState => !prevState) setShowAllTickets((prevState) => !prevState)
} }
name="showAllTickets" name="showAllTickets"
color="primary" color="primary"
/> />
} }
/> />
)} )}
/> />
</> </>
)} )}
<TicketsQueueSelect <TicketsQueueSelect
style={{ marginLeft: 6 }} style={{ marginLeft: 6 }}
selectedQueueIds={selectedQueueIds} selectedQueueIds={selectedQueueIds}
userQueues={user?.queues} userQueues={user?.queues}
onChange={values => setSelectedQueueIds(values)} onChange={(values) => setSelectedQueueIds(values)}
/> />
</Paper> </Paper>
<TabPanel value={tab} name="open" className={classes.ticketsWrapper}> <TabPanel value={tab} name="open" className={classes.ticketsWrapper}>
<TicketsList <TicketsList
status="open" status="open"
showAll={showAllTickets} showAll={showAllTickets}
selectedQueueIds={selectedQueueIds} selectedQueueIds={selectedQueueIds}
/> />
<TicketsList status="pending" selectedQueueIds={selectedQueueIds} /> <TicketsList status="pending" selectedQueueIds={selectedQueueIds} />
</TabPanel> </TabPanel>
<TabPanel value={tab} name="closed" className={classes.ticketsWrapper}> <TabPanel value={tab} name="closed" className={classes.ticketsWrapper}>
<TicketsList <TicketsList
status="closed" status="closed"
showAll={true} showAll={true}
selectedQueueIds={selectedQueueIds} selectedQueueIds={selectedQueueIds}
/> />
</TabPanel> </TabPanel>
<TabPanel value={tab} name="search" className={classes.ticketsWrapper}> <TabPanel value={tab} name="search" className={classes.ticketsWrapper}>
<TicketsList <TicketsList
searchParam={searchParam} searchParam={searchParam}
showAll={true} showAll={true}
selectedQueueIds={selectedQueueIds} selectedQueueIds={selectedQueueIds}
/> />
</TabPanel> </TabPanel>
</Paper> </Paper>
); );
}; };
export default TicketsManager; export default TicketsManager;

View File

@@ -14,6 +14,7 @@ import SettingsOutlinedIcon from "@material-ui/icons/SettingsOutlined";
import PeopleAltOutlinedIcon from "@material-ui/icons/PeopleAltOutlined"; import PeopleAltOutlinedIcon from "@material-ui/icons/PeopleAltOutlined";
import ContactPhoneOutlinedIcon from "@material-ui/icons/ContactPhoneOutlined"; import ContactPhoneOutlinedIcon from "@material-ui/icons/ContactPhoneOutlined";
import AccountTreeOutlinedIcon from "@material-ui/icons/AccountTreeOutlined"; import AccountTreeOutlinedIcon from "@material-ui/icons/AccountTreeOutlined";
import QuestionAnswerOutlinedIcon from "@material-ui/icons/QuestionAnswerOutlined";
import { i18n } from "../translate/i18n"; import { i18n } from "../translate/i18n";
import { WhatsAppsContext } from "../context/WhatsApp/WhatsAppsContext"; import { WhatsAppsContext } from "../context/WhatsApp/WhatsAppsContext";
@@ -21,109 +22,115 @@ import { AuthContext } from "../context/Auth/AuthContext";
import { Can } from "../components/Can"; import { Can } from "../components/Can";
function ListItemLink(props) { function ListItemLink(props) {
const { icon, primary, to, className } = props; const { icon, primary, to, className } = props;
const renderLink = React.useMemo( const renderLink = React.useMemo(
() => () =>
React.forwardRef((itemProps, ref) => ( React.forwardRef((itemProps, ref) => (
<RouterLink to={to} ref={ref} {...itemProps} /> <RouterLink to={to} ref={ref} {...itemProps} />
)), )),
[to] [to]
); );
return ( return (
<li> <li>
<ListItem button component={renderLink} className={className}> <ListItem button component={renderLink} className={className}>
{icon ? <ListItemIcon>{icon}</ListItemIcon> : null} {icon ? <ListItemIcon>{icon}</ListItemIcon> : null}
<ListItemText primary={primary} /> <ListItemText primary={primary} />
</ListItem> </ListItem>
</li> </li>
); );
} }
const MainListItems = () => { const MainListItems = (props) => {
const { whatsApps } = useContext(WhatsAppsContext); const { drawerClose } = props;
const { user } = useContext(AuthContext); const { whatsApps } = useContext(WhatsAppsContext);
const [connectionWarning, setConnectionWarning] = useState(false); const { user } = useContext(AuthContext);
const [connectionWarning, setConnectionWarning] = useState(false);
useEffect(() => { useEffect(() => {
const delayDebounceFn = setTimeout(() => { const delayDebounceFn = setTimeout(() => {
if (whatsApps.length > 0) { if (whatsApps.length > 0) {
const offlineWhats = whatsApps.filter(whats => { const offlineWhats = whatsApps.filter((whats) => {
return ( return (
whats.status === "qrcode" || whats.status === "qrcode" ||
whats.status === "PAIRING" || whats.status === "PAIRING" ||
whats.status === "DISCONNECTED" || whats.status === "DISCONNECTED" ||
whats.status === "TIMEOUT" || whats.status === "TIMEOUT" ||
whats.status === "OPENING" whats.status === "OPENING"
); );
}); });
if (offlineWhats.length > 0) { if (offlineWhats.length > 0) {
setConnectionWarning(true); setConnectionWarning(true);
} else { } else {
setConnectionWarning(false); setConnectionWarning(false);
} }
} }
}, 2000); }, 2000);
return () => clearTimeout(delayDebounceFn); return () => clearTimeout(delayDebounceFn);
}, [whatsApps]); }, [whatsApps]);
return ( return (
<div> <div onClick={drawerClose}>
<ListItemLink <ListItemLink
to="/" to="/"
primary="Dashboard" primary="Dashboard"
icon={<DashboardOutlinedIcon />} icon={<DashboardOutlinedIcon />}
/> />
<ListItemLink <ListItemLink
to="/connections" to="/connections"
primary={i18n.t("mainDrawer.listItems.connections")} primary={i18n.t("mainDrawer.listItems.connections")}
icon={ icon={
<Badge badgeContent={connectionWarning ? "!" : 0} color="error"> <Badge badgeContent={connectionWarning ? "!" : 0} color="error">
<SyncAltIcon /> <SyncAltIcon />
</Badge> </Badge>
} }
/> />
<ListItemLink <ListItemLink
to="/tickets" to="/tickets"
primary={i18n.t("mainDrawer.listItems.tickets")} primary={i18n.t("mainDrawer.listItems.tickets")}
icon={<WhatsAppIcon />} icon={<WhatsAppIcon />}
/> />
<ListItemLink <ListItemLink
to="/contacts" to="/contacts"
primary={i18n.t("mainDrawer.listItems.contacts")} primary={i18n.t("mainDrawer.listItems.contacts")}
icon={<ContactPhoneOutlinedIcon />} icon={<ContactPhoneOutlinedIcon />}
/> />
<Can <ListItemLink
role={user.profile} to="/quickAnswers"
perform="drawer-admin-items:view" primary={i18n.t("mainDrawer.listItems.quickAnswers")}
yes={() => ( icon={<QuestionAnswerOutlinedIcon />}
<> />
<Divider /> <Can
<ListSubheader inset> role={user.profile}
{i18n.t("mainDrawer.listItems.administration")} perform="drawer-admin-items:view"
</ListSubheader> yes={() => (
<ListItemLink <>
to="/users" <Divider />
primary={i18n.t("mainDrawer.listItems.users")} <ListSubheader inset>
icon={<PeopleAltOutlinedIcon />} {i18n.t("mainDrawer.listItems.administration")}
/> </ListSubheader>
<ListItemLink <ListItemLink
to="/queues" to="/users"
primary={i18n.t("mainDrawer.listItems.queues")} primary={i18n.t("mainDrawer.listItems.users")}
icon={<AccountTreeOutlinedIcon />} icon={<PeopleAltOutlinedIcon />}
/> />
<ListItemLink <ListItemLink
to="/settings" to="/queues"
primary={i18n.t("mainDrawer.listItems.settings")} primary={i18n.t("mainDrawer.listItems.queues")}
icon={<SettingsOutlinedIcon />} icon={<AccountTreeOutlinedIcon />}
/> />
</> <ListItemLink
)} to="/settings"
/> primary={i18n.t("mainDrawer.listItems.settings")}
</div> icon={<SettingsOutlinedIcon />}
); />
</>
)}
/>
</div>
);
}; };
export default MainListItems; export default MainListItems;

View File

@@ -1,17 +1,17 @@
import React, { useState, useContext } from "react"; import React, { useState, useContext, useEffect } from "react";
import clsx from "clsx"; import clsx from "clsx";
import { import {
makeStyles, makeStyles,
Drawer, Drawer,
AppBar, AppBar,
Toolbar, Toolbar,
List, List,
Typography, Typography,
Divider, Divider,
MenuItem, MenuItem,
IconButton, IconButton,
Menu, Menu,
} from "@material-ui/core"; } from "@material-ui/core";
import MenuIcon from "@material-ui/icons/Menu"; import MenuIcon from "@material-ui/icons/Menu";
@@ -24,221 +24,245 @@ import UserModal from "../components/UserModal";
import { AuthContext } from "../context/Auth/AuthContext"; import { AuthContext } from "../context/Auth/AuthContext";
import BackdropLoading from "../components/BackdropLoading"; import BackdropLoading from "../components/BackdropLoading";
import { i18n } from "../translate/i18n"; import { i18n } from "../translate/i18n";
import { useLocalStorage } from "../hooks/useLocalStorage";
const drawerWidth = 240; const drawerWidth = 240;
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
display: "flex", display: "flex",
height: "100vh", height: "100vh",
}, [theme.breakpoints.down("sm")]: {
height: "calc(100vh - 56px)",
},
},
toolbar: { toolbar: {
paddingRight: 24, // keep right padding when drawer closed paddingRight: 24, // keep right padding when drawer closed
}, },
toolbarIcon: { toolbarIcon: {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "flex-end", justifyContent: "flex-end",
padding: "0 8px", padding: "0 8px",
minHeight: "48px", minHeight: "48px",
}, },
appBar: { appBar: {
zIndex: theme.zIndex.drawer + 1, zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], { transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen, duration: theme.transitions.duration.leavingScreen,
}), }),
}, },
appBarShift: { appBarShift: {
marginLeft: drawerWidth, marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`, width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(["width", "margin"], { transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen, duration: theme.transitions.duration.enteringScreen,
}), }),
}, },
menuButton: { menuButton: {
marginRight: 36, marginRight: 36,
}, },
menuButtonHidden: { menuButtonHidden: {
display: "none", display: "none",
}, },
title: { title: {
flexGrow: 1, flexGrow: 1,
}, },
drawerPaper: { drawerPaper: {
position: "relative", position: "relative",
whiteSpace: "nowrap", whiteSpace: "nowrap",
width: drawerWidth, width: drawerWidth,
transition: theme.transitions.create("width", { transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen, duration: theme.transitions.duration.enteringScreen,
}), }),
}, },
drawerPaperClose: { drawerPaperClose: {
overflowX: "hidden", overflowX: "hidden",
transition: theme.transitions.create("width", { transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen, duration: theme.transitions.duration.leavingScreen,
}), }),
width: theme.spacing(7), width: theme.spacing(7),
[theme.breakpoints.up("sm")]: { [theme.breakpoints.up("sm")]: {
width: theme.spacing(9), width: theme.spacing(9),
}, },
}, },
appBarSpacer: { appBarSpacer: {
minHeight: "48px", minHeight: "48px",
}, },
content: { content: {
flex: 1, flex: 1,
overflow: "auto", overflow: "auto",
}, },
container: { container: {
paddingTop: theme.spacing(4), paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4), paddingBottom: theme.spacing(4),
}, },
paper: { paper: {
padding: theme.spacing(2), padding: theme.spacing(2),
display: "flex", display: "flex",
overflow: "auto", overflow: "auto",
flexDirection: "column", flexDirection: "column",
}, },
})); }));
const LoggedInLayout = ({ children }) => { const LoggedInLayout = ({ children }) => {
const classes = useStyles(); const classes = useStyles();
const [userModalOpen, setUserModalOpen] = useState(false); const [userModalOpen, setUserModalOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const { handleLogout, loading } = useContext(AuthContext); const { handleLogout, loading } = useContext(AuthContext);
const [drawerOpen, setDrawerOpen] = useLocalStorage("drawerOpen", true); const [drawerOpen, setDrawerOpen] = useState(false);
const { user } = useContext(AuthContext); const [drawerVariant, setDrawerVariant] = useState("permanent");
const { user } = useContext(AuthContext);
const handleMenu = event => { useEffect(() => {
setAnchorEl(event.currentTarget); if (document.body.offsetWidth > 600) {
setMenuOpen(true); setDrawerOpen(true);
}; }
}, []);
const handleCloseMenu = () => { useEffect(() => {
setAnchorEl(null); if (document.body.offsetWidth < 600) {
setMenuOpen(false); setDrawerVariant("temporary");
}; } else {
setDrawerVariant("permanent");
}
}, [drawerOpen]);
const handleOpenUserModal = () => { const handleMenu = (event) => {
setUserModalOpen(true); setAnchorEl(event.currentTarget);
handleCloseMenu(); setMenuOpen(true);
}; };
const handleClickLogout = () => { const handleCloseMenu = () => {
handleCloseMenu(); setAnchorEl(null);
handleLogout(); setMenuOpen(false);
}; };
if (loading) { const handleOpenUserModal = () => {
return <BackdropLoading />; setUserModalOpen(true);
} handleCloseMenu();
};
return ( const handleClickLogout = () => {
<div className={classes.root}> handleCloseMenu();
<Drawer handleLogout();
variant="permanent" };
classes={{
paper: clsx(
classes.drawerPaper,
!drawerOpen && classes.drawerPaperClose
),
}}
open={drawerOpen}
>
<div className={classes.toolbarIcon}>
<IconButton onClick={() => setDrawerOpen(!drawerOpen)}>
<ChevronLeftIcon />
</IconButton>
</div>
<Divider />
<List>
<MainListItems />
</List>
<Divider />
</Drawer>
<UserModal
open={userModalOpen}
onClose={() => setUserModalOpen(false)}
userId={user?.id}
/>
<AppBar
position="absolute"
className={clsx(classes.appBar, drawerOpen && classes.appBarShift)}
color={process.env.NODE_ENV === "development" ? "inherit" : "primary"}
>
<Toolbar variant="dense" className={classes.toolbar}>
<IconButton
edge="start"
color="inherit"
aria-label="open drawer"
onClick={() => setDrawerOpen(!drawerOpen)}
className={clsx(
classes.menuButton,
drawerOpen && classes.menuButtonHidden
)}
>
<MenuIcon />
</IconButton>
<Typography
component="h1"
variant="h6"
color="inherit"
noWrap
className={classes.title}
>
WhaTicket
</Typography>
{user.id && <NotificationsPopOver />}
<div> const drawerClose = () => {
<IconButton if (document.body.offsetWidth < 600) {
aria-label="account of current user" setDrawerOpen(false);
aria-controls="menu-appbar" }
aria-haspopup="true" };
onClick={handleMenu}
color="inherit"
>
<AccountCircle />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
getContentAnchorEl={null}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
open={menuOpen}
onClose={handleCloseMenu}
>
<MenuItem onClick={handleOpenUserModal}>
{i18n.t("mainDrawer.appBar.user.profile")}
</MenuItem>
<MenuItem onClick={handleClickLogout}>
{i18n.t("mainDrawer.appBar.user.logout")}
</MenuItem>
</Menu>
</div>
</Toolbar>
</AppBar>
<main className={classes.content}>
<div className={classes.appBarSpacer} />
{children ? children : null} if (loading) {
</main> return <BackdropLoading />;
</div> }
);
return (
<div className={classes.root}>
<Drawer
variant={drawerVariant}
className={drawerOpen ? classes.drawerPaper : classes.drawerPaperClose}
classes={{
paper: clsx(
classes.drawerPaper,
!drawerOpen && classes.drawerPaperClose
),
}}
open={drawerOpen}
>
<div className={classes.toolbarIcon}>
<IconButton onClick={() => setDrawerOpen(!drawerOpen)}>
<ChevronLeftIcon />
</IconButton>
</div>
<Divider />
<List>
<MainListItems drawerClose={drawerClose} />
</List>
<Divider />
</Drawer>
<UserModal
open={userModalOpen}
onClose={() => setUserModalOpen(false)}
userId={user?.id}
/>
<AppBar
position="absolute"
className={clsx(classes.appBar, drawerOpen && classes.appBarShift)}
color={process.env.NODE_ENV === "development" ? "inherit" : "primary"}
>
<Toolbar variant="dense" className={classes.toolbar}>
<IconButton
edge="start"
color="inherit"
aria-label="open drawer"
onClick={() => setDrawerOpen(!drawerOpen)}
className={clsx(
classes.menuButton,
drawerOpen && classes.menuButtonHidden
)}
>
<MenuIcon />
</IconButton>
<Typography
component="h1"
variant="h6"
color="inherit"
noWrap
className={classes.title}
>
WhaTicket
</Typography>
{user.id && <NotificationsPopOver />}
<div>
<IconButton
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleMenu}
color="inherit"
>
<AccountCircle />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
getContentAnchorEl={null}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
open={menuOpen}
onClose={handleCloseMenu}
>
<MenuItem onClick={handleOpenUserModal}>
{i18n.t("mainDrawer.appBar.user.profile")}
</MenuItem>
<MenuItem onClick={handleClickLogout}>
{i18n.t("mainDrawer.appBar.user.logout")}
</MenuItem>
</Menu>
</div>
</Toolbar>
</AppBar>
<main className={classes.content}>
<div className={classes.appBarSpacer} />
{children ? children : null}
</main>
</div>
);
}; };
export default LoggedInLayout; export default LoggedInLayout;

View File

@@ -36,314 +36,314 @@ import { AuthContext } from "../../context/Auth/AuthContext";
import { Can } from "../../components/Can"; import { Can } from "../../components/Can";
const reducer = (state, action) => { const reducer = (state, action) => {
if (action.type === "LOAD_CONTACTS") { if (action.type === "LOAD_CONTACTS") {
const contacts = action.payload; const contacts = action.payload;
const newContacts = []; const newContacts = [];
contacts.forEach(contact => { contacts.forEach((contact) => {
const contactIndex = state.findIndex(c => c.id === contact.id); const contactIndex = state.findIndex((c) => c.id === contact.id);
if (contactIndex !== -1) { if (contactIndex !== -1) {
state[contactIndex] = contact; state[contactIndex] = contact;
} else { } else {
newContacts.push(contact); newContacts.push(contact);
} }
}); });
return [...state, ...newContacts]; return [...state, ...newContacts];
} }
if (action.type === "UPDATE_CONTACTS") { if (action.type === "UPDATE_CONTACTS") {
const contact = action.payload; const contact = action.payload;
const contactIndex = state.findIndex(c => c.id === contact.id); const contactIndex = state.findIndex((c) => c.id === contact.id);
if (contactIndex !== -1) { if (contactIndex !== -1) {
state[contactIndex] = contact; state[contactIndex] = contact;
return [...state]; return [...state];
} else { } else {
return [contact, ...state]; return [contact, ...state];
} }
} }
if (action.type === "DELETE_CONTACT") { if (action.type === "DELETE_CONTACT") {
const contactId = action.payload; const contactId = action.payload;
const contactIndex = state.findIndex(c => c.id === contactId); const contactIndex = state.findIndex((c) => c.id === contactId);
if (contactIndex !== -1) { if (contactIndex !== -1) {
state.splice(contactIndex, 1); state.splice(contactIndex, 1);
} }
return [...state]; return [...state];
} }
if (action.type === "RESET") { if (action.type === "RESET") {
return []; return [];
} }
}; };
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles((theme) => ({
mainPaper: { mainPaper: {
flex: 1, flex: 1,
padding: theme.spacing(1), padding: theme.spacing(1),
overflowY: "scroll", overflowY: "scroll",
...theme.scrollbarStyles, ...theme.scrollbarStyles,
}, },
})); }));
const Contacts = () => { const Contacts = () => {
const classes = useStyles(); const classes = useStyles();
const history = useHistory(); const history = useHistory();
const { user } = useContext(AuthContext); const { user } = useContext(AuthContext);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [pageNumber, setPageNumber] = useState(1); const [pageNumber, setPageNumber] = useState(1);
const [searchParam, setSearchParam] = useState(""); const [searchParam, setSearchParam] = useState("");
const [contacts, dispatch] = useReducer(reducer, []); 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); const [hasMore, setHasMore] = useState(false);
useEffect(() => { useEffect(() => {
dispatch({ type: "RESET" }); dispatch({ type: "RESET" });
setPageNumber(1); setPageNumber(1);
}, [searchParam]); }, [searchParam]);
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
const delayDebounceFn = setTimeout(() => { const delayDebounceFn = setTimeout(() => {
const fetchContacts = async () => { const fetchContacts = async () => {
try { try {
const { data } = await api.get("/contacts/", { const { data } = await api.get("/contacts/", {
params: { searchParam, pageNumber }, params: { searchParam, pageNumber },
}); });
dispatch({ type: "LOAD_CONTACTS", payload: data.contacts }); dispatch({ type: "LOAD_CONTACTS", payload: data.contacts });
setHasMore(data.hasMore); setHasMore(data.hasMore);
setLoading(false); setLoading(false);
} catch (err) { } catch (err) {
toastError(err); toastError(err);
} }
}; };
fetchContacts(); fetchContacts();
}, 500); }, 500);
return () => clearTimeout(delayDebounceFn); return () => clearTimeout(delayDebounceFn);
}, [searchParam, pageNumber]); }, [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") {
dispatch({ type: "UPDATE_CONTACTS", payload: data.contact }); dispatch({ type: "UPDATE_CONTACTS", payload: data.contact });
} }
if (data.action === "delete") { if (data.action === "delete") {
dispatch({ type: "DELETE_CONTACT", payload: +data.contactId }); dispatch({ type: "DELETE_CONTACT", payload: +data.contactId });
} }
}); });
return () => { return () => {
socket.disconnect(); socket.disconnect();
}; };
}, []); }, []);
const handleSearch = event => { const handleSearch = (event) => {
setSearchParam(event.target.value.toLowerCase()); setSearchParam(event.target.value.toLowerCase());
}; };
const handleOpenContactModal = () => { const handleOpenContactModal = () => {
setSelectedContactId(null); setSelectedContactId(null);
setContactModalOpen(true); setContactModalOpen(true);
}; };
const handleCloseContactModal = () => { const handleCloseContactModal = () => {
setSelectedContactId(null); setSelectedContactId(null);
setContactModalOpen(false); setContactModalOpen(false);
}; };
const handleSaveTicket = async contactId => { const handleSaveTicket = async (contactId) => {
if (!contactId) return; if (!contactId) return;
setLoading(true); setLoading(true);
try { try {
const { data: ticket } = await api.post("/tickets", { const { data: ticket } = await api.post("/tickets", {
contactId: contactId, contactId: contactId,
userId: user?.id, userId: user?.id,
status: "open", status: "open",
}); });
history.push(`/tickets/${ticket.id}`); history.push(`/tickets/${ticket.id}`);
} catch (err) { } catch (err) {
toastError(err); toastError(err);
} }
setLoading(false); setLoading(false);
}; };
const hadleEditContact = contactId => { const hadleEditContact = (contactId) => {
setSelectedContactId(contactId); setSelectedContactId(contactId);
setContactModalOpen(true); setContactModalOpen(true);
}; };
const handleDeleteContact = async contactId => { const handleDeleteContact = async (contactId) => {
try { try {
await api.delete(`/contacts/${contactId}`); await api.delete(`/contacts/${contactId}`);
toast.success(i18n.t("contacts.toasts.deleted")); toast.success(i18n.t("contacts.toasts.deleted"));
} catch (err) { } catch (err) {
toastError(err); toastError(err);
} }
setDeletingContact(null); setDeletingContact(null);
setSearchParam(""); setSearchParam("");
setPageNumber(1); setPageNumber(1);
}; };
const handleimportContact = async () => { const handleimportContact = async () => {
try { try {
await api.post("/contacts/import"); await api.post("/contacts/import");
history.go(0); history.go(0);
} catch (err) { } catch (err) {
toastError(err); toastError(err);
} }
}; };
const loadMore = () => { const loadMore = () => {
setPageNumber(prevState => prevState + 1); setPageNumber((prevState) => prevState + 1);
}; };
const handleScroll = e => { const handleScroll = (e) => {
if (!hasMore || loading) return; if (!hasMore || loading) return;
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
if (scrollHeight - (scrollTop + 100) < clientHeight) { if (scrollHeight - (scrollTop + 100) < clientHeight) {
loadMore(); loadMore();
} }
}; };
return ( return (
<MainContainer className={classes.mainContainer}> <MainContainer className={classes.mainContainer}>
<ContactModal <ContactModal
open={contactModalOpen} open={contactModalOpen}
onClose={handleCloseContactModal} onClose={handleCloseContactModal}
aria-labelledby="form-dialog-title" aria-labelledby="form-dialog-title"
contactId={selectedContactId} contactId={selectedContactId}
></ContactModal> ></ContactModal>
<ConfirmationModal <ConfirmationModal
title={ title={
deletingContact deletingContact
? `${i18n.t("contacts.confirmationModal.deleteTitle")} ${ ? `${i18n.t("contacts.confirmationModal.deleteTitle")} ${
deletingContact.name deletingContact.name
}?` }?`
: `${i18n.t("contacts.confirmationModal.importTitlte")}` : `${i18n.t("contacts.confirmationModal.importTitlte")}`
} }
open={confirmOpen} open={confirmOpen}
onClose={setConfirmOpen} onClose={setConfirmOpen}
onConfirm={e => onConfirm={(e) =>
deletingContact deletingContact
? handleDeleteContact(deletingContact.id) ? handleDeleteContact(deletingContact.id)
: handleimportContact() : handleimportContact()
} }
> >
{deletingContact {deletingContact
? `${i18n.t("contacts.confirmationModal.deleteMessage")}` ? `${i18n.t("contacts.confirmationModal.deleteMessage")}`
: `${i18n.t("contacts.confirmationModal.importMessage")}`} : `${i18n.t("contacts.confirmationModal.importMessage")}`}
</ConfirmationModal> </ConfirmationModal>
<MainHeader> <MainHeader>
<Title>{i18n.t("contacts.title")}</Title> <Title>{i18n.t("contacts.title")}</Title>
<MainHeaderButtonsWrapper> <MainHeaderButtonsWrapper>
<TextField <TextField
placeholder={i18n.t("contacts.searchPlaceholder")} placeholder={i18n.t("contacts.searchPlaceholder")}
type="search" type="search"
value={searchParam} value={searchParam}
onChange={handleSearch} onChange={handleSearch}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
<SearchIcon style={{ color: "gray" }} /> <SearchIcon style={{ color: "gray" }} />
</InputAdornment> </InputAdornment>
), ),
}} }}
/> />
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={e => setConfirmOpen(true)} onClick={(e) => setConfirmOpen(true)}
> >
{i18n.t("contacts.buttons.import")} {i18n.t("contacts.buttons.import")}
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={handleOpenContactModal} onClick={handleOpenContactModal}
> >
{i18n.t("contacts.buttons.add")} {i18n.t("contacts.buttons.add")}
</Button> </Button>
</MainHeaderButtonsWrapper> </MainHeaderButtonsWrapper>
</MainHeader> </MainHeader>
<Paper <Paper
className={classes.mainPaper} className={classes.mainPaper}
variant="outlined" variant="outlined"
onScroll={handleScroll} onScroll={handleScroll}
> >
<Table size="small"> <Table size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell padding="checkbox" /> <TableCell padding="checkbox" />
<TableCell>{i18n.t("contacts.table.name")}</TableCell> <TableCell>{i18n.t("contacts.table.name")}</TableCell>
<TableCell align="center"> <TableCell align="center">
{i18n.t("contacts.table.whatsapp")} {i18n.t("contacts.table.whatsapp")}
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
{i18n.t("contacts.table.email")} {i18n.t("contacts.table.email")}
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
{i18n.t("contacts.table.actions")} {i18n.t("contacts.table.actions")}
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<> <>
{contacts.map(contact => ( {contacts.map((contact) => (
<TableRow key={contact.id}> <TableRow key={contact.id}>
<TableCell style={{ paddingRight: 0 }}> <TableCell style={{ paddingRight: 0 }}>
{<Avatar src={contact.profilePicUrl} />} {<Avatar src={contact.profilePicUrl} />}
</TableCell> </TableCell>
<TableCell>{contact.name}</TableCell> <TableCell>{contact.name}</TableCell>
<TableCell align="center">{contact.number}</TableCell> <TableCell align="center">{contact.number}</TableCell>
<TableCell align="center">{contact.email}</TableCell> <TableCell align="center">{contact.email}</TableCell>
<TableCell align="center"> <TableCell align="center">
<IconButton <IconButton
size="small" size="small"
onClick={() => handleSaveTicket(contact.id)} onClick={() => handleSaveTicket(contact.id)}
> >
<WhatsAppIcon /> <WhatsAppIcon />
</IconButton> </IconButton>
<IconButton <IconButton
size="small" size="small"
onClick={() => hadleEditContact(contact.id)} onClick={() => hadleEditContact(contact.id)}
> >
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
<Can <Can
role={user.profile} role={user.profile}
perform="contacts-page:deleteContact" perform="contacts-page:deleteContact"
yes={() => ( yes={() => (
<IconButton <IconButton
size="small" size="small"
onClick={e => { onClick={(e) => {
setConfirmOpen(true); setConfirmOpen(true);
setDeletingContact(contact); setDeletingContact(contact);
}} }}
> >
<DeleteOutlineIcon /> <DeleteOutlineIcon />
</IconButton> </IconButton>
)} )}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
{loading && <TableRowSkeleton avatar columns={3} />} {loading && <TableRowSkeleton avatar columns={3} />}
</> </>
</TableBody> </TableBody>
</Table> </Table>
</Paper> </Paper>
</MainContainer> </MainContainer>
); );
}; };
export default Contacts; export default Contacts;

View File

@@ -30,105 +30,105 @@ import { AuthContext } from "../../context/Auth/AuthContext";
// ); // );
// }; // };
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles((theme) => ({
paper: { paper: {
marginTop: theme.spacing(8), marginTop: theme.spacing(8),
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
}, },
avatar: { avatar: {
margin: theme.spacing(1), margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main, backgroundColor: theme.palette.secondary.main,
}, },
form: { form: {
width: "100%", // Fix IE 11 issue. width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(1), marginTop: theme.spacing(1),
}, },
submit: { submit: {
margin: theme.spacing(3, 0, 2), margin: theme.spacing(3, 0, 2),
}, },
})); }));
const Login = () => { const Login = () => {
const classes = useStyles(); const classes = useStyles();
const [user, setUser] = useState({ email: "", password: "" }); const [user, setUser] = useState({ email: "", password: "" });
const { handleLogin } = useContext(AuthContext); const { handleLogin } = useContext(AuthContext);
const handleChangeInput = e => { const handleChangeInput = (e) => {
setUser({ ...user, [e.target.name]: e.target.value }); setUser({ ...user, [e.target.name]: e.target.value });
}; };
const handlSubmit = e => { const handlSubmit = (e) => {
e.preventDefault(); e.preventDefault();
handleLogin(user); handleLogin(user);
}; };
return ( return (
<Container component="main" maxWidth="xs"> <Container component="main" maxWidth="xs">
<CssBaseline /> <CssBaseline />
<div className={classes.paper}> <div className={classes.paper}>
<Avatar className={classes.avatar}> <Avatar className={classes.avatar}>
<LockOutlinedIcon /> <LockOutlinedIcon />
</Avatar> </Avatar>
<Typography component="h1" variant="h5"> <Typography component="h1" variant="h5">
{i18n.t("login.title")} {i18n.t("login.title")}
</Typography> </Typography>
<form className={classes.form} noValidate onSubmit={handlSubmit}> <form className={classes.form} noValidate onSubmit={handlSubmit}>
<TextField <TextField
variant="outlined" variant="outlined"
margin="normal" margin="normal"
required required
fullWidth fullWidth
id="email" id="email"
label={i18n.t("login.form.email")} label={i18n.t("login.form.email")}
name="email" name="email"
value={user.email} value={user.email}
onChange={handleChangeInput} onChange={handleChangeInput}
autoComplete="email" autoComplete="email"
autoFocus autoFocus
/> />
<TextField <TextField
variant="outlined" variant="outlined"
margin="normal" margin="normal"
required required
fullWidth fullWidth
name="password" name="password"
label={i18n.t("login.form.password")} label={i18n.t("login.form.password")}
type="password" type="password"
id="password" id="password"
value={user.password} value={user.password}
onChange={handleChangeInput} onChange={handleChangeInput}
autoComplete="current-password" autoComplete="current-password"
/> />
<Button <Button
type="submit" type="submit"
fullWidth fullWidth
variant="contained" variant="contained"
color="primary" color="primary"
className={classes.submit} className={classes.submit}
> >
{i18n.t("login.buttons.submit")} {i18n.t("login.buttons.submit")}
</Button> </Button>
<Grid container> <Grid container>
<Grid item> <Grid item>
<Link <Link
href="#" href="#"
variant="body2" variant="body2"
component={RouterLink} component={RouterLink}
to="/signup" to="/signup"
> >
{i18n.t("login.buttons.register")} {i18n.t("login.buttons.register")}
</Link> </Link>
</Grid> </Grid>
</Grid> </Grid>
</form> </form>
</div> </div>
<Box mt={8}>{/* <Copyright /> */}</Box> <Box mt={8}>{/* <Copyright /> */}</Box>
</Container> </Container>
); );
}; };
export default Login; export default Login;

View File

@@ -3,16 +3,16 @@ import React, { useEffect, useReducer, useState } from "react";
import openSocket from "socket.io-client"; import openSocket from "socket.io-client";
import { import {
Button, Button,
IconButton, IconButton,
makeStyles, makeStyles,
Paper, Paper,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableHead, TableHead,
TableRow, TableRow,
Typography, Typography,
} from "@material-ui/core"; } from "@material-ui/core";
import MainContainer from "../../components/MainContainer"; import MainContainer from "../../components/MainContainer";
@@ -28,241 +28,241 @@ import QueueModal from "../../components/QueueModal";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import ConfirmationModal from "../../components/ConfirmationModal"; import ConfirmationModal from "../../components/ConfirmationModal";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles((theme) => ({
mainPaper: { mainPaper: {
flex: 1, flex: 1,
padding: theme.spacing(1), padding: theme.spacing(1),
overflowY: "scroll", overflowY: "scroll",
...theme.scrollbarStyles, ...theme.scrollbarStyles,
}, },
customTableCell: { customTableCell: {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
}, },
})); }));
const reducer = (state, action) => { const reducer = (state, action) => {
if (action.type === "LOAD_QUEUES") { if (action.type === "LOAD_QUEUES") {
const queues = action.payload; const queues = action.payload;
const newQueues = []; const newQueues = [];
queues.forEach(queue => { queues.forEach((queue) => {
const queueIndex = state.findIndex(q => q.id === queue.id); const queueIndex = state.findIndex((q) => q.id === queue.id);
if (queueIndex !== -1) { if (queueIndex !== -1) {
state[queueIndex] = queue; state[queueIndex] = queue;
} else { } else {
newQueues.push(queue); newQueues.push(queue);
} }
}); });
return [...state, ...newQueues]; return [...state, ...newQueues];
} }
if (action.type === "UPDATE_QUEUES") { if (action.type === "UPDATE_QUEUES") {
const queue = action.payload; const queue = action.payload;
const queueIndex = state.findIndex(u => u.id === queue.id); const queueIndex = state.findIndex((u) => u.id === queue.id);
if (queueIndex !== -1) { if (queueIndex !== -1) {
state[queueIndex] = queue; state[queueIndex] = queue;
return [...state]; return [...state];
} else { } else {
return [queue, ...state]; return [queue, ...state];
} }
} }
if (action.type === "DELETE_QUEUE") { if (action.type === "DELETE_QUEUE") {
const queueId = action.payload; const queueId = action.payload;
const queueIndex = state.findIndex(q => q.id === queueId); const queueIndex = state.findIndex((q) => q.id === queueId);
if (queueIndex !== -1) { if (queueIndex !== -1) {
state.splice(queueIndex, 1); state.splice(queueIndex, 1);
} }
return [...state]; return [...state];
} }
if (action.type === "RESET") { if (action.type === "RESET") {
return []; return [];
} }
}; };
const Queues = () => { const Queues = () => {
const classes = useStyles(); const classes = useStyles();
const [queues, dispatch] = useReducer(reducer, []); const [queues, dispatch] = useReducer(reducer, []);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [queueModalOpen, setQueueModalOpen] = useState(false); const [queueModalOpen, setQueueModalOpen] = useState(false);
const [selectedQueue, setSelectedQueue] = useState(null); const [selectedQueue, setSelectedQueue] = useState(null);
const [confirmModalOpen, setConfirmModalOpen] = useState(false); const [confirmModalOpen, setConfirmModalOpen] = useState(false);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
setLoading(true); setLoading(true);
try { try {
const { data } = await api.get("/queue"); const { data } = await api.get("/queue");
dispatch({ type: "LOAD_QUEUES", payload: data }); dispatch({ type: "LOAD_QUEUES", payload: data });
setLoading(false); setLoading(false);
} catch (err) { } catch (err) {
toastError(err); toastError(err);
setLoading(false); setLoading(false);
} }
})(); })();
}, []); }, []);
useEffect(() => { useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL); const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("queue", data => { socket.on("queue", (data) => {
if (data.action === "update" || data.action === "create") { if (data.action === "update" || data.action === "create") {
dispatch({ type: "UPDATE_QUEUES", payload: data.queue }); dispatch({ type: "UPDATE_QUEUES", payload: data.queue });
} }
if (data.action === "delete") { if (data.action === "delete") {
dispatch({ type: "DELETE_QUEUE", payload: data.queueId }); dispatch({ type: "DELETE_QUEUE", payload: data.queueId });
} }
}); });
return () => { return () => {
socket.disconnect(); socket.disconnect();
}; };
}, []); }, []);
const handleOpenQueueModal = () => { const handleOpenQueueModal = () => {
setQueueModalOpen(true); setQueueModalOpen(true);
setSelectedQueue(null); setSelectedQueue(null);
}; };
const handleCloseQueueModal = () => { const handleCloseQueueModal = () => {
setQueueModalOpen(false); setQueueModalOpen(false);
setSelectedQueue(null); setSelectedQueue(null);
}; };
const handleEditQueue = queue => { const handleEditQueue = (queue) => {
setSelectedQueue(queue); setSelectedQueue(queue);
setQueueModalOpen(true); setQueueModalOpen(true);
}; };
const handleCloseConfirmationModal = () => { const handleCloseConfirmationModal = () => {
setConfirmModalOpen(false); setConfirmModalOpen(false);
setSelectedQueue(null); setSelectedQueue(null);
}; };
const handleDeleteQueue = async queueId => { const handleDeleteQueue = async (queueId) => {
try { try {
await api.delete(`/queue/${queueId}`); await api.delete(`/queue/${queueId}`);
toast.success(i18n.t("Queue deleted successfully!")); toast.success(i18n.t("Queue deleted successfully!"));
} catch (err) { } catch (err) {
toastError(err); toastError(err);
} }
setSelectedQueue(null); setSelectedQueue(null);
}; };
return ( return (
<MainContainer> <MainContainer>
<ConfirmationModal <ConfirmationModal
title={ title={
selectedQueue && selectedQueue &&
`${i18n.t("queues.confirmationModal.deleteTitle")} ${ `${i18n.t("queues.confirmationModal.deleteTitle")} ${
selectedQueue.name selectedQueue.name
}?` }?`
} }
open={confirmModalOpen} open={confirmModalOpen}
onClose={handleCloseConfirmationModal} onClose={handleCloseConfirmationModal}
onConfirm={() => handleDeleteQueue(selectedQueue.id)} onConfirm={() => handleDeleteQueue(selectedQueue.id)}
> >
{i18n.t("queues.confirmationModal.deleteMessage")} {i18n.t("queues.confirmationModal.deleteMessage")}
</ConfirmationModal> </ConfirmationModal>
<QueueModal <QueueModal
open={queueModalOpen} open={queueModalOpen}
onClose={handleCloseQueueModal} onClose={handleCloseQueueModal}
queueId={selectedQueue?.id} queueId={selectedQueue?.id}
/> />
<MainHeader> <MainHeader>
<Title>{i18n.t("queues.title")}</Title> <Title>{i18n.t("queues.title")}</Title>
<MainHeaderButtonsWrapper> <MainHeaderButtonsWrapper>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={handleOpenQueueModal} onClick={handleOpenQueueModal}
> >
{i18n.t("queues.buttons.add")} {i18n.t("queues.buttons.add")}
</Button> </Button>
</MainHeaderButtonsWrapper> </MainHeaderButtonsWrapper>
</MainHeader> </MainHeader>
<Paper className={classes.mainPaper} variant="outlined"> <Paper className={classes.mainPaper} variant="outlined">
<Table size="small"> <Table size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell align="center"> <TableCell align="center">
{i18n.t("queues.table.name")} {i18n.t("queues.table.name")}
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
{i18n.t("queues.table.color")} {i18n.t("queues.table.color")}
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
{i18n.t("queues.table.greeting")} {i18n.t("queues.table.greeting")}
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
{i18n.t("queues.table.actions")} {i18n.t("queues.table.actions")}
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<> <>
{queues.map(queue => ( {queues.map((queue) => (
<TableRow key={queue.id}> <TableRow key={queue.id}>
<TableCell align="center">{queue.name}</TableCell> <TableCell align="center">{queue.name}</TableCell>
<TableCell align="center"> <TableCell align="center">
<div className={classes.customTableCell}> <div className={classes.customTableCell}>
<span <span
style={{ style={{
backgroundColor: queue.color, backgroundColor: queue.color,
width: 60, width: 60,
height: 20, height: 20,
alignSelf: "center", alignSelf: "center",
}} }}
/> />
</div> </div>
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
<div className={classes.customTableCell}> <div className={classes.customTableCell}>
<Typography <Typography
style={{ width: 300, align: "center" }} style={{ width: 300, align: "center" }}
noWrap noWrap
variant="body2" variant="body2"
> >
{queue.greetingMessage} {queue.greetingMessage}
</Typography> </Typography>
</div> </div>
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
<IconButton <IconButton
size="small" size="small"
onClick={() => handleEditQueue(queue)} onClick={() => handleEditQueue(queue)}
> >
<Edit /> <Edit />
</IconButton> </IconButton>
<IconButton <IconButton
size="small" size="small"
onClick={() => { onClick={() => {
setSelectedQueue(queue); setSelectedQueue(queue);
setConfirmModalOpen(true); setConfirmModalOpen(true);
}} }}
> >
<DeleteOutline /> <DeleteOutline />
</IconButton> </IconButton>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
{loading && <TableRowSkeleton columns={4} />} {loading && <TableRowSkeleton columns={4} />}
</> </>
</TableBody> </TableBody>
</Table> </Table>
</Paper> </Paper>
</MainContainer> </MainContainer>
); );
}; };
export default Queues; export default Queues;

View File

@@ -0,0 +1,288 @@
import React, { useState, useEffect, useReducer } from "react";
import openSocket from "socket.io-client";
import {
Button,
IconButton,
makeStyles,
Paper,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
InputAdornment,
TextField,
} from "@material-ui/core";
import { Edit, DeleteOutline } from "@material-ui/icons";
import SearchIcon from "@material-ui/icons/Search";
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 { i18n } from "../../translate/i18n";
import TableRowSkeleton from "../../components/TableRowSkeleton";
import QuickAnswersModal from "../../components/QuickAnswersModal";
import ConfirmationModal from "../../components/ConfirmationModal";
import { toast } from "react-toastify";
import toastError from "../../errors/toastError";
const reducer = (state, action) => {
if (action.type === "LOAD_QUICK_ANSWERS") {
const quickAnswers = action.payload;
const newQuickAnswers = [];
quickAnswers.forEach((quickAnswer) => {
const quickAnswerIndex = state.findIndex((q) => q.id === quickAnswer.id);
if (quickAnswerIndex !== -1) {
state[quickAnswerIndex] = quickAnswer;
} else {
newQuickAnswers.push(quickAnswer);
}
});
return [...state, ...newQuickAnswers];
}
if (action.type === "UPDATE_QUICK_ANSWERS") {
const quickAnswer = action.payload;
const quickAnswerIndex = state.findIndex((q) => q.id === quickAnswer.id);
if (quickAnswerIndex !== -1) {
state[quickAnswerIndex] = quickAnswer;
return [...state];
} else {
return [quickAnswer, ...state];
}
}
if (action.type === "DELETE_QUICK_ANSWERS") {
const quickAnswerId = action.payload;
const quickAnswerIndex = state.findIndex((q) => q.id === quickAnswerId);
if (quickAnswerIndex !== -1) {
state.splice(quickAnswerIndex, 1);
}
return [...state];
}
if (action.type === "RESET") {
return [];
}
};
const useStyles = makeStyles((theme) => ({
mainPaper: {
flex: 1,
padding: theme.spacing(1),
overflowY: "scroll",
...theme.scrollbarStyles,
},
}));
const QuickAnswers = () => {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [pageNumber, setPageNumber] = useState(1);
const [searchParam, setSearchParam] = useState("");
const [quickAnswers, dispatch] = useReducer(reducer, []);
const [selectedQuickAnswers, setSelectedQuickAnswers] = useState(null);
const [quickAnswersModalOpen, setQuickAnswersModalOpen] = useState(false);
const [deletingQuickAnswers, setDeletingQuickAnswers] = useState(null);
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
const [hasMore, setHasMore] = useState(false);
useEffect(() => {
dispatch({ type: "RESET" });
setPageNumber(1);
}, [searchParam]);
useEffect(() => {
setLoading(true);
const delayDebounceFn = setTimeout(() => {
const fetchQuickAnswers = async () => {
try {
const { data } = await api.get("/quickAnswers/", {
params: { searchParam, pageNumber },
});
dispatch({ type: "LOAD_QUICK_ANSWERS", payload: data.quickAnswers });
setHasMore(data.hasMore);
setLoading(false);
} catch (err) {
toastError(err);
}
};
fetchQuickAnswers();
}, 500);
return () => clearTimeout(delayDebounceFn);
}, [searchParam, pageNumber]);
useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("quickAnswer", (data) => {
if (data.action === "update" || data.action === "create") {
dispatch({ type: "UPDATE_QUICK_ANSWERS", payload: data.quickAnswer });
}
if (data.action === "delete") {
dispatch({
type: "DELETE_QUICK_ANSWERS",
payload: +data.quickAnswerId,
});
}
});
return () => {
socket.disconnect();
};
}, []);
const handleSearch = (event) => {
setSearchParam(event.target.value.toLowerCase());
};
const handleOpenQuickAnswersModal = () => {
setSelectedQuickAnswers(null);
setQuickAnswersModalOpen(true);
};
const handleCloseQuickAnswersModal = () => {
setSelectedQuickAnswers(null);
setQuickAnswersModalOpen(false);
};
const handleEditQuickAnswers = (quickAnswer) => {
setSelectedQuickAnswers(quickAnswer);
setQuickAnswersModalOpen(true);
};
const handleDeleteQuickAnswers = async (quickAnswerId) => {
try {
await api.delete(`/quickAnswers/${quickAnswerId}`);
toast.success(i18n.t("quickAnswers.toasts.deleted"));
} catch (err) {
toastError(err);
}
setDeletingQuickAnswers(null);
setSearchParam("");
setPageNumber(1);
};
const loadMore = () => {
setPageNumber((prevState) => prevState + 1);
};
const handleScroll = (e) => {
if (!hasMore || loading) return;
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
if (scrollHeight - (scrollTop + 100) < clientHeight) {
loadMore();
}
};
return (
<MainContainer>
<ConfirmationModal
title={
deletingQuickAnswers &&
`${i18n.t("quickAnswers.confirmationModal.deleteTitle")} ${
deletingQuickAnswers.shortcut
}?`
}
open={confirmModalOpen}
onClose={setConfirmModalOpen}
onConfirm={() => handleDeleteQuickAnswers(deletingQuickAnswers.id)}
>
{i18n.t("quickAnswers.confirmationModal.deleteMessage")}
</ConfirmationModal>
<QuickAnswersModal
open={quickAnswersModalOpen}
onClose={handleCloseQuickAnswersModal}
aria-labelledby="form-dialog-title"
quickAnswerId={selectedQuickAnswers && selectedQuickAnswers.id}
></QuickAnswersModal>
<MainHeader>
<Title>{i18n.t("quickAnswers.title")}</Title>
<MainHeaderButtonsWrapper>
<TextField
placeholder={i18n.t("quickAnswers.searchPlaceholder")}
type="search"
value={searchParam}
onChange={handleSearch}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{ color: "gray" }} />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
onClick={handleOpenQuickAnswersModal}
>
{i18n.t("quickAnswers.buttons.add")}
</Button>
</MainHeaderButtonsWrapper>
</MainHeader>
<Paper
className={classes.mainPaper}
variant="outlined"
onScroll={handleScroll}
>
<Table size="small">
<TableHead>
<TableRow>
<TableCell align="center">
{i18n.t("quickAnswers.table.shortcut")}
</TableCell>
<TableCell align="center">
{i18n.t("quickAnswers.table.message")}
</TableCell>
<TableCell align="center">
{i18n.t("quickAnswers.table.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<>
{quickAnswers.map((quickAnswer) => (
<TableRow key={quickAnswer.id}>
<TableCell align="center">{quickAnswer.shortcut}</TableCell>
<TableCell align="center">{quickAnswer.message}</TableCell>
<TableCell align="center">
<IconButton
size="small"
onClick={() => handleEditQuickAnswers(quickAnswer)}
>
<Edit />
</IconButton>
<IconButton
size="small"
onClick={(e) => {
setConfirmModalOpen(true);
setDeletingQuickAnswers(quickAnswer);
}}
>
<DeleteOutline />
</IconButton>
</TableCell>
</TableRow>
))}
{loading && <TableRowSkeleton columns={3} />}
</>
</TableBody>
</Table>
</Paper>
</MainContainer>
);
};
export default QuickAnswers;

View File

@@ -8,69 +8,98 @@ import TicketsManager from "../../components/TicketsManager/";
import Ticket from "../../components/Ticket/"; import Ticket from "../../components/Ticket/";
import { i18n } from "../../translate/i18n"; import { i18n } from "../../translate/i18n";
import Hidden from "@material-ui/core/Hidden";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles((theme) => ({
chatContainer: { chatContainer: {
flex: 1, flex: 1,
// backgroundColor: "#eee", // // backgroundColor: "#eee",
padding: theme.spacing(4), // padding: theme.spacing(4),
height: `calc(100% - 48px)`, height: `calc(100% - 48px)`,
overflowY: "hidden", overflowY: "hidden",
}, },
chatPapper: { chatPapper: {
// backgroundColor: "red", // backgroundColor: "red",
display: "flex", display: "flex",
height: "100%", height: "100%",
}, },
contactsWrapper: { contactsWrapper: {
display: "flex", display: "flex",
height: "100%", height: "100%",
flexDirection: "column", flexDirection: "column",
overflowY: "hidden", overflowY: "hidden",
}, },
messagessWrapper: { contactsWrapperSmall: {
display: "flex", display: "flex",
height: "100%", height: "100%",
flexDirection: "column", flexDirection: "column",
}, overflowY: "hidden",
welcomeMsg: { [theme.breakpoints.down("sm")]: {
backgroundColor: "#eee", display: "none",
display: "flex", },
justifyContent: "space-evenly", },
alignItems: "center", messagessWrapper: {
height: "100%", display: "flex",
textAlign: "center", height: "100%",
}, flexDirection: "column",
},
welcomeMsg: {
backgroundColor: "#eee",
display: "flex",
justifyContent: "space-evenly",
alignItems: "center",
height: "100%",
textAlign: "center",
borderRadius: 0,
},
ticketsManager: {},
ticketsManagerClosed: {
[theme.breakpoints.down("sm")]: {
display: "none",
},
},
})); }));
const Chat = () => { const Chat = () => {
const classes = useStyles(); const classes = useStyles();
const { ticketId } = useParams(); const { ticketId } = useParams();
return ( return (
<div className={classes.chatContainer}> <div className={classes.chatContainer}>
<div className={classes.chatPapper}> <div className={classes.chatPapper}>
<Grid container spacing={0}> <Grid container spacing={0}>
<Grid item xs={4} className={classes.contactsWrapper}> {/* <Grid item xs={4} className={classes.contactsWrapper}> */}
<TicketsManager /> <Grid
</Grid> item
<Grid item xs={8} className={classes.messagessWrapper}> xs={12}
{ticketId ? ( md={4}
<> className={
<Ticket /> ticketId ? classes.contactsWrapperSmall : classes.contactsWrapper
</> }
) : ( >
<Paper square variant="outlined" className={classes.welcomeMsg}> <TicketsManager />
<span>{i18n.t("chat.noTicketMessage")}</span> </Grid>
</Paper> <Grid item xs={12} md={8} className={classes.messagessWrapper}>
)} {/* <Grid item xs={8} className={classes.messagessWrapper}> */}
</Grid> {ticketId ? (
</Grid> <>
</div> <Ticket />
</div> </>
); ) : (
<Hidden only={["sm", "xs"]}>
<Paper className={classes.welcomeMsg}>
{/* <Paper square variant="outlined" className={classes.welcomeMsg}> */}
<span>{i18n.t("chat.noTicketMessage")}</span>
</Paper>
</Hidden>
)}
</Grid>
</Grid>
</div>
</div>
);
}; };
export default Chat; export default Chat;

View File

@@ -31,257 +31,257 @@ import ConfirmationModal from "../../components/ConfirmationModal";
import toastError from "../../errors/toastError"; import toastError from "../../errors/toastError";
const reducer = (state, action) => { const reducer = (state, action) => {
if (action.type === "LOAD_USERS") { if (action.type === "LOAD_USERS") {
const users = action.payload; const users = action.payload;
const newUsers = []; const newUsers = [];
users.forEach(user => { users.forEach((user) => {
const userIndex = state.findIndex(u => u.id === user.id); const userIndex = state.findIndex((u) => u.id === user.id);
if (userIndex !== -1) { if (userIndex !== -1) {
state[userIndex] = user; state[userIndex] = user;
} else { } else {
newUsers.push(user); newUsers.push(user);
} }
}); });
return [...state, ...newUsers]; return [...state, ...newUsers];
} }
if (action.type === "UPDATE_USERS") { if (action.type === "UPDATE_USERS") {
const user = action.payload; const user = action.payload;
const userIndex = state.findIndex(u => u.id === user.id); const userIndex = state.findIndex((u) => u.id === user.id);
if (userIndex !== -1) { if (userIndex !== -1) {
state[userIndex] = user; state[userIndex] = user;
return [...state]; return [...state];
} else { } else {
return [user, ...state]; return [user, ...state];
} }
} }
if (action.type === "DELETE_USER") { if (action.type === "DELETE_USER") {
const userId = action.payload; const userId = action.payload;
const userIndex = state.findIndex(u => u.id === userId); const userIndex = state.findIndex((u) => u.id === userId);
if (userIndex !== -1) { if (userIndex !== -1) {
state.splice(userIndex, 1); state.splice(userIndex, 1);
} }
return [...state]; return [...state];
} }
if (action.type === "RESET") { if (action.type === "RESET") {
return []; return [];
} }
}; };
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles((theme) => ({
mainPaper: { mainPaper: {
flex: 1, flex: 1,
padding: theme.spacing(1), padding: theme.spacing(1),
overflowY: "scroll", overflowY: "scroll",
...theme.scrollbarStyles, ...theme.scrollbarStyles,
}, },
})); }));
const Users = () => { const Users = () => {
const classes = useStyles(); const classes = useStyles();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [pageNumber, setPageNumber] = useState(1); const [pageNumber, setPageNumber] = useState(1);
const [hasMore, setHasMore] = useState(false); const [hasMore, setHasMore] = useState(false);
const [selectedUser, setSelectedUser] = useState(null); const [selectedUser, setSelectedUser] = useState(null);
const [deletingUser, setDeletingUser] = useState(null); const [deletingUser, setDeletingUser] = useState(null);
const [userModalOpen, setUserModalOpen] = useState(false); const [userModalOpen, setUserModalOpen] = useState(false);
const [confirmModalOpen, setConfirmModalOpen] = useState(false); const [confirmModalOpen, setConfirmModalOpen] = useState(false);
const [searchParam, setSearchParam] = useState(""); const [searchParam, setSearchParam] = useState("");
const [users, dispatch] = useReducer(reducer, []); const [users, dispatch] = useReducer(reducer, []);
useEffect(() => { useEffect(() => {
dispatch({ type: "RESET" }); dispatch({ type: "RESET" });
setPageNumber(1); setPageNumber(1);
}, [searchParam]); }, [searchParam]);
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
const delayDebounceFn = setTimeout(() => { const delayDebounceFn = setTimeout(() => {
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
const { data } = await api.get("/users/", { const { data } = await api.get("/users/", {
params: { searchParam, pageNumber }, params: { searchParam, pageNumber },
}); });
dispatch({ type: "LOAD_USERS", payload: data.users }); dispatch({ type: "LOAD_USERS", payload: data.users });
setHasMore(data.hasMore); setHasMore(data.hasMore);
setLoading(false); setLoading(false);
} catch (err) { } catch (err) {
toastError(err); toastError(err);
} }
}; };
fetchUsers(); fetchUsers();
}, 500); }, 500);
return () => clearTimeout(delayDebounceFn); return () => clearTimeout(delayDebounceFn);
}, [searchParam, pageNumber]); }, [searchParam, pageNumber]);
useEffect(() => { useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL); const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("user", data => { socket.on("user", (data) => {
if (data.action === "update" || data.action === "create") { if (data.action === "update" || data.action === "create") {
dispatch({ type: "UPDATE_USERS", payload: data.user }); dispatch({ type: "UPDATE_USERS", payload: data.user });
} }
if (data.action === "delete") { if (data.action === "delete") {
dispatch({ type: "DELETE_USER", payload: +data.userId }); dispatch({ type: "DELETE_USER", payload: +data.userId });
} }
}); });
return () => { return () => {
socket.disconnect(); socket.disconnect();
}; };
}, []); }, []);
const handleOpenUserModal = () => { const handleOpenUserModal = () => {
setSelectedUser(null); setSelectedUser(null);
setUserModalOpen(true); setUserModalOpen(true);
}; };
const handleCloseUserModal = () => { const handleCloseUserModal = () => {
setSelectedUser(null); setSelectedUser(null);
setUserModalOpen(false); setUserModalOpen(false);
}; };
const handleSearch = event => { const handleSearch = (event) => {
setSearchParam(event.target.value.toLowerCase()); setSearchParam(event.target.value.toLowerCase());
}; };
const handleEditUser = user => { const handleEditUser = (user) => {
setSelectedUser(user); setSelectedUser(user);
setUserModalOpen(true); setUserModalOpen(true);
}; };
const handleDeleteUser = async userId => { const handleDeleteUser = async (userId) => {
try { try {
await api.delete(`/users/${userId}`); await api.delete(`/users/${userId}`);
toast.success(i18n.t("users.toasts.deleted")); toast.success(i18n.t("users.toasts.deleted"));
} catch (err) { } catch (err) {
toastError(err); toastError(err);
} }
setDeletingUser(null); setDeletingUser(null);
setSearchParam(""); setSearchParam("");
setPageNumber(1); setPageNumber(1);
}; };
const loadMore = () => { const loadMore = () => {
setPageNumber(prevState => prevState + 1); setPageNumber((prevState) => prevState + 1);
}; };
const handleScroll = e => { const handleScroll = (e) => {
if (!hasMore || loading) return; if (!hasMore || loading) return;
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
if (scrollHeight - (scrollTop + 100) < clientHeight) { if (scrollHeight - (scrollTop + 100) < clientHeight) {
loadMore(); loadMore();
} }
}; };
return ( return (
<MainContainer> <MainContainer>
<ConfirmationModal <ConfirmationModal
title={ title={
deletingUser && deletingUser &&
`${i18n.t("users.confirmationModal.deleteTitle")} ${ `${i18n.t("users.confirmationModal.deleteTitle")} ${
deletingUser.name deletingUser.name
}?` }?`
} }
open={confirmModalOpen} open={confirmModalOpen}
onClose={setConfirmModalOpen} onClose={setConfirmModalOpen}
onConfirm={() => handleDeleteUser(deletingUser.id)} onConfirm={() => handleDeleteUser(deletingUser.id)}
> >
{i18n.t("users.confirmationModal.deleteMessage")} {i18n.t("users.confirmationModal.deleteMessage")}
</ConfirmationModal> </ConfirmationModal>
<UserModal <UserModal
open={userModalOpen} open={userModalOpen}
onClose={handleCloseUserModal} onClose={handleCloseUserModal}
aria-labelledby="form-dialog-title" aria-labelledby="form-dialog-title"
userId={selectedUser && selectedUser.id} userId={selectedUser && selectedUser.id}
/> />
<MainHeader> <MainHeader>
<Title>{i18n.t("users.title")}</Title> <Title>{i18n.t("users.title")}</Title>
<MainHeaderButtonsWrapper> <MainHeaderButtonsWrapper>
<TextField <TextField
placeholder={i18n.t("contacts.searchPlaceholder")} placeholder={i18n.t("contacts.searchPlaceholder")}
type="search" type="search"
value={searchParam} value={searchParam}
onChange={handleSearch} onChange={handleSearch}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
<SearchIcon style={{ color: "gray" }} /> <SearchIcon style={{ color: "gray" }} />
</InputAdornment> </InputAdornment>
), ),
}} }}
/> />
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={handleOpenUserModal} onClick={handleOpenUserModal}
> >
{i18n.t("users.buttons.add")} {i18n.t("users.buttons.add")}
</Button> </Button>
</MainHeaderButtonsWrapper> </MainHeaderButtonsWrapper>
</MainHeader> </MainHeader>
<Paper <Paper
className={classes.mainPaper} className={classes.mainPaper}
variant="outlined" variant="outlined"
onScroll={handleScroll} onScroll={handleScroll}
> >
<Table size="small"> <Table size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell align="center">{i18n.t("users.table.name")}</TableCell> <TableCell align="center">{i18n.t("users.table.name")}</TableCell>
<TableCell align="center"> <TableCell align="center">
{i18n.t("users.table.email")} {i18n.t("users.table.email")}
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
{i18n.t("users.table.profile")} {i18n.t("users.table.profile")}
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
{i18n.t("users.table.actions")} {i18n.t("users.table.actions")}
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<> <>
{users.map(user => ( {users.map((user) => (
<TableRow key={user.id}> <TableRow key={user.id}>
<TableCell align="center">{user.name}</TableCell> <TableCell align="center">{user.name}</TableCell>
<TableCell align="center">{user.email}</TableCell> <TableCell align="center">{user.email}</TableCell>
<TableCell align="center">{user.profile}</TableCell> <TableCell align="center">{user.profile}</TableCell>
<TableCell align="center"> <TableCell align="center">
<IconButton <IconButton
size="small" size="small"
onClick={() => handleEditUser(user)} onClick={() => handleEditUser(user)}
> >
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
<IconButton <IconButton
size="small" size="small"
onClick={e => { onClick={(e) => {
setConfirmModalOpen(true); setConfirmModalOpen(true);
setDeletingUser(user); setDeletingUser(user);
}} }}
> >
<DeleteOutlineIcon /> <DeleteOutlineIcon />
</IconButton> </IconButton>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
{loading && <TableRowSkeleton columns={4} />} {loading && <TableRowSkeleton columns={4} />}
</> </>
</TableBody> </TableBody>
</Table> </Table>
</Paper> </Paper>
</MainContainer> </MainContainer>
); );
}; };
export default Users; export default Users;

View File

@@ -5,32 +5,32 @@ import { AuthContext } from "../context/Auth/AuthContext";
import BackdropLoading from "../components/BackdropLoading"; import BackdropLoading from "../components/BackdropLoading";
const Route = ({ component: Component, isPrivate = false, ...rest }) => { const Route = ({ component: Component, isPrivate = false, ...rest }) => {
const { isAuth, loading } = useContext(AuthContext); const { isAuth, loading } = useContext(AuthContext);
if (!isAuth && isPrivate) { if (!isAuth && isPrivate) {
return ( return (
<> <>
{loading && <BackdropLoading />} {loading && <BackdropLoading />}
<Redirect to={{ pathname: "/login", state: { from: rest.location } }} /> <Redirect to={{ pathname: "/login", state: { from: rest.location } }} />
</> </>
); );
} }
if (isAuth && !isPrivate) { if (isAuth && !isPrivate) {
return ( return (
<> <>
{loading && <BackdropLoading />} {loading && <BackdropLoading />}
<Redirect to={{ pathname: "/", state: { from: rest.location } }} />; <Redirect to={{ pathname: "/", state: { from: rest.location } }} />;
</> </>
); );
} }
return ( return (
<> <>
{loading && <BackdropLoading />} {loading && <BackdropLoading />}
<RouterRoute {...rest} component={Component} /> <RouterRoute {...rest} component={Component} />
</> </>
); );
}; };
export default Route; export default Route;

View File

@@ -11,44 +11,51 @@ import Connections from "../pages/Connections/";
import Settings from "../pages/Settings/"; import Settings from "../pages/Settings/";
import Users from "../pages/Users"; import Users from "../pages/Users";
import Contacts from "../pages/Contacts/"; import Contacts from "../pages/Contacts/";
import QuickAnswers from "../pages/QuickAnswers/";
import Queues from "../pages/Queues/"; import Queues from "../pages/Queues/";
import { AuthProvider } from "../context/Auth/AuthContext"; import { AuthProvider } from "../context/Auth/AuthContext";
import { WhatsAppsProvider } from "../context/WhatsApp/WhatsAppsContext"; import { WhatsAppsProvider } from "../context/WhatsApp/WhatsAppsContext";
import Route from "./Route"; import Route from "./Route";
const Routes = () => { const Routes = () => {
return ( return (
<BrowserRouter> <BrowserRouter>
<AuthProvider> <AuthProvider>
<Switch> <Switch>
<Route exact path="/login" component={Login} /> <Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} /> <Route exact path="/signup" component={Signup} />
<WhatsAppsProvider> <WhatsAppsProvider>
<LoggedInLayout> <LoggedInLayout>
<Route exact path="/" component={Dashboard} isPrivate /> <Route exact path="/" component={Dashboard} isPrivate />
<Route <Route
exact exact
path="/tickets/:ticketId?" path="/tickets/:ticketId?"
component={Tickets} component={Tickets}
isPrivate isPrivate
/> />
<Route <Route
exact exact
path="/connections" path="/connections"
component={Connections} component={Connections}
isPrivate isPrivate
/> />
<Route exact path="/contacts" component={Contacts} isPrivate /> <Route exact path="/contacts" component={Contacts} isPrivate />
<Route exact path="/users" component={Users} isPrivate /> <Route exact path="/users" component={Users} isPrivate />
<Route exact path="/Settings" component={Settings} isPrivate /> <Route
<Route exact path="/Queues" component={Queues} isPrivate /> exact
</LoggedInLayout> path="/quickAnswers"
</WhatsAppsProvider> component={QuickAnswers}
</Switch> isPrivate
<ToastContainer autoClose={3000} /> />
</AuthProvider> <Route exact path="/Settings" component={Settings} isPrivate />
</BrowserRouter> <Route exact path="/Queues" component={Queues} isPrivate />
); </LoggedInLayout>
</WhatsAppsProvider>
</Switch>
<ToastContainer autoClose={3000} />
</AuthProvider>
</BrowserRouter>
);
}; };
export default Routes; export default Routes;

View File

@@ -1,412 +1,448 @@
const messages = { const messages = {
en: { en: {
translations: { translations: {
signup: { signup: {
title: "Sign up", title: "Sign up",
toasts: { toasts: {
success: "User created successfully! Please login!", success: "User created successfully! Please login!",
fail: "Error creating user. Check the reported data.", fail: "Error creating user. Check the reported data.",
}, },
form: { form: {
name: "Name", name: "Name",
email: "Email", email: "Email",
password: "Password", password: "Password",
}, },
buttons: { buttons: {
submit: "Register", submit: "Register",
login: "Already have an account? Log in!", login: "Already have an account? Log in!",
}, },
}, },
login: { login: {
title: "Login", title: "Login",
form: { form: {
email: "Email", email: "Email",
password: "Password", password: "Password",
}, },
buttons: { buttons: {
submit: "Enter", submit: "Enter",
register: "Don't have an account? Register!", register: "Don't have an account? Register!",
}, },
}, },
auth: { auth: {
toasts: { toasts: {
success: "Login successfully!", success: "Login successfully!",
}, },
}, },
dashboard: { dashboard: {
charts: { charts: {
perDay: { perDay: {
title: "Tickets today: ", title: "Tickets today: ",
}, },
}, },
}, },
connections: { connections: {
title: "Connections", title: "Connections",
toasts: { toasts: {
deleted: "WhatsApp connection deleted sucessfully!", deleted: "WhatsApp connection deleted sucessfully!",
}, },
confirmationModal: { confirmationModal: {
deleteTitle: "Delete", deleteTitle: "Delete",
deleteMessage: "Are you sure? It cannot be reverted.", deleteMessage: "Are you sure? It cannot be reverted.",
disconnectTitle: "Disconnect", disconnectTitle: "Disconnect",
disconnectMessage: "Are you sure? You'll need to read QR Code again.", disconnectMessage: "Are you sure? You'll need to read QR Code again.",
}, },
buttons: { buttons: {
add: "Add WhatsApp", add: "Add WhatsApp",
disconnect: "Disconnect", disconnect: "Disconnect",
tryAgain: "Try Again", tryAgain: "Try Again",
qrcode: "QR CODE", qrcode: "QR CODE",
newQr: "New QR CODE", newQr: "New QR CODE",
connecting: "Connectiing", connecting: "Connectiing",
}, },
toolTips: { toolTips: {
disconnected: { disconnected: {
title: "Failed to start WhatsApp session", title: "Failed to start WhatsApp session",
content: content:
"Make sure your cell phone is connected to the internet and try again, or request a new QR Code", "Make sure your cell phone is connected to the internet and try again, or request a new QR Code",
}, },
qrcode: { qrcode: {
title: "Waiting for QR Code read", title: "Waiting for QR Code read",
content: content:
"Click on 'QR CODE' button and read the QR Code with your cell phone to start session", "Click on 'QR CODE' button and read the QR Code with your cell phone to start session",
}, },
connected: { connected: {
title: "Connection established", title: "Connection established",
}, },
timeout: { timeout: {
title: "Connection with cell phone has been lost", title: "Connection with cell phone has been lost",
content: content:
"Make sure your cell phone is connected to the internet and WhatsApp is open, or click on 'Disconnect' button to get a new QRcode", "Make sure your cell phone is connected to the internet and WhatsApp is open, or click on 'Disconnect' button to get a new QRcode",
}, },
}, },
table: { table: {
name: "Name", name: "Name",
status: "Status", status: "Status",
lastUpdate: "Last Update", lastUpdate: "Last Update",
default: "Default", default: "Default",
actions: "Actions", actions: "Actions",
session: "Session", session: "Session",
}, },
}, },
whatsappModal: { whatsappModal: {
title: { title: {
add: "Add WhatsApp", add: "Add WhatsApp",
edit: "Edit WhatsApp", edit: "Edit WhatsApp",
}, },
form: { form: {
name: "Name", name: "Name",
default: "Default", default: "Default",
}, },
buttons: { buttons: {
okAdd: "Add", okAdd: "Add",
okEdit: "Save", okEdit: "Save",
cancel: "Cancel", cancel: "Cancel",
}, },
success: "WhatsApp saved successfully.", success: "WhatsApp saved successfully.",
}, },
qrCode: { qrCode: {
message: "Read QrCode to start the session", message: "Read QrCode to start the session",
}, },
contacts: { contacts: {
title: "Contacts", title: "Contacts",
toasts: { toasts: {
deleted: "Contact deleted sucessfully!", deleted: "Contact deleted sucessfully!",
}, },
searchPlaceholder: "Search ...", searchPlaceholder: "Search ...",
confirmationModal: { confirmationModal: {
deleteTitle: "Delete", deleteTitle: "Delete",
importTitlte: "Import contacts", importTitlte: "Import contacts",
deleteMessage: deleteMessage:
"Are you sure you want to delete this contact? All related tickets will be lost.", "Are you sure you want to delete this contact? All related tickets will be lost.",
importMessage: "Do you want to import all contacts from the phone?", importMessage: "Do you want to import all contacts from the phone?",
}, },
buttons: { buttons: {
import: "Import Contacts", import: "Import Contacts",
add: "Add Contact", add: "Add Contact",
}, },
table: { table: {
name: "Name", name: "Name",
whatsapp: "WhatsApp", whatsapp: "WhatsApp",
email: "Email", email: "Email",
actions: "Actions", actions: "Actions",
}, },
}, },
contactModal: { contactModal: {
title: { title: {
add: "Add contact", add: "Add contact",
edit: "Edit contact", edit: "Edit contact",
}, },
form: { form: {
mainInfo: "Contact details", mainInfo: "Contact details",
extraInfo: "Additional information", extraInfo: "Additional information",
name: "Name", name: "Name",
number: "Whatsapp number", number: "Whatsapp number",
email: "Email", email: "Email",
extraName: "Field name", extraName: "Field name",
extraValue: "Value", extraValue: "Value",
}, },
buttons: { buttons: {
addExtraInfo: "Add information", addExtraInfo: "Add information",
okAdd: "Add", okAdd: "Add",
okEdit: "Save", okEdit: "Save",
cancel: "Cancel", cancel: "Cancel",
}, },
success: "Contact saved successfully.", success: "Contact saved successfully.",
}, },
queueModal: { quickAnswersModal: {
title: { title: {
add: "Add queue", add: "Add Quick Reply",
edit: "Edit queue", edit: "Edit Quick Answer",
}, },
form: { form: {
name: "Name", shortcut: "Shortcut",
color: "Color", message: "Quick Reply",
greetingMessage: "Greeting Message", },
}, buttons: {
buttons: { okAdd: "Add",
okAdd: "Add", okEdit: "Save",
okEdit: "Save", cancel: "Cancel",
cancel: "Cancel", },
}, success: "Quick Reply saved successfully.",
}, },
userModal: { queueModal: {
title: { title: {
add: "Add user", add: "Add queue",
edit: "Edit user", edit: "Edit queue",
}, },
form: { form: {
name: "Name", name: "Name",
email: "Email", color: "Color",
password: "Password", greetingMessage: "Greeting Message",
profile: "Profile", },
}, buttons: {
buttons: { okAdd: "Add",
okAdd: "Add", okEdit: "Save",
okEdit: "Save", cancel: "Cancel",
cancel: "Cancel", },
}, },
success: "User saved successfully.", userModal: {
}, title: {
chat: { add: "Add user",
noTicketMessage: "Select a ticket to start chatting.", edit: "Edit user",
}, },
ticketsManager: { form: {
buttons: { name: "Name",
newTicket: "New", email: "Email",
}, password: "Password",
}, profile: "Profile",
ticketsQueueSelect: { },
placeholder: "Queues", buttons: {
}, okAdd: "Add",
tickets: { okEdit: "Save",
toasts: { cancel: "Cancel",
deleted: "The ticket you were on has been deleted.", },
}, success: "User saved successfully.",
notification: { },
message: "Message from", chat: {
}, noTicketMessage: "Select a ticket to start chatting.",
tabs: { },
open: { title: "Inbox" }, ticketsManager: {
closed: { title: "Resolved" }, buttons: {
search: { title: "Search" }, newTicket: "New",
}, },
search: { },
placeholder: "Search tickets and messages.", ticketsQueueSelect: {
}, placeholder: "Queues",
buttons: { },
showAll: "All", tickets: {
}, toasts: {
}, deleted: "The ticket you were on has been deleted.",
transferTicketModal: { },
title: "Transfer Ticket", notification: {
fieldLabel: "Type to search for users", message: "Message from",
noOptions: "No user found with this name", },
buttons: { tabs: {
ok: "Transfer", open: { title: "Inbox" },
cancel: "Cancel", closed: { title: "Resolved" },
}, search: { title: "Search" },
}, },
ticketsList: { search: {
pendingHeader: "Queue", placeholder: "Search tickets and messages.",
assignedHeader: "Working on", },
noTicketsTitle: "Nothing here!", buttons: {
noTicketsMessage: "No tickets found with this status or search term.", showAll: "All",
buttons: { },
accept: "Accept", },
}, transferTicketModal: {
}, title: "Transfer Ticket",
newTicketModal: { fieldLabel: "Type to search for users",
title: "Create Ticket", noOptions: "No user found with this name",
fieldLabel: "Type to search for a contact", buttons: {
add: "Add", ok: "Transfer",
buttons: { cancel: "Cancel",
ok: "Save", },
cancel: "Cancel", },
}, ticketsList: {
}, pendingHeader: "Queue",
mainDrawer: { assignedHeader: "Working on",
listItems: { noTicketsTitle: "Nothing here!",
dashboard: "Dashboard", noTicketsMessage: "No tickets found with this status or search term.",
connections: "Connections", buttons: {
tickets: "Tickets", accept: "Accept",
contacts: "Contacts", },
queues: "Queues", },
administration: "Administration", newTicketModal: {
users: "Users", title: "Create Ticket",
settings: "Settings", fieldLabel: "Type to search for a contact",
}, add: "Add",
appBar: { buttons: {
user: { ok: "Save",
profile: "Profile", cancel: "Cancel",
logout: "Logout", },
}, },
}, mainDrawer: {
}, listItems: {
notifications: { dashboard: "Dashboard",
noTickets: "No notifications.", connections: "Connections",
}, tickets: "Tickets",
queues: { contacts: "Contacts",
title: "Queues", quickAnswers: "Quick Answers",
table: { queues: "Queues",
name: "Name", administration: "Administration",
color: "Color", users: "Users",
greeting: "Greeting message", settings: "Settings",
actions: "Actions", },
}, appBar: {
buttons: { user: {
add: "Add queue", profile: "Profile",
}, logout: "Logout",
confirmationModal: { },
deleteTitle: "Delete", },
deleteMessage: },
"Are you sure? It cannot be reverted! Tickets in this queue will still exist, but will not have any queues assigned.", notifications: {
}, noTickets: "No notifications.",
}, },
queueSelect: { queues: {
inputLabel: "Queues", title: "Queues",
}, table: {
users: { name: "Name",
title: "Users", color: "Color",
table: { greeting: "Greeting message",
name: "Name", actions: "Actions",
email: "Email", },
profile: "Profile", buttons: {
actions: "Actions", add: "Add queue",
}, },
buttons: { confirmationModal: {
add: "Add user", deleteTitle: "Delete",
}, deleteMessage:
toasts: { "Are you sure? It cannot be reverted! Tickets in this queue will still exist, but will not have any queues assigned.",
deleted: "User deleted sucessfully.", },
}, },
confirmationModal: { queueSelect: {
deleteTitle: "Delete", inputLabel: "Queues",
deleteMessage: },
"All user data will be lost. Users' open tickets will be moved to queue.", quickAnswers: {
}, title: "Quick Answers",
}, table: {
settings: { shortcut: "Shortcut",
success: "Settings saved successfully.", message: "Quick Reply",
title: "Settings", actions: "Actions",
settings: { },
userCreation: { buttons: {
name: "User creation", add: "Add Quick Reply",
options: { },
enabled: "Enabled", toasts: {
disabled: "Disabled", deleted: "Quick Reply deleted successfully.",
}, },
}, searchPlaceholder: "Search...",
}, confirmationModal: {
}, deleteTitle: "Are you sure you want to delete this Quick Reply: ",
messagesList: { deleteMessage: "This action cannot be undone.",
header: { },
assignedTo: "Assigned to:", },
buttons: { users: {
return: "Return", title: "Users",
resolve: "Resolve", table: {
reopen: "Reopen", name: "Name",
accept: "Accept", email: "Email",
}, profile: "Profile",
}, actions: "Actions",
}, },
messagesInput: { buttons: {
placeholderOpen: "Type a message", add: "Add user",
placeholderClosed: "Reopen or accept this ticket to send a message.", },
signMessage: "Sign", toasts: {
}, deleted: "User deleted sucessfully.",
contactDrawer: { },
header: "Contact details", confirmationModal: {
buttons: { deleteTitle: "Delete",
edit: "Edit contact", deleteMessage:
}, "All user data will be lost. Users' open tickets will be moved to queue.",
extraInfo: "Other information", },
}, },
ticketOptionsMenu: { settings: {
delete: "Delete", success: "Settings saved successfully.",
transfer: "Transfer", title: "Settings",
confirmationModal: { settings: {
title: "Delete ticket #", userCreation: {
titleFrom: "from contact ", name: "User creation",
message: "Attention! All ticket's related messages will be lost.", options: {
}, enabled: "Enabled",
buttons: { disabled: "Disabled",
delete: "Delete", },
cancel: "Cancel", },
}, },
}, },
confirmationModal: { messagesList: {
buttons: { header: {
confirm: "Ok", assignedTo: "Assigned to:",
cancel: "Cancel", buttons: {
}, return: "Return",
}, resolve: "Resolve",
messageOptionsMenu: { reopen: "Reopen",
delete: "Delete", accept: "Accept",
reply: "Reply", },
confirmationModal: { },
title: "Delete message?", },
message: "This action cannot be reverted.", messagesInput: {
}, placeholderOpen: "Type a message",
}, placeholderClosed: "Reopen or accept this ticket to send a message.",
backendErrors: { signMessage: "Sign",
ERR_NO_OTHER_WHATSAPP: },
"There must be at lest one default WhatsApp connection.", contactDrawer: {
ERR_NO_DEF_WAPP_FOUND: header: "Contact details",
"No default WhatsApp found. Check connections page.", buttons: {
ERR_WAPP_NOT_INITIALIZED: edit: "Edit contact",
"This WhatsApp session is not initialized. Check connections page.", },
ERR_WAPP_CHECK_CONTACT: extraInfo: "Other information",
"Could not check WhatsApp contact. Check connections page.", },
ERR_WAPP_INVALID_CONTACT: "This is not a valid whatsapp number.", ticketOptionsMenu: {
ERR_WAPP_DOWNLOAD_MEDIA: delete: "Delete",
"Could not download media from WhatsApp. Check connections page.", transfer: "Transfer",
ERR_INVALID_CREDENTIALS: "Authentication error. Please try again.", confirmationModal: {
ERR_SENDING_WAPP_MSG: title: "Delete ticket #",
"Error sending WhatsApp message. Check connections page.", titleFrom: "from contact ",
ERR_DELETE_WAPP_MSG: "Couldn't delete message from WhatsApp.", message: "Attention! All ticket's related messages will be lost.",
ERR_OTHER_OPEN_TICKET: },
"There's already an open ticket for this contact.", buttons: {
ERR_SESSION_EXPIRED: "Session expired. Please login.", delete: "Delete",
ERR_USER_CREATION_DISABLED: cancel: "Cancel",
"User creation was disabled by administrator.", },
ERR_NO_PERMISSION: "You don't have permission to access this resource.", },
ERR_DUPLICATED_CONTACT: "A contact with this number already exists.", confirmationModal: {
ERR_NO_SETTING_FOUND: "No setting found with this ID.", buttons: {
ERR_NO_CONTACT_FOUND: "No contact found with this ID.", confirm: "Ok",
ERR_NO_TICKET_FOUND: "No ticket found with this ID.", cancel: "Cancel",
ERR_NO_USER_FOUND: "No user found with this ID.", },
ERR_NO_WAPP_FOUND: "No WhatsApp found with this ID.", },
ERR_CREATING_MESSAGE: "Error while creating message on database.", messageOptionsMenu: {
ERR_CREATING_TICKET: "Error while creating ticket on database.", delete: "Delete",
ERR_FETCH_WAPP_MSG: reply: "Reply",
"Error fetching the message in WhtasApp, maybe it is too old.", confirmationModal: {
ERR_QUEUE_COLOR_ALREADY_EXISTS: title: "Delete message?",
"This color is already in use, pick another one.", message: "This action cannot be reverted.",
ERR_WAPP_GREETING_REQUIRED: },
"Greeting message is required if there is more than one queue.", },
}, backendErrors: {
}, ERR_NO_OTHER_WHATSAPP:
}, "There must be at lest one default WhatsApp connection.",
ERR_NO_DEF_WAPP_FOUND:
"No default WhatsApp found. Check connections page.",
ERR_WAPP_NOT_INITIALIZED:
"This WhatsApp session is not initialized. Check connections page.",
ERR_WAPP_CHECK_CONTACT:
"Could not check WhatsApp contact. Check connections page.",
ERR_WAPP_INVALID_CONTACT: "This is not a valid whatsapp number.",
ERR_WAPP_DOWNLOAD_MEDIA:
"Could not download media from WhatsApp. Check connections page.",
ERR_INVALID_CREDENTIALS: "Authentication error. Please try again.",
ERR_SENDING_WAPP_MSG:
"Error sending WhatsApp message. Check connections page.",
ERR_DELETE_WAPP_MSG: "Couldn't delete message from WhatsApp.",
ERR_OTHER_OPEN_TICKET:
"There's already an open ticket for this contact.",
ERR_SESSION_EXPIRED: "Session expired. Please login.",
ERR_USER_CREATION_DISABLED:
"User creation was disabled by administrator.",
ERR_NO_PERMISSION: "You don't have permission to access this resource.",
ERR_DUPLICATED_CONTACT: "A contact with this number already exists.",
ERR_NO_SETTING_FOUND: "No setting found with this ID.",
ERR_NO_CONTACT_FOUND: "No contact found with this ID.",
ERR_NO_TICKET_FOUND: "No ticket found with this ID.",
ERR_NO_USER_FOUND: "No user found with this ID.",
ERR_NO_WAPP_FOUND: "No WhatsApp found with this ID.",
ERR_CREATING_MESSAGE: "Error while creating message on database.",
ERR_CREATING_TICKET: "Error while creating ticket on database.",
ERR_FETCH_WAPP_MSG:
"Error fetching the message in WhtasApp, maybe it is too old.",
ERR_QUEUE_COLOR_ALREADY_EXISTS:
"This color is already in use, pick another one.",
ERR_WAPP_GREETING_REQUIRED:
"Greeting message is required if there is more than one queue.",
},
},
},
}; };
export { messages }; export { messages };

View File

@@ -1,418 +1,455 @@
const messages = { const messages = {
es: { es: {
translations: { translations: {
signup: { signup: {
title: "Registro", title: "Registro",
toasts: { toasts: {
success: success:
"¡El usuario ha sido creado satisfactoriamente! ¡Ahora inicia sesión!", "¡El usuario ha sido creado satisfactoriamente! ¡Ahora inicia sesión!",
fail: "Error creando el usuario. Verifica la data reportada.", fail: "Error creando el usuario. Verifica la data reportada.",
}, },
form: { form: {
name: "Nombre", name: "Nombre",
email: "Correo Electrónico", email: "Correo Electrónico",
password: "Contraseña", password: "Contraseña",
}, },
buttons: { buttons: {
submit: "Regístrate", submit: "Regístrate",
login: "¿Ya tienes una cuenta? ¡Inicia sesión!", login: "¿Ya tienes una cuenta? ¡Inicia sesión!",
}, },
}, },
login: { login: {
title: "Inicio de Sesión", title: "Inicio de Sesión",
form: { form: {
email: "Correo Electrónico", email: "Correo Electrónico",
password: "Contraseña", password: "Contraseña",
}, },
buttons: { buttons: {
submit: "Ingresa", submit: "Ingresa",
register: "¿No tienes cuenta? ¡Regístrate!", register: "¿No tienes cuenta? ¡Regístrate!",
}, },
}, },
auth: { auth: {
toasts: { toasts: {
success: "¡Inicio de sesión exitoso!", success: "¡Inicio de sesión exitoso!",
}, },
}, },
dashboard: { dashboard: {
charts: { charts: {
perDay: { perDay: {
title: "Tickets hoy: ", title: "Tickets hoy: ",
}, },
}, },
}, },
connections: { connections: {
title: "Conexiones", title: "Conexiones",
toasts: { toasts: {
deleted: deleted:
"¡La conexión de WhatsApp ha sido borrada satisfactoriamente!", "¡La conexión de WhatsApp ha sido borrada satisfactoriamente!",
}, },
confirmationModal: { confirmationModal: {
deleteTitle: "Borrar", deleteTitle: "Borrar",
deleteMessage: "¿Estás seguro? Este proceso no puede ser revertido.", deleteMessage: "¿Estás seguro? Este proceso no puede ser revertido.",
disconnectTitle: "Desconectar", disconnectTitle: "Desconectar",
disconnectMessage: "Estás seguro? Deberá volver a leer el código QR", disconnectMessage: "Estás seguro? Deberá volver a leer el código QR",
}, },
buttons: { buttons: {
add: "Agrega WhatsApp", add: "Agrega WhatsApp",
disconnect: "Desconectar", disconnect: "Desconectar",
tryAgain: "Inténtalo de nuevo", tryAgain: "Inténtalo de nuevo",
qrcode: "QR CODE", qrcode: "QR CODE",
newQr: "Nuevo QR CODE", newQr: "Nuevo QR CODE",
connecting: "Conectando", connecting: "Conectando",
}, },
toolTips: { toolTips: {
disconnected: { disconnected: {
title: "No se pudo iniciar la sesión de WhatsApp", title: "No se pudo iniciar la sesión de WhatsApp",
content: content:
"Asegúrese de que su teléfono celular esté conectado a Internet y vuelva a intentarlo o solicite un nuevo código QR", "Asegúrese de que su teléfono celular esté conectado a Internet y vuelva a intentarlo o solicite un nuevo código QR",
}, },
qrcode: { qrcode: {
title: "Esperando la lectura del código QR", title: "Esperando la lectura del código QR",
content: content:
"Haga clic en el botón 'CÓDIGO QR' y lea el Código QR con su teléfono celular para iniciar la sesión", "Haga clic en el botón 'CÓDIGO QR' y lea el Código QR con su teléfono celular para iniciar la sesión",
}, },
connected: { connected: {
title: "Conexión establecida", title: "Conexión establecida",
}, },
timeout: { timeout: {
title: "Se perdió la conexión con el teléfono celular", title: "Se perdió la conexión con el teléfono celular",
content: content:
"Asegúrese de que su teléfono celular esté conectado a Internet y que WhatsApp esté abierto, o haga clic en el botón 'Desconectar' para obtener un nuevo código QR", "Asegúrese de que su teléfono celular esté conectado a Internet y que WhatsApp esté abierto, o haga clic en el botón 'Desconectar' para obtener un nuevo código QR",
}, },
}, },
table: { table: {
name: "Nombre", name: "Nombre",
status: "Estado", status: "Estado",
lastUpdate: "Última Actualización", lastUpdate: "Última Actualización",
default: "Por Defecto", default: "Por Defecto",
actions: "Acciones", actions: "Acciones",
session: "Sesión", session: "Sesión",
}, },
}, },
whatsappModal: { whatsappModal: {
title: { title: {
add: "Agrega WhatsApp", add: "Agrega WhatsApp",
edit: "Edita WhatsApp", edit: "Edita WhatsApp",
}, },
form: { form: {
name: "Nombre", name: "Nombre",
default: "Por Defecto", default: "Por Defecto",
}, },
buttons: { buttons: {
okAdd: "Agregar", okAdd: "Agregar",
okEdit: "Guardar", okEdit: "Guardar",
cancel: "Cancelar", cancel: "Cancelar",
}, },
success: "WhatsApp guardado satisfactoriamente.", success: "WhatsApp guardado satisfactoriamente.",
}, },
qrCode: { qrCode: {
message: "Lée el código QR para empezar la sesión.", message: "Lée el código QR para empezar la sesión.",
}, },
contacts: { contacts: {
title: "Contactos", title: "Contactos",
toasts: { toasts: {
deleted: "¡Contacto borrado satisfactoriamente!", deleted: "¡Contacto borrado satisfactoriamente!",
}, },
searchPlaceholder: "Buscar...", searchPlaceholder: "Buscar...",
confirmationModal: { confirmationModal: {
deleteTitle: "Borrar", deleteTitle: "Borrar",
importTitlte: "Importar contactos", importTitlte: "Importar contactos",
deleteMessage: deleteMessage:
"¿Estás seguro que deseas borrar este contacto? Todos los tickets relacionados se perderán.", "¿Estás seguro que deseas borrar este contacto? Todos los tickets relacionados se perderán.",
importMessage: importMessage:
"¿Quieres importar todos los contactos desde tu teléfono?", "¿Quieres importar todos los contactos desde tu teléfono?",
}, },
buttons: { buttons: {
import: "Importar Contactos", import: "Importar Contactos",
add: "Agregar Contacto", add: "Agregar Contacto",
}, },
table: { table: {
name: "Nombre", name: "Nombre",
whatsapp: "WhatsApp", whatsapp: "WhatsApp",
email: "Correo Electrónico", email: "Correo Electrónico",
actions: "Acciones", actions: "Acciones",
}, },
}, },
contactModal: { contactModal: {
title: { title: {
add: "Agregar contacto", add: "Agregar contacto",
edit: "Editar contacto", edit: "Editar contacto",
}, },
form: { form: {
mainInfo: "Detalles del contacto", mainInfo: "Detalles del contacto",
extraInfo: "Información adicional", extraInfo: "Información adicional",
name: "Nombre", name: "Nombre",
number: "Número de Whatsapp", number: "Número de Whatsapp",
email: "Correo Electrónico", email: "Correo Electrónico",
extraName: "Nombre del Campo", extraName: "Nombre del Campo",
extraValue: "Valor", extraValue: "Valor",
}, },
buttons: { buttons: {
addExtraInfo: "Agregar información", addExtraInfo: "Agregar información",
okAdd: "Agregar", okAdd: "Agregar",
okEdit: "Guardar", okEdit: "Guardar",
cancel: "Cancelar", cancel: "Cancelar",
}, },
success: "Contacto guardado satisfactoriamente.", success: "Contacto guardado satisfactoriamente.",
}, },
queueModal: { quickAnswersModal: {
title: { title: {
add: "Agregar cola", add: "Agregar respuesta rápida",
edit: "Editar cola", edit: "Editar respuesta rápida",
}, },
form: { form: {
name: "Nombre", shortcut: "Atajo",
color: "Color", message: "Respuesta rápida",
greetingMessage: "Mensaje de saludo", },
}, buttons: {
buttons: { okAdd: "Agregar",
okAdd: "Añadir", okEdit: "Guardar",
okEdit: "Ahorrar", cancel: "Cancelar",
cancel: "Cancelar", },
}, success: "Respuesta rápida guardada correctamente.",
}, },
userModal: { queueModal: {
title: { title: {
add: "Agregar usuario", add: "Agregar cola",
edit: "Editar usuario", edit: "Editar cola",
}, },
form: { form: {
name: "Nombre", name: "Nombre",
email: "Correo Electrónico", color: "Color",
password: "Contraseña", greetingMessage: "Mensaje de saludo",
profile: "Perfil", },
}, buttons: {
buttons: { okAdd: "Añadir",
okAdd: "Agregar", okEdit: "Ahorrar",
okEdit: "Guardar", cancel: "Cancelar",
cancel: "Cancelar", },
}, },
success: "Usuario guardado satisfactoriamente.", userModal: {
}, title: {
chat: { add: "Agregar usuario",
noTicketMessage: "Selecciona un ticket para empezar a chatear.", edit: "Editar usuario",
}, },
ticketsManager: { form: {
buttons: { name: "Nombre",
newTicket: "Nuevo", email: "Correo Electrónico",
}, password: "Contraseña",
}, profile: "Perfil",
ticketsQueueSelect: { },
placeholder: "Linhas", buttons: {
}, okAdd: "Agregar",
tickets: { okEdit: "Guardar",
toasts: { cancel: "Cancelar",
deleted: "El ticket en el que estabas ha sido borrado.", },
}, success: "Usuario guardado satisfactoriamente.",
notification: { },
message: "Mensaje de", chat: {
}, noTicketMessage: "Selecciona un ticket para empezar a chatear.",
tabs: { },
open: { title: "Bandeja" }, ticketsManager: {
closed: { title: "Resueltos" }, buttons: {
search: { title: "Buscar" }, newTicket: "Nuevo",
}, },
search: { },
placeholder: "Buscar tickets y mensajes.", ticketsQueueSelect: {
}, placeholder: "Linhas",
buttons: { },
showAll: "Todos", tickets: {
}, toasts: {
}, deleted: "El ticket en el que estabas ha sido borrado.",
transferTicketModal: { },
title: "Transferir Ticket", notification: {
fieldLabel: "Escriba para buscar usuarios", message: "Mensaje de",
noOptions: "No se encontraron usuarios con ese nombre", },
buttons: { tabs: {
ok: "Transferir", open: { title: "Bandeja" },
cancel: "Cancelar", closed: { title: "Resueltos" },
}, search: { title: "Buscar" },
}, },
ticketsList: { search: {
pendingHeader: "Cola", placeholder: "Buscar tickets y mensajes.",
assignedHeader: "Trabajando en", },
noTicketsTitle: "¡Nada acá!", buttons: {
noTicketsMessage: showAll: "Todos",
"No se encontraron tickets con este estado o término de búsqueda", },
buttons: { },
accept: "Acceptar", transferTicketModal: {
}, title: "Transferir Ticket",
}, fieldLabel: "Escriba para buscar usuarios",
newTicketModal: { noOptions: "No se encontraron usuarios con ese nombre",
title: "Crear Ticket", buttons: {
fieldLabel: "Escribe para buscar un contacto", ok: "Transferir",
add: "Añadir", cancel: "Cancelar",
buttons: { },
ok: "Guardar", },
cancel: "Cancelar", ticketsList: {
}, pendingHeader: "Cola",
}, assignedHeader: "Trabajando en",
mainDrawer: { noTicketsTitle: "¡Nada acá!",
listItems: { noTicketsMessage:
dashboard: "Dashboard", "No se encontraron tickets con este estado o término de búsqueda",
connections: "Conexiones", buttons: {
tickets: "Tickets", accept: "Acceptar",
contacts: "Contactos", },
queues: "Linhas", },
administration: "Administración", newTicketModal: {
users: "Usuarios", title: "Crear Ticket",
settings: "Configuración", fieldLabel: "Escribe para buscar un contacto",
}, add: "Añadir",
appBar: { buttons: {
user: { ok: "Guardar",
profile: "Perfil", cancel: "Cancelar",
logout: "Cerrar Sesión", },
}, },
}, mainDrawer: {
}, listItems: {
notifications: { dashboard: "Dashboard",
noTickets: "Sin notificaciones.", connections: "Conexiones",
}, tickets: "Tickets",
queues: { contacts: "Contactos",
title: "Linhas", quickAnswers: "Respuestas rápidas",
table: { queues: "Linhas",
name: "Nombre", administration: "Administración",
color: "Color", users: "Usuarios",
greeting: "Mensaje de saludo", settings: "Configuración",
actions: "Comportamiento", },
}, appBar: {
buttons: { user: {
add: "Agregar cola", profile: "Perfil",
}, logout: "Cerrar Sesión",
confirmationModal: { },
deleteTitle: "Eliminar", },
deleteMessage: },
"¿Estás seguro? ¡Esta acción no se puede revertir! Los tickets en esa cola seguirán existiendo, pero ya no tendrán ninguna cola asignada.", notifications: {
}, noTickets: "Sin notificaciones.",
}, },
queueSelect: { queues: {
inputLabel: "Linhas", title: "Linhas",
}, table: {
users: { name: "Nombre",
title: "Usuarios", color: "Color",
table: { greeting: "Mensaje de saludo",
name: "Nombre", actions: "Comportamiento",
email: "Correo Electrónico", },
profile: "Perfil", buttons: {
actions: "Acciones", add: "Agregar cola",
}, },
buttons: { confirmationModal: {
add: "Agregar usuario", deleteTitle: "Eliminar",
}, deleteMessage:
toasts: { "¿Estás seguro? ¡Esta acción no se puede revertir! Los tickets en esa cola seguirán existiendo, pero ya no tendrán ninguna cola asignada.",
deleted: "Usuario borrado satisfactoriamente.", },
}, },
confirmationModal: { queueSelect: {
deleteTitle: "Borrar", inputLabel: "Linhas",
deleteMessage: },
"Toda la información del usuario se perderá. Los tickets abiertos de los usuarios se moverán a la cola.", quickAnswers: {
}, title: "Respuestas rápidas",
}, table: {
settings: { shortcut: "Atajo",
success: "Configuración guardada satisfactoriamente.", message: "Respuesta rápida",
title: "Configuración", actions: "Acciones",
settings: { },
userCreation: { buttons: {
name: "Creación de usuarios", add: "Agregar respuesta rápida",
options: { },
enabled: "Habilitado", toasts: {
disabled: "Deshabilitado", deleted: "Respuesta rápida eliminada correctamente",
}, },
}, searchPlaceholder: "Buscar ...",
}, confirmationModal: {
}, deleteTitle:
messagesList: { "¿Está seguro de que desea eliminar esta respuesta rápida?",
header: { deleteMessage: "Esta acción no se puede deshacer.",
assignedTo: "Asignado a:", },
buttons: { },
return: "Devolver", users: {
resolve: "Resolver", title: "Usuarios",
reopen: "Reabrir", table: {
accept: "Aceptar", name: "Nombre",
}, email: "Correo Electrónico",
}, profile: "Perfil",
}, actions: "Acciones",
messagesInput: { },
placeholderOpen: "Escribe un mensaje", buttons: {
placeholderClosed: add: "Agregar usuario",
"Vuelva a abrir o acepte este ticket para enviar un mensaje.", },
signMessage: "Firmar", toasts: {
}, deleted: "Usuario borrado satisfactoriamente.",
contactDrawer: { },
header: "Detalles del contacto", confirmationModal: {
buttons: { deleteTitle: "Borrar",
edit: "Editar contacto", deleteMessage:
}, "Toda la información del usuario se perderá. Los tickets abiertos de los usuarios se moverán a la cola.",
extraInfo: "Otra información", },
}, },
ticketOptionsMenu: { settings: {
delete: "Borrar", success: "Configuración guardada satisfactoriamente.",
transfer: "Transferir", title: "Configuración",
confirmationModal: { settings: {
title: "¿Borrar ticket #", userCreation: {
titleFrom: "del contacto ", name: "Creación de usuarios",
message: options: {
"¡Atención! Todos los mensajes Todos los mensajes relacionados con el ticket se perderán.", enabled: "Habilitado",
}, disabled: "Deshabilitado",
buttons: { },
delete: "Borrar", },
cancel: "Cancelar", },
}, },
}, messagesList: {
confirmationModal: { header: {
buttons: { assignedTo: "Asignado a:",
confirm: "Ok", buttons: {
cancel: "Cancelar", return: "Devolver",
}, resolve: "Resolver",
}, reopen: "Reabrir",
messageOptionsMenu: { accept: "Aceptar",
delete: "Borrar", },
reply: "Responder", },
confirmationModal: { },
title: "¿Borrar mensaje?", messagesInput: {
message: "Esta acción no puede ser revertida.", placeholderOpen: "Escribe un mensaje",
}, placeholderClosed:
}, "Vuelva a abrir o acepte este ticket para enviar un mensaje.",
backendErrors: { signMessage: "Firmar",
ERR_NO_OTHER_WHATSAPP: },
"Debe haber al menos una conexión de WhatsApp predeterminada.", contactDrawer: {
ERR_NO_DEF_WAPP_FOUND: header: "Detalles del contacto",
"No se encontró WhatsApp predeterminado. Verifique la página de conexiones.", buttons: {
ERR_WAPP_NOT_INITIALIZED: edit: "Editar contacto",
"Esta sesión de WhatsApp no está inicializada. Verifique la página de conexiones.", },
ERR_WAPP_CHECK_CONTACT: extraInfo: "Otra información",
"No se pudo verificar el contacto de WhatsApp. Verifique la página de conexiones.", },
ERR_WAPP_INVALID_CONTACT: "Este no es un número de whatsapp válido.", ticketOptionsMenu: {
ERR_WAPP_DOWNLOAD_MEDIA: delete: "Borrar",
"No se pudieron descargar los medios de WhatsApp. Verifique la página de conexiones.", transfer: "Transferir",
ERR_INVALID_CREDENTIALS: "Error de autenticación. Vuelva a intentarlo.", confirmationModal: {
ERR_SENDING_WAPP_MSG: title: "¿Borrar ticket #",
"Error al enviar el mensaje de WhatsApp. Verifique la página de conexiones.", titleFrom: "del contacto ",
ERR_DELETE_WAPP_MSG: "No se pudo borrar el mensaje de WhatsApp.", message:
ERR_OTHER_OPEN_TICKET: "Ya hay un ticket abierto para este contacto.", "¡Atención! Todos los mensajes Todos los mensajes relacionados con el ticket se perderán.",
ERR_SESSION_EXPIRED: "Sesión caducada. Inicie sesión.", },
ERR_USER_CREATION_DISABLED: buttons: {
"La creación de usuarios fue deshabilitada por el administrador.", delete: "Borrar",
ERR_NO_PERMISSION: "No tienes permiso para acceder a este recurso.", cancel: "Cancelar",
ERR_DUPLICATED_CONTACT: "Ya existe un contacto con este número.", },
ERR_NO_SETTING_FOUND: },
"No se encontró ninguna configuración con este ID.", confirmationModal: {
ERR_NO_CONTACT_FOUND: "No se encontró ningún contacto con este ID.", buttons: {
ERR_NO_TICKET_FOUND: "No se encontró ningún ticket con este ID.", confirm: "Ok",
ERR_NO_USER_FOUND: "No se encontró ningún usuario con este ID.", cancel: "Cancelar",
ERR_NO_WAPP_FOUND: "No se encontró WhatsApp con este ID.", },
ERR_CREATING_MESSAGE: "Error al crear el mensaje en la base de datos.", },
ERR_CREATING_TICKET: "Error al crear el ticket en la base de datos.", messageOptionsMenu: {
ERR_FETCH_WAPP_MSG: delete: "Borrar",
"Error al obtener el mensaje en WhtasApp, tal vez sea demasiado antiguo.", reply: "Responder",
ERR_QUEUE_COLOR_ALREADY_EXISTS: confirmationModal: {
"Este color ya está en uso, elija otro.", title: "¿Borrar mensaje?",
ERR_WAPP_GREETING_REQUIRED: message: "Esta acción no puede ser revertida.",
"El mensaje de saludo es obligatorio cuando hay más de una cola.", },
}, },
}, backendErrors: {
}, ERR_NO_OTHER_WHATSAPP:
"Debe haber al menos una conexión de WhatsApp predeterminada.",
ERR_NO_DEF_WAPP_FOUND:
"No se encontró WhatsApp predeterminado. Verifique la página de conexiones.",
ERR_WAPP_NOT_INITIALIZED:
"Esta sesión de WhatsApp no está inicializada. Verifique la página de conexiones.",
ERR_WAPP_CHECK_CONTACT:
"No se pudo verificar el contacto de WhatsApp. Verifique la página de conexiones.",
ERR_WAPP_INVALID_CONTACT: "Este no es un número de whatsapp válido.",
ERR_WAPP_DOWNLOAD_MEDIA:
"No se pudieron descargar los medios de WhatsApp. Verifique la página de conexiones.",
ERR_INVALID_CREDENTIALS: "Error de autenticación. Vuelva a intentarlo.",
ERR_SENDING_WAPP_MSG:
"Error al enviar el mensaje de WhatsApp. Verifique la página de conexiones.",
ERR_DELETE_WAPP_MSG: "No se pudo borrar el mensaje de WhatsApp.",
ERR_OTHER_OPEN_TICKET: "Ya hay un ticket abierto para este contacto.",
ERR_SESSION_EXPIRED: "Sesión caducada. Inicie sesión.",
ERR_USER_CREATION_DISABLED:
"La creación de usuarios fue deshabilitada por el administrador.",
ERR_NO_PERMISSION: "No tienes permiso para acceder a este recurso.",
ERR_DUPLICATED_CONTACT: "Ya existe un contacto con este número.",
ERR_NO_SETTING_FOUND:
"No se encontró ninguna configuración con este ID.",
ERR_NO_CONTACT_FOUND: "No se encontró ningún contacto con este ID.",
ERR_NO_TICKET_FOUND: "No se encontró ningún ticket con este ID.",
ERR_NO_USER_FOUND: "No se encontró ningún usuario con este ID.",
ERR_NO_WAPP_FOUND: "No se encontró WhatsApp con este ID.",
ERR_CREATING_MESSAGE: "Error al crear el mensaje en la base de datos.",
ERR_CREATING_TICKET: "Error al crear el ticket en la base de datos.",
ERR_FETCH_WAPP_MSG:
"Error al obtener el mensaje en WhtasApp, tal vez sea demasiado antiguo.",
ERR_QUEUE_COLOR_ALREADY_EXISTS:
"Este color ya está en uso, elija otro.",
ERR_WAPP_GREETING_REQUIRED:
"El mensaje de saludo es obligatorio cuando hay más de una cola.",
},
},
},
}; };
export { messages }; export { messages };

View File

@@ -1,414 +1,451 @@
const messages = { const messages = {
pt: { pt: {
translations: { translations: {
signup: { signup: {
title: "Cadastre-se", title: "Cadastre-se",
toasts: { toasts: {
success: "Usuário criado com sucesso! Faça seu login!!!.", success: "Usuário criado com sucesso! Faça seu login!!!.",
fail: "Erro ao criar usuário. Verifique os dados informados.", fail: "Erro ao criar usuário. Verifique os dados informados.",
}, },
form: { form: {
name: "Nome", name: "Nome",
email: "Email", email: "Email",
password: "Senha", password: "Senha",
}, },
buttons: { buttons: {
submit: "Cadastrar", submit: "Cadastrar",
login: "Já tem uma conta? Entre!", login: "Já tem uma conta? Entre!",
}, },
}, },
login: { login: {
title: "Login", title: "Login",
form: { form: {
email: "Email", email: "Email",
password: "Senha", password: "Senha",
}, },
buttons: { buttons: {
submit: "Entrar", submit: "Entrar",
register: "Não tem um conta? Cadastre-se!", register: "Não tem um conta? Cadastre-se!",
}, },
}, },
auth: { auth: {
toasts: { toasts: {
success: "Login efetuado com sucesso!", success: "Login efetuado com sucesso!",
}, },
}, },
dashboard: { dashboard: {
charts: { charts: {
perDay: { perDay: {
title: "Tickets hoje: ", title: "Tickets hoje: ",
}, },
}, },
}, },
connections: { connections: {
title: "Conexões", title: "Conexões",
toasts: { toasts: {
deleted: "Conexão com o WhatsApp excluída com sucesso!", deleted: "Conexão com o WhatsApp excluída com sucesso!",
}, },
confirmationModal: { confirmationModal: {
deleteTitle: "Deletar", deleteTitle: "Deletar",
deleteMessage: "Você tem certeza? Essa ação não pode ser revertida.", deleteMessage: "Você tem certeza? Essa ação não pode ser revertida.",
disconnectTitle: "Desconectar", disconnectTitle: "Desconectar",
disconnectMessage: disconnectMessage:
"Tem certeza? Você precisará ler o QR Code novamente.", "Tem certeza? Você precisará ler o QR Code novamente.",
}, },
buttons: { buttons: {
add: "Adicionar WhatsApp", add: "Adicionar WhatsApp",
disconnect: "desconectar", disconnect: "desconectar",
tryAgain: "Tentar novamente", tryAgain: "Tentar novamente",
qrcode: "QR CODE", qrcode: "QR CODE",
newQr: "Novo QR CODE", newQr: "Novo QR CODE",
connecting: "Conectando", connecting: "Conectando",
}, },
toolTips: { toolTips: {
disconnected: { disconnected: {
title: "Falha ao iniciar sessão do WhatsApp", title: "Falha ao iniciar sessão do WhatsApp",
content: content:
"Certifique-se de que seu celular esteja conectado à internet e tente novamente, ou solicite um novo QR Code", "Certifique-se de que seu celular esteja conectado à internet e tente novamente, ou solicite um novo QR Code",
}, },
qrcode: { qrcode: {
title: "Esperando leitura do QR Code", title: "Esperando leitura do QR Code",
content: content:
"Clique no botão 'QR CODE' e leia o QR Code com o seu celular para iniciar a sessão", "Clique no botão 'QR CODE' e leia o QR Code com o seu celular para iniciar a sessão",
}, },
connected: { connected: {
title: "Conexão estabelecida!", title: "Conexão estabelecida!",
}, },
timeout: { timeout: {
title: "A conexão com o celular foi perdida", title: "A conexão com o celular foi perdida",
content: content:
"Certifique-se de que seu celular esteja conectado à internet e o WhatsApp esteja aberto, ou clique no botão 'Desconectar' para obter um novo QR Code", "Certifique-se de que seu celular esteja conectado à internet e o WhatsApp esteja aberto, ou clique no botão 'Desconectar' para obter um novo QR Code",
}, },
}, },
table: { table: {
name: "Nome", name: "Nome",
status: "Status", status: "Status",
lastUpdate: "Última atualização", lastUpdate: "Última atualização",
default: "Padrão", default: "Padrão",
actions: "Ações", actions: "Ações",
session: "Sessão", session: "Sessão",
}, },
}, },
whatsappModal: { whatsappModal: {
title: { title: {
add: "Adicionar WhatsApp", add: "Adicionar WhatsApp",
edit: "Editar WhatsApp", edit: "Editar WhatsApp",
}, },
form: { form: {
name: "Nome", name: "Nome",
default: "Padrão", default: "Padrão",
}, },
buttons: { buttons: {
okAdd: "Adicionar", okAdd: "Adicionar",
okEdit: "Salvar", okEdit: "Salvar",
cancel: "Cancelar", cancel: "Cancelar",
}, },
success: "WhatsApp salvo com sucesso.", success: "WhatsApp salvo com sucesso.",
}, },
qrCode: { qrCode: {
message: "Leia o QrCode para iniciar a sessão", message: "Leia o QrCode para iniciar a sessão",
}, },
contacts: { contacts: {
title: "Contatos", title: "Contatos",
toasts: { toasts: {
deleted: "Contato excluído com sucesso!", deleted: "Contato excluído com sucesso!",
}, },
searchPlaceholder: "Pesquisar...", searchPlaceholder: "Pesquisar...",
confirmationModal: { confirmationModal: {
deleteTitle: "Deletar ", deleteTitle: "Deletar ",
importTitlte: "Importar contatos", importTitlte: "Importar contatos",
deleteMessage: deleteMessage:
"Tem certeza que deseja deletar este contato? Todos os tickets relacionados serão perdidos.", "Tem certeza que deseja deletar este contato? Todos os tickets relacionados serão perdidos.",
importMessage: "Deseja importas todos os contatos do telefone?", importMessage: "Deseja importas todos os contatos do telefone?",
}, },
buttons: { buttons: {
import: "Importar Contatos", import: "Importar Contatos",
add: "Adicionar Contato", add: "Adicionar Contato",
}, },
table: { table: {
name: "Nome", name: "Nome",
whatsapp: "WhatsApp", whatsapp: "WhatsApp",
email: "Email", email: "Email",
actions: "Ações", actions: "Ações",
}, },
}, },
contactModal: { contactModal: {
title: { title: {
add: "Adicionar contato", add: "Adicionar contato",
edit: "Editar contato", edit: "Editar contato",
}, },
form: { form: {
mainInfo: "Dados do contato", mainInfo: "Dados do contato",
extraInfo: "Informações adicionais", extraInfo: "Informações adicionais",
name: "Nome", name: "Nome",
number: "Número do Whatsapp", number: "Número do Whatsapp",
email: "Email", email: "Email",
extraName: "Nome do campo", extraName: "Nome do campo",
extraValue: "Valor", extraValue: "Valor",
}, },
buttons: { buttons: {
addExtraInfo: "Adicionar informação", addExtraInfo: "Adicionar informação",
okAdd: "Adicionar", okAdd: "Adicionar",
okEdit: "Salvar", okEdit: "Salvar",
cancel: "Cancelar", cancel: "Cancelar",
}, },
success: "Contato salvo com sucesso.", success: "Contato salvo com sucesso.",
}, },
queueModal: { quickAnswersModal: {
title: { title: {
add: "Adicionar fila", add: "Adicionar Resposta Rápida",
edit: "Editar fila", edit: "Editar Resposta Rápida",
}, },
form: { form: {
name: "Nome", shortcut: "Atalho",
color: "Cor", message: "Resposta Rápida",
greetingMessage: "Mensagem de saudação", },
}, buttons: {
buttons: { okAdd: "Adicionar",
okAdd: "Adicionar", okEdit: "Salvar",
okEdit: "Salvar", cancel: "Cancelar",
cancel: "Cancelar", },
}, success: "Resposta Rápida salva com sucesso.",
}, },
userModal: { queueModal: {
title: { title: {
add: "Adicionar usuário", add: "Adicionar fila",
edit: "Editar usuário", edit: "Editar fila",
}, },
form: { form: {
name: "Nome", name: "Nome",
email: "Email", color: "Cor",
password: "Senha", greetingMessage: "Mensagem de saudação",
profile: "Perfil", },
}, buttons: {
buttons: { okAdd: "Adicionar",
okAdd: "Adicionar", okEdit: "Salvar",
okEdit: "Salvar", cancel: "Cancelar",
cancel: "Cancelar", },
}, },
success: "Usuário salvo com sucesso.", userModal: {
}, title: {
chat: { add: "Adicionar usuário",
noTicketMessage: "Selecione um ticket para começar a conversar.", edit: "Editar usuário",
}, },
ticketsManager: { form: {
buttons: { name: "Nome",
newTicket: "Novo", email: "Email",
}, password: "Senha",
}, profile: "Perfil",
ticketsQueueSelect: { },
placeholder: "Filas", buttons: {
}, okAdd: "Adicionar",
tickets: { okEdit: "Salvar",
toasts: { cancel: "Cancelar",
deleted: "O ticket que você estava foi deletado.", },
}, success: "Usuário salvo com sucesso.",
notification: { },
message: "Mensagem de", chat: {
}, noTicketMessage: "Selecione um ticket para começar a conversar.",
tabs: { },
open: { title: "Inbox" }, ticketsManager: {
closed: { title: "Resolvidos" }, buttons: {
search: { title: "Busca" }, newTicket: "Novo",
}, },
search: { },
placeholder: "Buscar tickets e mensagens", ticketsQueueSelect: {
}, placeholder: "Filas",
buttons: { },
showAll: "Todos", tickets: {
}, toasts: {
}, deleted: "O ticket que você estava foi deletado.",
transferTicketModal: { },
title: "Transferir Ticket", notification: {
fieldLabel: "Digite para buscar usuários", message: "Mensagem de",
noOptions: "Nenhum usuário encontrado com esse nome", },
buttons: { tabs: {
ok: "Transferir", open: { title: "Inbox" },
cancel: "Cancelar", closed: { title: "Resolvidos" },
}, search: { title: "Busca" },
}, },
ticketsList: { search: {
pendingHeader: "Aguardando", placeholder: "Buscar tickets e mensagens",
assignedHeader: "Atendendo", },
noTicketsTitle: "Nada aqui!", buttons: {
noTicketsMessage: showAll: "Todos",
"Nenhum ticket encontrado com esse status ou termo pesquisado", },
buttons: { },
accept: "Aceitar", transferTicketModal: {
}, title: "Transferir Ticket",
}, fieldLabel: "Digite para buscar usuários",
newTicketModal: { noOptions: "Nenhum usuário encontrado com esse nome",
title: "Criar Ticket", buttons: {
fieldLabel: "Digite para pesquisar o contato", ok: "Transferir",
add: "Adicionar", cancel: "Cancelar",
buttons: { },
ok: "Salvar", },
cancel: "Cancelar", ticketsList: {
}, pendingHeader: "Aguardando",
}, assignedHeader: "Atendendo",
mainDrawer: { noTicketsTitle: "Nada aqui!",
listItems: { noTicketsMessage:
dashboard: "Dashboard", "Nenhum ticket encontrado com esse status ou termo pesquisado",
connections: "Conexões", buttons: {
tickets: "Tickets", accept: "Aceitar",
contacts: "Contatos", },
queues: "Filas", },
administration: "Administração", newTicketModal: {
users: "Usuários", title: "Criar Ticket",
settings: "Configurações", fieldLabel: "Digite para pesquisar o contato",
}, add: "Adicionar",
appBar: { buttons: {
user: { ok: "Salvar",
profile: "Perfil", cancel: "Cancelar",
logout: "Sair", },
}, },
}, mainDrawer: {
}, listItems: {
notifications: { dashboard: "Dashboard",
noTickets: "Nenhuma notificação.", connections: "Conexões",
}, tickets: "Tickets",
queues: { contacts: "Contatos",
title: "Filas", quickAnswers: "Respostas Rápidas",
table: { queues: "Filas",
name: "Nome", administration: "Administração",
color: "Cor", users: "Usuários",
greeting: "Mensagem de saudação", settings: "Configurações",
actions: "Ações", },
}, appBar: {
buttons: { user: {
add: "Adicionar fila", profile: "Perfil",
}, logout: "Sair",
confirmationModal: { },
deleteTitle: "Excluir", },
deleteMessage: },
"Você tem certeza? Essa ação não pode ser revertida! Os tickets dessa fila continuarão existindo, mas não terão mais nenhuma fila atribuída.", notifications: {
}, noTickets: "Nenhuma notificação.",
}, },
queueSelect: { queues: {
inputLabel: "Filas", title: "Filas",
}, table: {
users: { name: "Nome",
title: "Usuários", color: "Cor",
table: { greeting: "Mensagem de saudação",
name: "Nome", actions: "Ações",
email: "Email", },
profile: "Perfil", buttons: {
actions: "Ações", add: "Adicionar fila",
}, },
buttons: { confirmationModal: {
add: "Adicionar usuário", deleteTitle: "Excluir",
}, deleteMessage:
toasts: { "Você tem certeza? Essa ação não pode ser revertida! Os tickets dessa fila continuarão existindo, mas não terão mais nenhuma fila atribuída.",
deleted: "Usuário excluído com sucesso.", },
}, },
confirmationModal: { queueSelect: {
deleteTitle: "Excluir", inputLabel: "Filas",
deleteMessage: },
"Todos os dados do usuário serão perdidos. Os tickets abertos deste usuário serão movidos para a fila.", quickAnswers: {
}, title: "Respostas Rápidas",
}, table: {
settings: { shortcut: "Atalho",
success: "Configurações salvas com sucesso.", message: "Resposta Rápida",
title: "Configurações", actions: "Ações",
settings: { },
userCreation: { buttons: {
name: "Criação de usuário", add: "Adicionar Resposta Rápida",
options: { },
enabled: "Ativado", toasts: {
disabled: "Desativado", deleted: "Resposta Rápida excluída com sucesso.",
}, },
}, searchPlaceholder: "Pesquisar...",
}, confirmationModal: {
}, deleteTitle:
messagesList: { "Você tem certeza que quer excluir esta Resposta Rápida: ",
header: { deleteMessage: "Esta ação não pode ser revertida.",
assignedTo: "Atribuído à:", },
buttons: { },
return: "Retornar", users: {
resolve: "Resolver", title: "Usuários",
reopen: "Reabrir", table: {
accept: "Aceitar", name: "Nome",
}, email: "Email",
}, profile: "Perfil",
}, actions: "Ações",
messagesInput: { },
placeholderOpen: "Digite uma mensagem", buttons: {
placeholderClosed: add: "Adicionar usuário",
"Reabra ou aceite esse ticket para enviar uma mensagem.", },
signMessage: "Assinar", toasts: {
}, deleted: "Usuário excluído com sucesso.",
contactDrawer: { },
header: "Dados do contato", confirmationModal: {
buttons: { deleteTitle: "Excluir",
edit: "Editar contato", deleteMessage:
}, "Todos os dados do usuário serão perdidos. Os tickets abertos deste usuário serão movidos para a fila.",
extraInfo: "Outras informações", },
}, },
ticketOptionsMenu: { settings: {
delete: "Deletar", success: "Configurações salvas com sucesso.",
transfer: "Transferir", title: "Configurações",
confirmationModal: { settings: {
title: "Deletar o ticket do contato", userCreation: {
message: name: "Criação de usuário",
"Atenção! Todas as mensagens relacionadas ao ticket serão perdidas.", options: {
}, enabled: "Ativado",
buttons: { disabled: "Desativado",
delete: "Excluir", },
cancel: "Cancelar", },
}, },
}, },
confirmationModal: { messagesList: {
buttons: { header: {
confirm: "Ok", assignedTo: "Atribuído à:",
cancel: "Cancelar", buttons: {
}, return: "Retornar",
}, resolve: "Resolver",
messageOptionsMenu: { reopen: "Reabrir",
delete: "Deletar", accept: "Aceitar",
reply: "Responder", },
confirmationModal: { },
title: "Apagar mensagem?", },
message: "Esta ação não pode ser revertida.", messagesInput: {
}, placeholderOpen: "Digite uma mensagem",
}, placeholderClosed:
backendErrors: { "Reabra ou aceite esse ticket para enviar uma mensagem.",
ERR_NO_OTHER_WHATSAPP: "Deve haver pelo menos um WhatsApp padrão.", signMessage: "Assinar",
ERR_NO_DEF_WAPP_FOUND: },
"Nenhum WhatsApp padrão encontrado. Verifique a página de conexões.", contactDrawer: {
ERR_WAPP_NOT_INITIALIZED: header: "Dados do contato",
"Esta sessão do WhatsApp não foi inicializada. Verifique a página de conexões.", buttons: {
ERR_WAPP_CHECK_CONTACT: edit: "Editar contato",
"Não foi possível verificar o contato do WhatsApp. Verifique a página de conexões", },
ERR_WAPP_INVALID_CONTACT: "Este não é um número de Whatsapp válido.", extraInfo: "Outras informações",
ERR_WAPP_DOWNLOAD_MEDIA: },
"Não foi possível baixar mídia do WhatsApp. Verifique a página de conexões.", ticketOptionsMenu: {
ERR_INVALID_CREDENTIALS: delete: "Deletar",
"Erro de autenticação. Por favor, tente novamente.", transfer: "Transferir",
ERR_SENDING_WAPP_MSG: confirmationModal: {
"Erro ao enviar mensagem do WhatsApp. Verifique a página de conexões.", title: "Deletar o ticket do contato",
ERR_DELETE_WAPP_MSG: "Não foi possível excluir a mensagem do WhatsApp.", message:
ERR_OTHER_OPEN_TICKET: "Já existe um tíquete aberto para este contato.", "Atenção! Todas as mensagens relacionadas ao ticket serão perdidas.",
ERR_SESSION_EXPIRED: "Sessão expirada. Por favor entre.", },
ERR_USER_CREATION_DISABLED: buttons: {
"A criação do usuário foi desabilitada pelo administrador.", delete: "Excluir",
ERR_NO_PERMISSION: "Você não tem permissão para acessar este recurso.", cancel: "Cancelar",
ERR_DUPLICATED_CONTACT: "Já existe um contato com este número.", },
ERR_NO_SETTING_FOUND: "Nenhuma configuração encontrada com este ID.", },
ERR_NO_CONTACT_FOUND: "Nenhum contato encontrado com este ID.", confirmationModal: {
ERR_NO_TICKET_FOUND: "Nenhum tíquete encontrado com este ID.", buttons: {
ERR_NO_USER_FOUND: "Nenhum usuário encontrado com este ID.", confirm: "Ok",
ERR_NO_WAPP_FOUND: "Nenhum WhatsApp encontrado com este ID.", cancel: "Cancelar",
ERR_CREATING_MESSAGE: "Erro ao criar mensagem no banco de dados.", },
ERR_CREATING_TICKET: "Erro ao criar tíquete no banco de dados.", },
ERR_FETCH_WAPP_MSG: messageOptionsMenu: {
"Erro ao buscar a mensagem no WhtasApp, talvez ela seja muito antiga.", delete: "Deletar",
ERR_QUEUE_COLOR_ALREADY_EXISTS: reply: "Responder",
"Esta cor já está em uso, escolha outra.", confirmationModal: {
ERR_WAPP_GREETING_REQUIRED: title: "Apagar mensagem?",
"A mensagem de saudação é obrigatório quando há mais de uma fila.", message: "Esta ação não pode ser revertida.",
}, },
}, },
}, backendErrors: {
ERR_NO_OTHER_WHATSAPP: "Deve haver pelo menos um WhatsApp padrão.",
ERR_NO_DEF_WAPP_FOUND:
"Nenhum WhatsApp padrão encontrado. Verifique a página de conexões.",
ERR_WAPP_NOT_INITIALIZED:
"Esta sessão do WhatsApp não foi inicializada. Verifique a página de conexões.",
ERR_WAPP_CHECK_CONTACT:
"Não foi possível verificar o contato do WhatsApp. Verifique a página de conexões",
ERR_WAPP_INVALID_CONTACT: "Este não é um número de Whatsapp válido.",
ERR_WAPP_DOWNLOAD_MEDIA:
"Não foi possível baixar mídia do WhatsApp. Verifique a página de conexões.",
ERR_INVALID_CREDENTIALS:
"Erro de autenticação. Por favor, tente novamente.",
ERR_SENDING_WAPP_MSG:
"Erro ao enviar mensagem do WhatsApp. Verifique a página de conexões.",
ERR_DELETE_WAPP_MSG: "Não foi possível excluir a mensagem do WhatsApp.",
ERR_OTHER_OPEN_TICKET: "Já existe um tíquete aberto para este contato.",
ERR_SESSION_EXPIRED: "Sessão expirada. Por favor entre.",
ERR_USER_CREATION_DISABLED:
"A criação do usuário foi desabilitada pelo administrador.",
ERR_NO_PERMISSION: "Você não tem permissão para acessar este recurso.",
ERR_DUPLICATED_CONTACT: "Já existe um contato com este número.",
ERR_NO_SETTING_FOUND: "Nenhuma configuração encontrada com este ID.",
ERR_NO_CONTACT_FOUND: "Nenhum contato encontrado com este ID.",
ERR_NO_TICKET_FOUND: "Nenhum tíquete encontrado com este ID.",
ERR_NO_USER_FOUND: "Nenhum usuário encontrado com este ID.",
ERR_NO_WAPP_FOUND: "Nenhum WhatsApp encontrado com este ID.",
ERR_CREATING_MESSAGE: "Erro ao criar mensagem no banco de dados.",
ERR_CREATING_TICKET: "Erro ao criar tíquete no banco de dados.",
ERR_FETCH_WAPP_MSG:
"Erro ao buscar a mensagem no WhtasApp, talvez ela seja muito antiga.",
ERR_QUEUE_COLOR_ALREADY_EXISTS:
"Esta cor já está em uso, escolha outra.",
ERR_WAPP_GREETING_REQUIRED:
"A mensagem de saudação é obrigatório quando há mais de uma fila.",
},
},
},
}; };
export { messages }; export { messages };