hiesoftrd před 1 rokem
rodič
revize
6447a45524
100 změnil soubory, kde provedl 16273 přidání a 0 odebrání
  1. 338 0
      frontend/src/components/AnnouncementModal/index.js
  2. 329 0
      frontend/src/components/AnnouncementsPopover/index.js
  3. 23 0
      frontend/src/components/BackdropLoading/index.js
  4. 35 0
      frontend/src/components/ButtonWithSpinner/index.js
  5. 634 0
      frontend/src/components/CampaignModal/index.js
  6. 39 0
      frontend/src/components/Can/index.js
  7. 175 0
      frontend/src/components/CheckoutPage/CheckoutPage.js
  8. 76 0
      frontend/src/components/CheckoutPage/CheckoutSuccess/CheckoutSuccess.js
  9. 2 0
      frontend/src/components/CheckoutPage/CheckoutSuccess/index.js
  10. 117 0
      frontend/src/components/CheckoutPage/CheckoutSuccess/style.js
  11. 75 0
      frontend/src/components/CheckoutPage/FormModel/checkoutFormModel.js
  12. 32 0
      frontend/src/components/CheckoutPage/FormModel/formInitialValues.js
  13. 29 0
      frontend/src/components/CheckoutPage/FormModel/validationSchema.js
  14. 134 0
      frontend/src/components/CheckoutPage/Forms/AddressForm.js
  15. 238 0
      frontend/src/components/CheckoutPage/Forms/PaymentForm.js
  16. 61 0
      frontend/src/components/CheckoutPage/ReviewOrder/PaymentDetails.js
  17. 33 0
      frontend/src/components/CheckoutPage/ReviewOrder/ProductDetails.js
  18. 20 0
      frontend/src/components/CheckoutPage/ReviewOrder/ReviewOrder.js
  19. 26 0
      frontend/src/components/CheckoutPage/ReviewOrder/ShippingDetails.js
  20. 2 0
      frontend/src/components/CheckoutPage/ReviewOrder/index.js
  21. 12 0
      frontend/src/components/CheckoutPage/ReviewOrder/styles.js
  22. 2 0
      frontend/src/components/CheckoutPage/index.js
  23. 23 0
      frontend/src/components/CheckoutPage/styles.js
  24. 85 0
      frontend/src/components/ColorPicker/index.js
  25. 634 0
      frontend/src/components/CompaniesManager/index.js
  26. 45 0
      frontend/src/components/ConfirmationModal/index.js
  27. 199 0
      frontend/src/components/ContactDrawer/index.js
  28. 50 0
      frontend/src/components/ContactDrawerSkeleton/index.js
  29. 187 0
      frontend/src/components/ContactForm/index.js
  30. 181 0
      frontend/src/components/ContactListDialog/index.js
  31. 242 0
      frontend/src/components/ContactListItemModal/index.js
  32. 103 0
      frontend/src/components/ContactListTable/index.js
  33. 300 0
      frontend/src/components/ContactModal/index.js
  34. 204 0
      frontend/src/components/ContactNotes/index.js
  35. 206 0
      frontend/src/components/ContactNotesDialog/index.js
  36. 64 0
      frontend/src/components/ContactNotesDialogListItem/index.js
  37. 26 0
      frontend/src/components/ContactTag/index.js
  38. 50 0
      frontend/src/components/CurrencyInput/index.js
  39. 70 0
      frontend/src/components/DarkMode/index.js
  40. 53 0
      frontend/src/components/Dashboard/CardCounter.js
  41. 89 0
      frontend/src/components/Dashboard/TableAttendantsStatus.js
  42. 34 0
      frontend/src/components/Dialog/index.js
  43. 350 0
      frontend/src/components/FileModal/index.js
  44. 38 0
      frontend/src/components/FormFields/CheckboxField.js
  45. 54 0
      frontend/src/components/FormFields/DatePickerField.js
  46. 26 0
      frontend/src/components/FormFields/InputField.js
  47. 48 0
      frontend/src/components/FormFields/SelectField.js
  48. 5 0
      frontend/src/components/FormFields/index.js
  49. 291 0
      frontend/src/components/HelpsManager/index.js
  50. 217 0
      frontend/src/components/ImportContactsModal/index.js
  51. 45 0
      frontend/src/components/LanguageControl/index.js
  52. 53 0
      frontend/src/components/LocationPreview/index.js
  53. 31 0
      frontend/src/components/MainContainer/index.js
  54. 19 0
      frontend/src/components/MainHeader/index.js
  55. 21 0
      frontend/src/components/MainHeaderButtonsWrapper/index.js
  56. 186 0
      frontend/src/components/MarkdownWrapper/index.js
  57. 48 0
      frontend/src/components/MessageInput/RecordingTimer.js
  58. 513 0
      frontend/src/components/MessageInput/index.js
  59. 48 0
      frontend/src/components/MessageInputCustom/RecordingTimer.js
  60. 773 0
      frontend/src/components/MessageInputCustom/index.js
  61. 71 0
      frontend/src/components/MessageOptionsMenu/index.js
  62. 66 0
      frontend/src/components/MessageVariablesPicker/index.js
  63. 837 0
      frontend/src/components/MessagesList/index.js
  64. 50 0
      frontend/src/components/ModalImageCors/index.js
  65. 263 0
      frontend/src/components/ModalUsers/index.js
  66. 447 0
      frontend/src/components/NewTicketModal/index.js
  67. 270 0
      frontend/src/components/NotificationsPopOver/index.js
  68. 275 0
      frontend/src/components/NotificationsPopOver/index_Antigo.js
  69. 110 0
      frontend/src/components/NotificationsVolume/index.js
  70. 9 0
      frontend/src/components/OnlyForSuperUser/index.js
  71. 30 0
      frontend/src/components/OutlinedDiv/index.js
  72. 561 0
      frontend/src/components/PlansManager/index.js
  73. 312 0
      frontend/src/components/PromptModal/index.js
  74. 84 0
      frontend/src/components/QrcodeModal/index.js
  75. 505 0
      frontend/src/components/QueueIntegrationModal/index.js
  76. 510 0
      frontend/src/components/QueueModal/index.js
  77. 345 0
      frontend/src/components/QueueOptions/index.js
  78. 90 0
      frontend/src/components/QueueSelect/index copy.js
  79. 112 0
      frontend/src/components/QueueSelect/index.js
  80. 90 0
      frontend/src/components/QueueSelect/index_erro.js
  81. 92 0
      frontend/src/components/QueueSelectCustom/index.js
  82. 68 0
      frontend/src/components/QueueSelectSingle/index.js
  83. 332 0
      frontend/src/components/QuickMessageDialog/index.js
  84. 93 0
      frontend/src/components/QuickMessagesTable/index.js
  85. 382 0
      frontend/src/components/ScheduleModal/index.js
  86. 141 0
      frontend/src/components/SchedulesForm/index.js
  87. 727 0
      frontend/src/components/Settings/Options.js
  88. 70 0
      frontend/src/components/SubscriptionModal/index.js
  89. 9 0
      frontend/src/components/SubscriptionStepper/index.js
  90. 18 0
      frontend/src/components/TabPanel/index.js
  91. 52 0
      frontend/src/components/TableRowSkeleton/index.js
  92. 269 0
      frontend/src/components/TagModal/index.js
  93. 239 0
      frontend/src/components/TagModal/index.js_Backup
  94. 120 0
      frontend/src/components/TagsContainer/index.js
  95. 67 0
      frontend/src/components/TagsFilter/index.js
  96. 198 0
      frontend/src/components/Ticket/index.js
  97. 121 0
      frontend/src/components/TicketActionButtons/index.js
  98. 149 0
      frontend/src/components/TicketActionButtonsCustom/index.js
  99. 10 0
      frontend/src/components/TicketAdvancedLayout/index.js
  100. 36 0
      frontend/src/components/TicketHeader/index.js

+ 338 - 0
frontend/src/components/AnnouncementModal/index.js

@@ -0,0 +1,338 @@
+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 } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import CircularProgress from "@material-ui/core/CircularProgress";
+import AttachFileIcon from "@material-ui/icons/AttachFile";
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import IconButton from "@material-ui/core/IconButton";
+
+import { i18n } from "../../translate/i18n";
+import { head } from "lodash";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+import {
+  FormControl,
+  Grid,
+  InputLabel,
+  MenuItem,
+  Select,
+} from "@material-ui/core";
+import ConfirmationModal from "../ConfirmationModal";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    display: "flex",
+    flexWrap: "wrap",
+  },
+  multFieldLine: {
+    display: "flex",
+    "& > *:not(:last-child)": {
+      marginRight: theme.spacing(1),
+    },
+  },
+
+  btnWrapper: {
+    position: "relative",
+  },
+
+  buttonProgress: {
+    color: green[500],
+    position: "absolute",
+    top: "50%",
+    left: "50%",
+    marginTop: -12,
+    marginLeft: -12,
+  },
+  formControl: {
+    margin: theme.spacing(1),
+    minWidth: 120,
+  },
+  colorAdorment: {
+    width: 20,
+    height: 20,
+  },
+}));
+
+const AnnouncementSchema = Yup.object().shape({
+  title: Yup.string().required(i18n.t("announcements.dialog.form.required")),
+  text: Yup.string().required(i18n.t("announcements.dialog.form.required")),
+});
+
+const AnnouncementModal = ({ open, onClose, announcementId, reload }) => {
+  const classes = useStyles();
+
+  const initialState = {
+    title: "",
+    text: "",
+    priority: 3,
+    status: true,
+  };
+
+  const [confirmationOpen, setConfirmationOpen] = useState(false);
+  const [announcement, setAnnouncement] = useState(initialState);
+  const [attachment, setAttachment] = useState(null);
+  const attachmentFile = useRef(null);
+
+  useEffect(() => {
+    try {
+      (async () => {
+        if (!announcementId) return;
+
+        const { data } = await api.get(`/announcements/${announcementId}`);
+        setAnnouncement((prevState) => {
+          return { ...prevState, ...data };
+        });
+      })();
+    } catch (err) {
+      toastError(err);
+    }
+  }, [announcementId, open]);
+
+  const handleClose = () => {
+    setAnnouncement(initialState);
+    setAttachment(null);
+    onClose();
+  };
+
+  const handleAttachmentFile = (e) => {
+    const file = head(e.target.files);
+    if (file) {
+      setAttachment(file);
+    }
+  };
+
+  const handleSaveAnnouncement = async (values) => {
+    const announcementData = { ...values };
+    try {
+      if (announcementId) {
+        await api.put(`/announcements/${announcementId}`, announcementData);
+        if (attachment != null) {
+          const formData = new FormData();
+          formData.append("file", attachment);
+          await api.post(
+            `/announcements/${announcementId}/media-upload`,
+            formData
+          );
+        }
+      } else {
+        const { data } = await api.post("/announcements", announcementData);
+        if (attachment != null) {
+          const formData = new FormData();
+          formData.append("file", attachment);
+          await api.post(`/announcements/${data.id}/media-upload`, formData);
+        }
+      }
+      toast.success(i18n.t("announcements.toasts.success"));
+      if (typeof reload == "function") {
+        reload();
+      }
+    } catch (err) {
+      toastError(err);
+    }
+    handleClose();
+  };
+
+  const deleteMedia = async () => {
+    if (attachment) {
+      setAttachment(null);
+      attachmentFile.current.value = null;
+    }
+
+    if (announcement.mediaPath) {
+      await api.delete(`/announcements/${announcement.id}/media-upload`);
+      setAnnouncement((prev) => ({
+        ...prev,
+        mediaPath: null,
+      }));
+      toast.success(i18n.t("announcements.toasts.deleted"));
+      if (typeof reload == "function") {
+        reload();
+      }
+    }
+  };
+
+  return (
+    <div className={classes.root}>
+      <ConfirmationModal
+        title={i18n.t("announcements.confirmationModal.deleteTitle")}
+        open={confirmationOpen}
+        onClose={() => setConfirmationOpen(false)}
+        onConfirm={deleteMedia}
+      >
+        {i18n.t("announcements.confirmationModal.deleteMessage")}
+      </ConfirmationModal>
+      <Dialog
+        open={open}
+        onClose={handleClose}
+        maxWidth="xs"
+        fullWidth
+        scroll="paper"
+      >
+        <DialogTitle id="form-dialog-title">
+          {announcementId
+            ? `${i18n.t("announcements.dialog.edit")}`
+            : `${i18n.t("announcements.dialog.add")}`}
+        </DialogTitle>
+        <div style={{ display: "none" }}>
+          <input
+            type="file"
+            accept=".png,.jpg,.jpeg"
+            ref={attachmentFile}
+            onChange={(e) => handleAttachmentFile(e)}
+          />
+        </div>
+        <Formik
+          initialValues={announcement}
+          enableReinitialize={true}
+          validationSchema={AnnouncementSchema}
+          onSubmit={(values, actions) => {
+            setTimeout(() => {
+              handleSaveAnnouncement(values);
+              actions.setSubmitting(false);
+            }, 400);
+          }}
+        >
+          {({ touched, errors, isSubmitting, values }) => (
+            <Form>
+              <DialogContent dividers>
+                <Grid spacing={2} container>
+                  <Grid xs={12} item>
+                    <Field
+                      as={TextField}
+                      label={i18n.t("announcements.dialog.form.title")}
+                      name="title"
+                      error={touched.title && Boolean(errors.title)}
+                      helperText={touched.title && errors.title}
+                      variant="outlined"
+                      margin="dense"
+                      fullWidth
+                    />
+                  </Grid>
+                  <Grid xs={12} item>
+                    <Field
+                      as={TextField}
+                      label={i18n.t("announcements.dialog.form.text")}
+                      name="text"
+                      error={touched.text && Boolean(errors.text)}
+                      helperText={touched.text && errors.text}
+                      variant="outlined"
+                      margin="dense"
+                      multiline={true}
+                      rows={7}
+                      fullWidth
+                    />
+                  </Grid>
+                  <Grid xs={12} item>
+                    <FormControl variant="outlined" margin="dense" fullWidth>
+                      <InputLabel id="status-selection-label">
+                        {i18n.t("announcements.dialog.form.status")}
+                      </InputLabel>
+                      <Field
+                        as={Select}
+                        label={i18n.t("announcements.dialog.form.status")}
+                        placeholder={i18n.t("announcements.dialog.form.status")}
+                        labelId="status-selection-label"
+                        id="status"
+                        name="status"
+                        error={touched.status && Boolean(errors.status)}
+                      >
+                        <MenuItem value={true}>Ativo</MenuItem>
+                        <MenuItem value={false}>Inativo</MenuItem>
+                      </Field>
+                    </FormControl>
+                  </Grid>
+                  <Grid xs={12} item>
+                    <FormControl variant="outlined" margin="dense" fullWidth>
+                      <InputLabel id="priority-selection-label">
+                        {i18n.t("announcements.dialog.form.priority")}
+                      </InputLabel>
+                      <Field
+                        as={Select}
+                        label={i18n.t("announcements.dialog.form.priority")}
+                        placeholder={i18n.t(
+                          "announcements.dialog.form.priority"
+                        )}
+                        labelId="priority-selection-label"
+                        id="priority"
+                        name="priority"
+                        error={touched.priority && Boolean(errors.priority)}
+                      >
+                        <MenuItem value={1}>Alta</MenuItem>
+                        <MenuItem value={2}>Média</MenuItem>
+                        <MenuItem value={3}>Baixa</MenuItem>
+                      </Field>
+                    </FormControl>
+                  </Grid>
+                  {(announcement.mediaPath || attachment) && (
+                    <Grid xs={12} item>
+                      <Button startIcon={<AttachFileIcon />}>
+                        {attachment ? attachment.name : announcement.mediaName}
+                      </Button>
+                      <IconButton
+                        onClick={() => setConfirmationOpen(true)}
+                        color="secondary"
+                      >
+                        <DeleteOutlineIcon />
+                      </IconButton>
+                    </Grid>
+                  )}
+                </Grid>
+              </DialogContent>
+              <DialogActions>
+                {!attachment && !announcement.mediaPath && (
+                  <Button
+                    color="primary"
+                    onClick={() => attachmentFile.current.click()}
+                    disabled={isSubmitting}
+                    variant="outlined"
+                  >
+                    {i18n.t("announcements.dialog.buttons.attach")}
+                  </Button>
+                )}
+                <Button
+                  onClick={handleClose}
+                  color="secondary"
+                  disabled={isSubmitting}
+                  variant="outlined"
+                >
+                  {i18n.t("announcements.dialog.buttons.cancel")}
+                </Button>
+                <Button
+                  type="submit"
+                  color="primary"
+                  disabled={isSubmitting}
+                  variant="contained"
+                  className={classes.btnWrapper}
+                >
+                  {announcementId
+                    ? `${i18n.t("announcements.dialog.buttons.add")}`
+                    : `${i18n.t("announcements.dialog.buttons.edit")}`}
+                  {isSubmitting && (
+                    <CircularProgress
+                      size={24}
+                      className={classes.buttonProgress}
+                    />
+                  )}
+                </Button>
+              </DialogActions>
+            </Form>
+          )}
+        </Formik>
+      </Dialog>
+    </div>
+  );
+};
+
+export default AnnouncementModal;

+ 329 - 0
frontend/src/components/AnnouncementsPopover/index.js

@@ -0,0 +1,329 @@
+import React, { useEffect, useReducer, useState, useContext } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+import toastError from "../../errors/toastError";
+import Popover from "@material-ui/core/Popover";
+import AnnouncementIcon from "@material-ui/icons/Announcement";
+import Notifications from "@material-ui/icons/Notifications"
+
+import {
+  Avatar,
+  Badge,
+  IconButton,
+  List,
+  ListItem,
+  ListItemAvatar,
+  ListItemText,
+  Dialog,
+  Paper,
+  Typography,
+  DialogTitle,
+  DialogContent,
+  DialogActions,
+  Button,
+  DialogContentText,
+} from "@material-ui/core";
+import api from "../../services/api";
+import { isArray } from "lodash";
+import moment from "moment";
+import { SocketContext } from "../../context/Socket/SocketContext";
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    maxHeight: 3000,
+    maxWidth: 5000,
+    padding: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+}));
+
+function AnnouncementDialog({ announcement, open, handleClose }) {
+  const getMediaPath = (filename) => {
+    return `${process.env.REACT_APP_BACKEND_URL}/public/${filename}`;
+  };
+  return (
+    <Dialog
+      open={open}
+      onClose={() => handleClose()}
+      aria-labelledby="alert-dialog-title"
+      aria-describedby="alert-dialog-description"
+    >
+      <DialogTitle id="alert-dialog-title">{announcement.title}</DialogTitle>
+      <DialogContent>
+        {announcement.mediaPath && (
+          <div
+            style={{
+              border: "1px solid #f1f1f1",
+              margin: "0 auto 20px",
+              textAlign: "center",
+              width: "400px",
+              height: 300,
+              backgroundImage: `url(${getMediaPath(announcement.mediaPath)})`,
+              backgroundRepeat: "no-repeat",
+              backgroundSize: "contain",
+              backgroundPosition: "center",
+            }}
+          ></div>
+        )}
+        <DialogContentText id="alert-dialog-description">
+          {announcement.text}
+        </DialogContentText>
+      </DialogContent>
+      <DialogActions>
+        <Button onClick={() => handleClose()} color="primary" autoFocus>
+          Fechar
+        </Button>
+      </DialogActions>
+    </Dialog>
+  );
+}
+
+const reducer = (state, action) => {
+  if (action.type === "LOAD_ANNOUNCEMENTS") {
+    const announcements = action.payload;
+    const newAnnouncements = [];
+
+    if (isArray(announcements)) {
+      announcements.forEach((announcement) => {
+        const announcementIndex = state.findIndex(
+          (u) => u.id === announcement.id
+        );
+        if (announcementIndex !== -1) {
+          state[announcementIndex] = announcement;
+        } else {
+          newAnnouncements.push(announcement);
+        }
+      });
+    }
+
+    return [...state, ...newAnnouncements];
+  }
+
+  if (action.type === "UPDATE_ANNOUNCEMENTS") {
+    const announcement = action.payload;
+    const announcementIndex = state.findIndex((u) => u.id === announcement.id);
+
+    if (announcementIndex !== -1) {
+      state[announcementIndex] = announcement;
+      return [...state];
+    } else {
+      return [announcement, ...state];
+    }
+  }
+
+  if (action.type === "DELETE_ANNOUNCEMENT") {
+    const announcementId = action.payload;
+
+    const announcementIndex = state.findIndex((u) => u.id === announcementId);
+    if (announcementIndex !== -1) {
+      state.splice(announcementIndex, 1);
+    }
+    return [...state];
+  }
+
+  if (action.type === "RESET") {
+    return [];
+  }
+};
+
+export default function AnnouncementsPopover() {
+  const classes = useStyles();
+
+  const [loading, setLoading] = useState(false);
+  const [anchorEl, setAnchorEl] = useState(null);
+  const [pageNumber, setPageNumber] = useState(1);
+  const [hasMore, setHasMore] = useState(false);
+  const [searchParam] = useState("");
+  const [announcements, dispatch] = useReducer(reducer, []);
+  const [invisible, setInvisible] = useState(false);
+  const [announcement, setAnnouncement] = useState({});
+  const [showAnnouncementDialog, setShowAnnouncementDialog] = useState(false);
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+  }, [searchParam]);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      fetchAnnouncements();
+    }, 500);
+    return () => clearTimeout(delayDebounceFn);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [searchParam, pageNumber]);
+
+  useEffect(() => {
+    const companyId = localStorage.getItem("companyId");
+    const socket = socketManager.getSocket(companyId);
+    
+    if (!socket) {
+      return () => {}; 
+    }
+
+    socket.on(`company-announcement`, (data) => {
+      if (data.action === "update" || data.action === "create") {
+        dispatch({ type: "UPDATE_ANNOUNCEMENTS", payload: data.record });
+        setInvisible(false);
+      }
+      if (data.action === "delete") {
+        dispatch({ type: "DELETE_ANNOUNCEMENT", payload: +data.id });
+      }
+    });
+    return () => {
+      socket.disconnect();
+    };
+  }, [socketManager]);
+
+  const fetchAnnouncements = async () => {
+    try {
+      const { data } = await api.get("/announcements/", {
+        params: { searchParam, pageNumber },
+      });
+      dispatch({ type: "LOAD_ANNOUNCEMENTS", payload: data.records });
+      setHasMore(data.hasMore);
+      setLoading(false);
+    } catch (err) {
+      toastError(err);
+    }
+  };
+
+  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();
+    }
+  };
+
+  const handleClick = (event) => {
+    setAnchorEl(event.currentTarget);
+    setInvisible(true);
+  };
+
+  const handleClose = () => {
+    setAnchorEl(null);
+  };
+
+  const borderPriority = (priority) => {
+    if (priority === 1) {
+      return "4px solid #b81111";
+    }
+    if (priority === 2) {
+      return "4px solid orange";
+    }
+    if (priority === 3) {
+      return "4px solid grey";
+    }
+  };
+
+  const getMediaPath = (filename) => {
+    return `${process.env.REACT_APP_BACKEND_URL}/public/${filename}`;
+  };
+
+  const handleShowAnnouncementDialog = (record) => {
+    setAnnouncement(record);
+    setShowAnnouncementDialog(true);
+    setAnchorEl(null);
+  };
+
+  const open = Boolean(anchorEl);
+  const id = open ? "simple-popover" : undefined;
+
+  return (
+    <div>
+      <AnnouncementDialog
+        announcement={announcement}
+        open={showAnnouncementDialog}
+        handleClose={() => setShowAnnouncementDialog(false)}
+      />
+      <IconButton
+        variant="contained"
+        aria-describedby={id}
+        onClick={handleClick}
+        style={{ color: "white" }}
+      >
+        <Badge
+          color="secondary"
+          variant="dot"
+          invisible={invisible || announcements.length < 1}
+        >
+          <Notifications />
+        </Badge>
+      </IconButton>
+      <Popover
+        id={id}
+        open={open}
+        anchorEl={anchorEl}
+        onClose={handleClose}
+        anchorOrigin={{
+          vertical: "bottom",
+          horizontal: "center",
+        }}
+        transformOrigin={{
+          vertical: "top",
+          horizontal: "center",
+        }}
+      >
+        <Paper
+          variant="outlined"
+          onScroll={handleScroll}
+          className={classes.mainPaper}
+        >
+          <List
+            component="nav"
+            aria-label="main mailbox folders"
+            style={{ minWidth: 300 }}
+          >
+            {isArray(announcements) &&
+              announcements.map((item, key) => (
+                <ListItem
+                  key={key}
+                  style={{
+                    //background: key % 2 === 0 ? "#ededed" : "white",
+                    border: "1px solid #eee",
+                    borderLeft: borderPriority(item.priority),
+                    cursor: "pointer",
+                  }}
+                  onClick={() => handleShowAnnouncementDialog(item)}
+                >
+                  {item.mediaPath && (
+                    <ListItemAvatar>
+                      <Avatar
+                        alt={item.mediaName}
+                        src={getMediaPath(item.mediaPath)}
+                      />
+                    </ListItemAvatar>
+                  )}
+                  <ListItemText
+                    primary={item.title}
+                    secondary={
+                      <>
+                        <Typography component="span" style={{ fontSize: 12 }}>
+                          {moment(item.createdAt).format("DD/MM/YYYY")}
+                        </Typography>
+                        <span style={{ marginTop: 5, display: "block" }}></span>
+                        <Typography component="span" variant="body2">
+                          {item.text}
+                        </Typography>
+                      </>
+                    }
+                  />
+                </ListItem>
+              ))}
+            {isArray(announcements) && announcements.length === 0 && (
+              <ListItemText primary="Nenhum registro" />
+            )}
+          </List>
+        </Paper>
+      </Popover>
+    </div>
+  );
+}

+ 23 - 0
frontend/src/components/BackdropLoading/index.js

@@ -0,0 +1,23 @@
+import React from "react";
+
+import Backdrop from "@material-ui/core/Backdrop";
+import CircularProgress from "@material-ui/core/CircularProgress";
+import { makeStyles } from "@material-ui/core/styles";
+
+const useStyles = makeStyles(theme => ({
+	backdrop: {
+		zIndex: theme.zIndex.drawer + 1,
+		color: "#fff",
+	},
+}));
+
+const BackdropLoading = () => {
+	const classes = useStyles();
+	return (
+		<Backdrop className={classes.backdrop} open={true}>
+			<CircularProgress color="inherit" />
+		</Backdrop>
+	);
+};
+
+export default BackdropLoading;

+ 35 - 0
frontend/src/components/ButtonWithSpinner/index.js

@@ -0,0 +1,35 @@
+import React from "react";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import { CircularProgress, Button } from "@material-ui/core";
+
+const useStyles = makeStyles(theme => ({
+	button: {
+		position: "relative",
+	},
+
+	buttonProgress: {
+		color: green[500],
+		position: "absolute",
+		top: "50%",
+		left: "50%",
+		marginTop: -12,
+		marginLeft: -12,
+	},
+}));
+
+const ButtonWithSpinner = ({ loading, children, ...rest }) => {
+	const classes = useStyles();
+
+	return (
+		<Button className={classes.button} disabled={loading} {...rest}>
+			{children}
+			{loading && (
+				<CircularProgress size={24} className={classes.buttonProgress} />
+			)}
+		</Button>
+	);
+};
+
+export default ButtonWithSpinner;

+ 634 - 0
frontend/src/components/CampaignModal/index.js

@@ -0,0 +1,634 @@
+import React, { useState, useEffect, useRef, useContext } from "react";
+
+import * as Yup from "yup";
+import { Formik, Form, Field } from "formik";
+import { toast } from "react-toastify";
+import { head } from "lodash";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Button from "@material-ui/core/Button";
+import IconButton from "@material-ui/core/IconButton";
+import TextField from "@material-ui/core/TextField";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import CircularProgress from "@material-ui/core/CircularProgress";
+import AttachFileIcon from "@material-ui/icons/AttachFile";
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+
+import { i18n } from "../../translate/i18n";
+import moment from "moment";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+import {
+  Box,
+  FormControl,
+  Grid,
+  InputLabel,
+  MenuItem,
+  Select,
+  Tab,
+  Tabs,
+} from "@material-ui/core";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import ConfirmationModal from "../ConfirmationModal";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    display: "flex",
+    flexWrap: "wrap",
+    backgroundColor: "#fff"
+  },
+
+  tabmsg: {
+    backgroundColor: theme.palette.campaigntab,
+  },
+
+  textField: {
+    marginRight: theme.spacing(1),
+    flex: 1,
+  },
+
+  extraAttr: {
+    display: "flex",
+    justifyContent: "center",
+    alignItems: "center",
+  },
+
+  btnWrapper: {
+    position: "relative",
+  },
+
+  buttonProgress: {
+    color: green[500],
+    position: "absolute",
+    top: "50%",
+    left: "50%",
+    marginTop: -12,
+    marginLeft: -12,
+  },
+}));
+
+const CampaignSchema = Yup.object().shape({
+  name: Yup.string()
+    .min(2, i18n.t("campaigns.dialog.form.nameShort"))
+    .max(50, i18n.t("campaigns.dialog.form.nameLong"))
+    .required(i18n.t("campaigns.dialog.form.nameRequired")),
+});
+
+const CampaignModal = ({
+  open,
+  onClose,
+  campaignId,
+  initialValues,
+  onSave,
+  resetPagination,
+}) => {
+  const classes = useStyles();
+  const isMounted = useRef(true);
+  const { user } = useContext(AuthContext);
+  const { companyId } = user;
+  const [file, setFile] = useState(null);
+
+  const initialState = {
+    name: "",
+    message1: "",
+    message2: "",
+    message3: "",
+    message4: "",
+    message5: "",
+    status: "INATIVA", // INATIVA, PROGRAMADA, EM_ANDAMENTO, CANCELADA, FINALIZADA,
+    scheduledAt: "",
+    whatsappId: "",
+    contactListId: "",
+    tagListId: "Nenhuma",
+    companyId,
+  };
+
+  const [campaign, setCampaign] = useState(initialState);
+  const [whatsapps, setWhatsapps] = useState([]);
+  const [contactLists, setContactLists] = useState([]);
+  const [messageTab, setMessageTab] = useState(0);
+  const [attachment, setAttachment] = useState(null);
+  const [confirmationOpen, setConfirmationOpen] = useState(false);
+  const [campaignEditable, setCampaignEditable] = useState(true);
+  const attachmentFile = useRef(null);
+  const [tagLists, setTagLists] = useState([]);
+
+  useEffect(() => {
+    return () => {
+      isMounted.current = false;
+    };
+  }, []);
+
+  useEffect(() => {
+    (async () => {
+      try {
+        const { data } = await api.get("/files/", {
+          params: { companyId }
+        });
+
+        setFile(data.files);
+      } catch (err) {
+        toastError(err);
+      }
+    })();
+  }, []);
+
+  useEffect(() => {
+    if (isMounted.current) {
+      if (initialValues) {
+        setCampaign((prevState) => {
+          return { ...prevState, ...initialValues };
+        });
+      }
+
+      api
+        .get(`/contact-lists/list`, { params: { companyId } })
+        .then(({ data }) => setContactLists(data));
+
+      api
+        .get(`/whatsapp`, { params: { companyId, session: 0 } })
+        .then(({ data }) => setWhatsapps(data));
+
+      api.get(`/tags`, { params: { companyId } })
+        .then(({ data }) => {
+          const fetchedTags = data.tags;
+          // Perform any necessary data transformation here
+          const formattedTagLists = fetchedTags.map((tag) => ({
+            id: tag.id,
+            name: tag.name,
+          }));
+          setTagLists(formattedTagLists);
+        })
+        .catch((error) => {
+          console.error("Error retrieving tags:", error);
+        });
+        
+      if (!campaignId) return;
+
+      api.get(`/campaigns/${campaignId}`).then(({ data }) => {
+        setCampaign((prev) => {
+          let prevCampaignData = Object.assign({}, prev);
+
+          Object.entries(data).forEach(([key, value]) => {
+            if (key === "scheduledAt" && value !== "" && value !== null) {
+              prevCampaignData[key] = moment(value).format("YYYY-MM-DDTHH:mm");
+            } else {
+              prevCampaignData[key] = value === null ? "" : value;
+            }
+          });
+
+          return {...prevCampaignData, tagListId: data.tagId || "Nenhuma"};
+        });
+      });
+    }
+  }, [campaignId, open, initialValues, companyId]);
+
+  useEffect(() => {
+    const now = moment();
+    const scheduledAt = moment(campaign.scheduledAt);
+    const moreThenAnHour =
+      !Number.isNaN(scheduledAt.diff(now)) && scheduledAt.diff(now, "hour") > 1;
+    const isEditable =
+      campaign.status === "INATIVA" ||
+      (campaign.status === "PROGRAMADA" && moreThenAnHour);
+
+    setCampaignEditable(isEditable);
+  }, [campaign.status, campaign.scheduledAt]);
+
+  const handleClose = () => {
+    onClose();
+    setCampaign(initialState);
+  };
+
+  const handleAttachmentFile = (e) => {
+    const file = head(e.target.files);
+    if (file) {
+      setAttachment(file);
+    }
+  };
+
+  const handleSaveCampaign = async (values) => {
+    try {
+      const dataValues = {};
+      Object.entries(values).forEach(([key, value]) => {
+        if (key === "scheduledAt" && value !== "" && value !== null) {
+          dataValues[key] = moment(value).format("YYYY-MM-DD HH:mm:ss");
+        } else {
+          dataValues[key] = value === "" ? null : value;
+        }
+      });
+
+      if (campaignId) {
+        await api.put(`/campaigns/${campaignId}`, dataValues);
+
+        if (attachment != null) {
+          const formData = new FormData();
+          formData.append("file", attachment);
+          await api.post(`/campaigns/${campaignId}/media-upload`, formData);
+        }
+        handleClose();
+      } else {
+        const { data } = await api.post("/campaigns", dataValues);
+
+        if (attachment != null) {
+          const formData = new FormData();
+          formData.append("file", attachment);
+          await api.post(`/campaigns/${data.id}/media-upload`, formData);
+        }
+        if (onSave) {
+          onSave(data);
+        }
+        handleClose();
+      }
+      toast.success(i18n.t("campaigns.toasts.success"));
+    } catch (err) {
+      console.log(err);
+      toastError(err);
+    }
+  };
+
+  const deleteMedia = async () => {
+    if (attachment) {
+      setAttachment(null);
+      attachmentFile.current.value = null;
+    }
+
+    if (campaign.mediaPath) {
+      await api.delete(`/campaigns/${campaign.id}/media-upload`);
+      setCampaign((prev) => ({ ...prev, mediaPath: null, mediaName: null }));
+      toast.success(i18n.t("campaigns.toasts.deleted"));
+    }
+  };
+
+  const renderMessageField = (identifier) => {
+    return (
+      <Field
+        as={TextField}
+        id={identifier}
+        name={identifier}
+        fullWidth
+        rows={5}
+        label={i18n.t(`campaigns.dialog.form.${identifier}`)}
+        placeholder={i18n.t("campaigns.dialog.form.messagePlaceholder")}
+        multiline={true}
+        variant="outlined"
+        helperText={i18n.t("campaigns.dialog.form.helper")}
+        disabled={!campaignEditable && campaign.status !== "CANCELADA"}
+      />
+    );
+  };
+
+  const cancelCampaign = async () => {
+    try {
+      await api.post(`/campaigns/${campaign.id}/cancel`);
+      toast.success(i18n.t("campaigns.toasts.cancel"));
+      setCampaign((prev) => ({ ...prev, status: "CANCELADA" }));
+      resetPagination();
+    } catch (err) {
+      toast.error(err.message);
+    }
+  };
+
+  const restartCampaign = async () => {
+    try {
+      await api.post(`/campaigns/${campaign.id}/restart`);
+      toast.success(i18n.t("campaigns.toasts.restart"));
+      setCampaign((prev) => ({ ...prev, status: "EM_ANDAMENTO" }));
+      resetPagination();
+    } catch (err) {
+      toast.error(err.message);
+    }
+  };
+
+  return (
+    <div className={classes.root}>
+      <ConfirmationModal
+        title={i18n.t("campaigns.confirmationModal.deleteTitle")}
+        open={confirmationOpen}
+        onClose={() => setConfirmationOpen(false)}
+        onConfirm={deleteMedia}
+      >
+        {i18n.t("campaigns.confirmationModal.deleteMessage")}
+      </ConfirmationModal>
+      <Dialog
+        open={open}
+        onClose={handleClose}
+        fullWidth
+        maxWidth="md"
+        scroll="paper"
+      >
+        <DialogTitle id="form-dialog-title">
+          {campaignEditable ? (
+            <>
+              {campaignId
+                ? `${i18n.t("campaigns.dialog.update")}`
+                : `${i18n.t("campaigns.dialog.new")}`}
+            </>
+          ) : (
+            <>{`${i18n.t("campaigns.dialog.readonly")}`}</>
+          )}
+        </DialogTitle>
+        <div style={{ display: "none" }}>
+          <input
+            type="file"
+            ref={attachmentFile}
+            onChange={(e) => handleAttachmentFile(e)}
+          />
+        </div>
+        <Formik
+          initialValues={campaign}
+          enableReinitialize={true}
+          validationSchema={CampaignSchema}
+          onSubmit={(values, actions) => {
+            setTimeout(() => {
+              handleSaveCampaign(values);
+              actions.setSubmitting(false);
+            }, 400);
+          }}
+        >
+          {({ values, errors, touched, isSubmitting }) => (
+            <Form>
+              <DialogContent dividers>
+                <Grid spacing={2} container>
+                  <Grid xs={12} md={9} item>
+                    <Field
+                      as={TextField}
+                      label={i18n.t("campaigns.dialog.form.name")}
+                      name="name"
+                      error={touched.name && Boolean(errors.name)}
+                      helperText={touched.name && errors.name}
+                      variant="outlined"
+                      margin="dense"
+                      fullWidth
+                      className={classes.textField}
+                      disabled={!campaignEditable}
+                    />
+                  </Grid>
+                  <Grid xs={12} md={4} item>
+                    <FormControl
+                      variant="outlined"
+                      margin="dense"
+                      fullWidth
+                      className={classes.formControl}
+                    >
+                      <InputLabel id="contactList-selection-label">
+                        {i18n.t("campaigns.dialog.form.contactList")}
+                      </InputLabel>
+                      <Field
+                        as={Select}
+                        label={i18n.t("campaigns.dialog.form.contactList")}
+                        placeholder={i18n.t(
+                          "campaigns.dialog.form.contactList"
+                        )}
+                        labelId="contactList-selection-label"
+                        id="contactListId"
+                        name="contactListId"
+                        error={
+                          touched.contactListId && Boolean(errors.contactListId)
+                        }
+                        disabled={!campaignEditable}
+                      >
+                        <MenuItem value="">Nenhuma</MenuItem>
+                        {contactLists &&
+                          contactLists.map((contactList) => (
+                            <MenuItem
+                              key={contactList.id}
+                              value={contactList.id}
+                            >
+                              {contactList.name}
+                            </MenuItem>
+                          ))}
+                      </Field>
+                    </FormControl>
+                  </Grid>
+                  <Grid xs={12} md={4} item>
+                    <FormControl
+                      variant="outlined"
+                      margin="dense"
+                      fullWidth
+                      className={classes.formControl}
+                    >
+                      <InputLabel id="tagList-selection-label">
+                        {i18n.t("campaigns.dialog.form.tagList")}
+                      </InputLabel>
+                      <Field
+                        as={Select}
+                        label={i18n.t("campaigns.dialog.form.tagList")}
+                        placeholder={i18n.t("campaigns.dialog.form.tagList")}
+                        labelId="tagList-selection-label"
+                        id="tagListId"
+                        name="tagListId"
+                        error={touched.tagListId && Boolean(errors.tagListId)}
+                        disabled={!campaignEditable}
+                      >
+                        <MenuItem value="">Nenhuma</MenuItem>
+                        {Array.isArray(tagLists) &&
+                          tagLists.map((tagList) => (
+                            <MenuItem key={tagList.id} value={tagList.id}>
+                              {tagList.name}
+                            </MenuItem>
+                          ))}
+                      </Field>
+                    </FormControl>
+                  </Grid>
+                  <Grid xs={12} md={4} item>
+                    <FormControl
+                      variant="outlined"
+                      margin="dense"
+                      fullWidth
+                      className={classes.formControl}
+                    >
+                      <InputLabel id="whatsapp-selection-label">
+                        {i18n.t("campaigns.dialog.form.whatsapp")}
+                      </InputLabel>
+                      <Field
+                        as={Select}
+                        label={i18n.t("campaigns.dialog.form.whatsapp")}
+                        placeholder={i18n.t("campaigns.dialog.form.whatsapp")}
+                        labelId="whatsapp-selection-label"
+                        id="whatsappId"
+                        name="whatsappId"
+                        error={touched.whatsappId && Boolean(errors.whatsappId)}
+                        disabled={!campaignEditable}
+                      >
+                        <MenuItem value="">Nenhuma</MenuItem>
+                        {whatsapps &&
+                          whatsapps.map((whatsapp) => (
+                            <MenuItem key={whatsapp.id} value={whatsapp.id}>
+                              {whatsapp.name}
+                            </MenuItem>
+                          ))}
+                      </Field>
+                    </FormControl>
+                  </Grid>
+                  <Grid xs={12} md={4} item>
+                    <Field
+                      as={TextField}
+                      label={i18n.t("campaigns.dialog.form.scheduledAt")}
+                      name="scheduledAt"
+                      error={touched.scheduledAt && Boolean(errors.scheduledAt)}
+                      helperText={touched.scheduledAt && errors.scheduledAt}
+                      variant="outlined"
+                      margin="dense"
+                      type="datetime-local"
+                      InputLabelProps={{
+                        shrink: true,
+                      }}
+                      fullWidth
+                      className={classes.textField}
+                      disabled={!campaignEditable}
+                    />
+                  </Grid>
+                  <Grid xs={12} md={4} item>
+                  <FormControl
+                      variant="outlined"
+                      margin="dense"
+                      className={classes.FormControl}
+                      fullWidth
+                    >
+                      <InputLabel id="fileListId-selection-label">{i18n.t("campaigns.dialog.form.fileList")}</InputLabel>
+                      <Field
+                        as={Select}
+                        label={i18n.t("campaigns.dialog.form.fileList")}
+                        name="fileListId"
+                        id="fileListId"
+                        placeholder={i18n.t("campaigns.dialog.form.fileList")}
+                        labelId="fileListId-selection-label"
+                        value={values.fileListId || ""}
+                      >
+                        <MenuItem value={""} >{"Nenhum"}</MenuItem>
+                        {file.map(f => (
+                          <MenuItem key={f.id} value={f.id}>
+                            {f.name}
+                          </MenuItem>
+                        ))}
+                      </Field>
+                    </FormControl>
+                  </Grid>
+                  <Grid xs={12} item>
+                    <Tabs
+                      value={messageTab}
+                      indicatorColor="primary"
+                      textColor="primary"
+                      className={classes.tabmsg}
+                      onChange={(e, v) => setMessageTab(v)}
+                      variant="fullWidth"
+                      centered
+                      style={{
+                        borderRadius: 2,
+                      }}
+                    >
+                      <Tab label="Msg. 1" index={0} />
+                      <Tab label="Msg. 2" index={1} />
+                      <Tab label="Msg. 3" index={2} />
+                      <Tab label="Msg. 4" index={3} />
+                      <Tab label="Msg. 5" index={4} />
+                    </Tabs>
+                    <Box style={{ paddingTop: 20, border: "none" }}>
+                      {messageTab === 0 && (
+                        <>{renderMessageField("message1")}</>
+                      )}
+                      {messageTab === 1 && (
+                        <>{renderMessageField("message2")}</>
+                      )}
+                      {messageTab === 2 && (
+                        <>{renderMessageField("message3")}</>
+                      )}
+                      {messageTab === 3 && (
+                        <>{renderMessageField("message4")}</>
+                      )}
+                      {messageTab === 4 && (
+                        <>{renderMessageField("message5")}</>
+                      )}
+                    </Box>
+                  </Grid>
+                  {(campaign.mediaPath || attachment) && (
+                    <Grid xs={12} item>
+                      <Button startIcon={<AttachFileIcon />}>
+                        {attachment != null
+                          ? attachment.name
+                          : campaign.mediaName}
+                      </Button>
+                      {campaignEditable && (
+                        <IconButton
+                          onClick={() => setConfirmationOpen(true)}
+                          color="secondary"
+                        >
+                          <DeleteOutlineIcon />
+                        </IconButton>
+                      )}
+                    </Grid>
+                  )}
+                </Grid>
+              </DialogContent>
+              <DialogActions>
+                {campaign.status === "CANCELADA" && (
+                  <Button
+                    color="primary"
+                    onClick={() => restartCampaign()}
+                    variant="outlined"
+                  >
+                    {i18n.t("campaigns.dialog.buttons.restart")}
+                  </Button>
+                )}
+                {campaign.status === "EM_ANDAMENTO" && (
+                  <Button
+                    color="primary"
+                    onClick={() => cancelCampaign()}
+                    variant="outlined"
+                  >
+                    {i18n.t("campaigns.dialog.buttons.cancel")}
+                  </Button>
+                )}
+                {!attachment && !campaign.mediaPath && campaignEditable && (
+                  <Button
+                    color="primary"
+                    onClick={() => attachmentFile.current.click()}
+                    disabled={isSubmitting}
+                    variant="outlined"
+                  >
+                    {i18n.t("campaigns.dialog.buttons.attach")}
+                  </Button>
+                )}
+                <Button
+                  onClick={handleClose}
+                  color="secondary"
+                  disabled={isSubmitting}
+                  variant="outlined"
+                >
+                  {i18n.t("campaigns.dialog.buttons.close")}
+                </Button>
+                {(campaignEditable || campaign.status === "CANCELADA") && (
+                  <Button
+                    type="submit"
+                    color="primary"
+                    disabled={isSubmitting}
+                    variant="contained"
+                    className={classes.btnWrapper}
+                  >
+                    {campaignId
+                      ? `${i18n.t("campaigns.dialog.buttons.edit")}`
+                      : `${i18n.t("campaigns.dialog.buttons.add")}`}
+                    {isSubmitting && (
+                      <CircularProgress
+                        size={24}
+                        className={classes.buttonProgress}
+                      />
+                    )}
+                  </Button>
+                )}
+              </DialogActions>
+            </Form>
+          )}
+        </Formik>
+      </Dialog>
+    </div>
+  );
+};
+
+export default CampaignModal;

+ 39 - 0
frontend/src/components/Can/index.js

@@ -0,0 +1,39 @@
+import rules from "../../rules";
+
+const check = (role, action, data) => {
+	const permissions = rules[role];
+	if (!permissions) {
+		// role is not present in the rules
+		return false;
+	}
+
+	const staticPermissions = permissions.static;
+
+	if (staticPermissions && staticPermissions.includes(action)) {
+		// static rule not provided for action
+		return true;
+	}
+
+	const dynamicPermissions = permissions.dynamic;
+
+	if (dynamicPermissions) {
+		const permissionCondition = dynamicPermissions[action];
+		if (!permissionCondition) {
+			// dynamic rule not provided for action
+			return false;
+		}
+
+		return permissionCondition(data);
+	}
+	return false;
+};
+
+const Can = ({ role, perform, data, yes, no }) =>
+	check(role, perform, data) ? yes() : no();
+
+Can.defaultProps = {
+	yes: () => null,
+	no: () => null,
+};
+
+export { Can };

+ 175 - 0
frontend/src/components/CheckoutPage/CheckoutPage.js

@@ -0,0 +1,175 @@
+import React, { useContext, useState } from "react";
+import {
+  Stepper,
+  Step,
+  StepLabel,
+  Button,
+  Typography,
+  CircularProgress,
+} from "@material-ui/core";
+import { Formik, Form } from "formik";
+
+import AddressForm from "./Forms/AddressForm";
+import PaymentForm from "./Forms/PaymentForm";
+import ReviewOrder from "./ReviewOrder";
+import CheckoutSuccess from "./CheckoutSuccess";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+import { toast } from "react-toastify";
+import { AuthContext } from "../../context/Auth/AuthContext";
+
+
+import validationSchema from "./FormModel/validationSchema";
+import checkoutFormModel from "./FormModel/checkoutFormModel";
+import formInitialValues from "./FormModel/formInitialValues";
+
+import useStyles from "./styles";
+import Invoices from "../../pages/Financeiro";
+import { i18n } from "../../translate/i18n";
+
+
+export default function CheckoutPage(props) {
+  const steps = [i18n.t("checkoutPage.steps.data"), i18n.t("checkoutPage.steps.customize"), i18n.t("checkoutPage.steps.review")];
+  const { formId, formField } = checkoutFormModel;
+  
+  
+  
+  const classes = useStyles();
+  const [activeStep, setActiveStep] = useState(1);
+  const [datePayment, setDatePayment] = useState(null);
+  const [invoiceId, setinvoiceId] = useState(props.Invoice.id);
+  const currentValidationSchema = validationSchema[activeStep];
+  const isLastStep = activeStep === steps.length - 1;
+  const { user } = useContext(AuthContext);
+
+function _renderStepContent(step, setFieldValue, setActiveStep, values ) {
+
+  switch (step) {
+    case 0:
+      return <AddressForm formField={formField} values={values} setFieldValue={setFieldValue}  />;
+    case 1:
+      return <PaymentForm 
+      formField={formField} 
+      setFieldValue={setFieldValue} 
+      setActiveStep={setActiveStep} 
+      activeStep={step} 
+      invoiceId={invoiceId}
+      values={values}
+      />;
+    case 2:
+      return <ReviewOrder />;
+    default:
+      return <div>Not Found</div>;
+  }
+}
+
+
+  async function _submitForm(values, actions) {
+    try {
+      const plan = JSON.parse(values.plan);
+      const newValues = {
+        firstName: values.firstName,
+        lastName: values.lastName,
+        address2: values.address2,
+        city: values.city,
+        state: values.state,
+        zipcode: values.zipcode,
+        country: values.country,
+        useAddressForPaymentDetails: values.useAddressForPaymentDetails,
+        nameOnCard: values.nameOnCard,
+        cardNumber: values.cardNumber,
+        cvv: values.cvv,
+        plan: values.plan,
+        price: plan.price,
+        users: plan.users,
+        connections: plan.connections,
+        invoiceId: invoiceId
+      }
+
+      const { data } = await api.post("/subscription", newValues);
+      setDatePayment(data)
+      actions.setSubmitting(false);
+      setActiveStep(activeStep + 1);
+      toast.success(i18n.t("checkoutPage.success"));
+    } catch (err) {
+      toastError(err);
+    }
+  }
+
+  function _handleSubmit(values, actions) {
+    if (isLastStep) {
+      _submitForm(values, actions);
+    } else {
+      setActiveStep(activeStep + 1);
+      actions.setTouched({});
+      actions.setSubmitting(false);
+    }
+  }
+
+  function _handleBack() {
+    setActiveStep(activeStep - 1);
+  }
+
+  return (
+    <React.Fragment>
+      <Typography component="h1" variant="h4" align="center">
+        {i18n.t("checkoutPage.closeToEnd")}
+      </Typography>
+      <Stepper activeStep={activeStep} className={classes.stepper}>
+        {steps.map((label) => (
+          <Step key={label}>
+            <StepLabel>{label}</StepLabel>
+          </Step>
+        ))}
+      </Stepper>
+      <React.Fragment>
+        {activeStep === steps.length ? (
+          <CheckoutSuccess pix={datePayment} />
+        ) : (
+          <Formik
+            initialValues={{
+              ...user, 
+              ...formInitialValues
+            }}
+            validationSchema={currentValidationSchema}
+            onSubmit={_handleSubmit}
+          >
+            {({ isSubmitting, setFieldValue, values }) => (
+              <Form id={formId}>
+                {_renderStepContent(activeStep, setFieldValue, setActiveStep, values)}
+
+                <div className={classes.buttons}>
+                  {activeStep !== 1 && (
+                    <Button onClick={_handleBack} className={classes.button}>
+                      {i18n.t("checkoutPage.BACK")}
+                    </Button>
+                  )}
+                  <div className={classes.wrapper}>
+                    {activeStep !== 1 && (
+                      <Button
+                        disabled={isSubmitting}
+                        type="submit"
+                        variant="contained"
+                        color="primary"
+                        className={classes.button}
+                      >
+                        {isLastStep ? i18n.t("checkoutPage.PAY") : i18n.t("checkoutPage.NEXT")}
+                      </Button>
+                    )}
+                    {isSubmitting && (
+                      <CircularProgress
+                        size={24}
+                        className={classes.buttonProgress}
+                      />
+                    )}
+                  </div>
+                </div>
+              </Form>
+            )}
+          </Formik>
+        )}
+      </React.Fragment>
+    </React.Fragment>
+  );
+}

+ 76 - 0
frontend/src/components/CheckoutPage/CheckoutSuccess/CheckoutSuccess.js

@@ -0,0 +1,76 @@
+import React, { useState, useEffect, useContext } from 'react';
+import { useHistory } from "react-router-dom";
+import QRCode from 'react-qr-code';
+import { SuccessContent, Total } from './style';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
+import { FaCopy, FaCheckCircle } from 'react-icons/fa';
+import { SocketContext } from "../../../context/Socket/SocketContext";
+import { useDate } from "../../../hooks/useDate";
+import { toast } from "react-toastify";
+
+function CheckoutSuccess(props) {
+
+  const { pix } = props;
+  const [pixString,] = useState(pix.qrcode.qrcode);
+  const [copied, setCopied] = useState(false);
+  const history = useHistory();
+
+  const { dateToClient } = useDate();
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    const companyId = localStorage.getItem("companyId");
+    const socket = socketManager.getSocket(companyId);
+    
+    socket.on(`company-${companyId}-payment`, (data) => {
+
+      if (data.action === "CONCLUIDA") {
+        toast.success(`Sua licença foi renovada até ${dateToClient(data.company.dueDate)}!`);
+        setTimeout(() => {
+          history.push("/");
+        }, 4000);
+      }
+    });
+  }, [history, socketManager]);
+
+  const handleCopyQR = () => {
+    setTimeout(() => {
+      setCopied(false);
+    }, 1 * 1000);
+    setCopied(true);
+  };
+
+  return (
+    <React.Fragment>
+      <Total>
+        <span>TOTAL</span>
+        <strong>R${pix.valor.original.toLocaleString('pt-br', { minimumFractionDigits: 2 })}</strong>
+      </Total>
+      <SuccessContent>
+        <QRCode value={pixString} />
+        <CopyToClipboard text={pixString} onCopy={handleCopyQR}>
+          <button className="copy-button" type="button">
+            {copied ? (
+              <>
+                <span>Copiado</span>
+                <FaCheckCircle size={18} />
+              </>
+            ) : (
+              <>
+                <span>Copiar código QR</span>
+                <FaCopy size={18} />
+              </>
+            )}
+          </button>
+        </CopyToClipboard>
+        <span>
+          Para finalizar, basta realizar o pagamento escaneando ou colando o
+          código Pix acima :)
+        </span>
+      </SuccessContent>
+    </React.Fragment>
+  );
+}
+
+export default CheckoutSuccess;

+ 2 - 0
frontend/src/components/CheckoutPage/CheckoutSuccess/index.js

@@ -0,0 +1,2 @@
+import CheckoutSuccess from './CheckoutSuccess';
+export default CheckoutSuccess;

+ 117 - 0
frontend/src/components/CheckoutPage/CheckoutSuccess/style.js

@@ -0,0 +1,117 @@
+import styled from 'styled-components';
+
+export const Container = styled.div`
+  footer {
+    margin-top: 30px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    width: 100%;
+
+    @media (max-width: 768px) {
+      flex-direction: column;
+      .checkout-buttons {
+        display: flex;
+        flex-direction: column-reverse;
+        width: 100%;
+
+        button {
+          width: 100%;
+          margin-top: 16px;
+          margin-left: 0;
+        }
+      }
+    }
+
+    button {
+      margin-left: 16px;
+    }
+  }
+`;
+export const Total = styled.div`
+  display: flex;
+  align-items: baseline;
+
+  span {
+    color: #333;
+    font-weight: bold;
+  }
+
+  strong {
+    color: #333;
+    font-size: 28px;
+    margin-left: 5px;
+  }
+
+  @media (max-width: 768px) {
+    min-width: 100%;
+    justify-content: space-between;
+  }
+`;
+
+export const SuccessContent = styled.div`
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+
+  > h2 {
+    text-align: center;
+  }
+
+  > svg {
+    margin-top: 16px;
+  }
+
+  > span {
+    margin-top: 24px;
+    text-align: center;
+  }
+
+  > p,
+  strong {
+    margin-top: 8px;
+    font-size: 9px;
+    color: #999;
+  }
+
+  .copy-button {
+    font-size: 14px;
+    cursor: pointer;
+    text-align: center;
+    user-select: none;
+    min-height: 56px;
+    display: inline-flex;
+    -webkit-box-pack: center;
+    justify-content: center;
+    -webkit-box-align: center;
+    align-items: center;
+    background-color: #f9f9f9;
+    color: #000;
+    -webkit-appearance: none !important;
+    outline: none;
+    margin-top: 16px;
+    width: 256px;
+    font-weight: 600;
+    text-transform: uppercase;
+    border: none;
+
+    > span {
+      margin-right: 8px;
+    }
+  }
+`;
+
+export const CheckoutWrapper = styled.div`
+  width: 100%;
+  margin: 0 auto 0;
+  max-width: 1110px;
+  display: flex;
+  flex-direction: column;
+  -webkit-box-align: center;
+  align-items: center;
+  padding: 50px 95px;
+  background: #fff;
+  @media (max-width: 768px) {
+    padding: 16px 24px;
+`;

+ 75 - 0
frontend/src/components/CheckoutPage/FormModel/checkoutFormModel.js

@@ -0,0 +1,75 @@
+import { i18n } from "../../../translate/i18n";
+
+export default {
+  formId: 'checkoutForm',
+  formField: {
+    firstName: {
+      name: 'firstName',
+      label: i18n.t('checkoutPage.form.firstName.label'),
+      requiredErrorMsg: i18n.t('checkoutPage.form.firstName.required')
+    },
+    lastName: {
+      name: 'lastName',
+      label: i18n.t('checkoutPage.form.lastName.label'),
+      requiredErrorMsg: i18n.t('checkoutPage.form.lastName.required')
+    },
+    address1: {
+      name: 'address2',
+      label: i18n.t('checkoutPage.form.address1.label'),
+      requiredErrorMsg: i18n.t('checkoutPage.form.address1.required')
+    },
+
+    city: {
+      name: 'city',
+      label: i18n.t('checkoutPage.form.city.label'),
+      requiredErrorMsg: i18n.t('checkoutPage.form.city.required')
+    },
+    state: {
+      name: 'state',
+      label: i18n.t('checkoutPage.form.state.label'),
+      requiredErrorMsg: i18n.t('checkoutPage.form.state.required')
+    },
+    zipcode: {
+      name: 'zipcode',
+      label: i18n.t('checkoutPage.form.zipcode.label'),
+      requiredErrorMsg: i18n.t('checkoutPage.form.zipcode.required'),
+      invalidErrorMsg: i18n.t('checkoutPage.form.zipcode.invalid')
+    },
+    country: {
+      name: 'country',
+      label: i18n.t('checkoutPage.form.country.label'),
+      requiredErrorMsg: i18n.t('checkoutPage.form.country.required')
+    },
+    useAddressForPaymentDetails: {
+      name: 'useAddressForPaymentDetails',
+      label: i18n.t('checkoutPage.form.useAddressForPaymentDetails.label')
+    },
+    invoiceId: {
+      name: 'invoiceId',
+      label: i18n.t('checkoutPage.form.invoiceId.label'),
+    },
+    nameOnCard: {
+      name: 'nameOnCard',
+      label: i18n.t('checkoutPage.form.nameOnCard.label'),
+      requiredErrorMsg: i18n.t('checkoutPage.form.nameOnCard.required')
+    },
+    cardNumber: {
+      name: 'cardNumber',
+      label: i18n.t('checkoutPage.form.cardNumber.label'),
+      requiredErrorMsg: i18n.t('checkoutPage.form.cardNumber.required'),
+      invalidErrorMsg: i18n.t('checkoutPage.form.cardNumber.invalid')
+    },
+    expiryDate: {
+      name: 'expiryDate',
+      label: i18n.t('checkoutPage.form.expiryDate.label'),
+      requiredErrorMsg: i18n.t('checkoutPage.form.expiryDate.required'),
+      invalidErrorMsg: i18n.t('checkoutPage.form.expiryDate.invalid')
+    },
+    cvv: {
+      name: 'cvv',
+      label: i18n.t('checkoutPage.form.cvv.label'),
+      requiredErrorMsg: i18n.t('checkoutPage.form.cvv.required'),
+      invalidErrorMsg: i18n.t('checkoutPage.form.cvv.invalid')
+    }
+  }
+};

+ 32 - 0
frontend/src/components/CheckoutPage/FormModel/formInitialValues.js

@@ -0,0 +1,32 @@
+import checkoutFormModel from './checkoutFormModel';
+const {
+  formField: {
+    firstName,
+    lastName,
+    address1,
+    city,
+    state,
+    zipcode,
+    country,
+    useAddressForPaymentDetails,
+    nameOnCard,
+    cardNumber,
+    invoiceId,
+    cvv
+  }
+} = checkoutFormModel;
+
+export default {
+  [firstName.name]: '',
+  [lastName.name]: '',
+  [address1.name]: '',
+  [city.name]: '',
+  [state.name]: '',
+  [zipcode.name]: '',
+  [country.name]: '',
+  [useAddressForPaymentDetails.name]: false,
+  [nameOnCard.name]: '',
+  [cardNumber.name]: '',
+  [invoiceId.name]: '',
+  [cvv.name]: ''
+};

+ 29 - 0
frontend/src/components/CheckoutPage/FormModel/validationSchema.js

@@ -0,0 +1,29 @@
+import * as Yup from 'yup';
+import checkoutFormModel from './checkoutFormModel';
+const {
+  formField: {
+    firstName,
+    address1,
+    city,
+    zipcode,
+    country,
+  }
+} = checkoutFormModel;
+
+
+export default [
+  Yup.object().shape({
+    [firstName.name]: Yup.string().required(`${firstName.requiredErrorMsg}`),
+    [address1.name]: Yup.string().required(`${address1.requiredErrorMsg}`),
+    [city.name]: Yup.string()
+      .nullable()
+      .required(`${city.requiredErrorMsg}`),
+    [zipcode.name]: Yup.string()
+      .required(`${zipcode.requiredErrorMsg}`),
+
+    [country.name]: Yup.string()
+      .nullable()
+      .required(`${country.requiredErrorMsg}`)
+  }),
+
+];

+ 134 - 0
frontend/src/components/CheckoutPage/Forms/AddressForm.js

@@ -0,0 +1,134 @@
+import React, { useContext, useEffect, useState } from "react";
+import { Grid, Typography } from "@material-ui/core";
+import { InputField, SelectField } from "../../FormFields";
+import { AuthContext } from "../../../context/Auth/AuthContext";
+
+const countries = [
+  {
+    value: "BR",
+    label: "Brasil",
+  },
+  {
+    value: "usa",
+    label: "United States",
+  },
+];
+
+export default function AddressForm(props) {
+
+  const { user } = useContext(AuthContext);
+  const [billingName, setBillingName] = useState(user.company.billingName);
+  const [addressZipCode, setAddressZipCode] = useState(user.company.addressZipCode);
+  const [addressStreet, setAddressStreet] = useState(user.company.addressStreet);
+  const [addressState, setAddressState] = useState(user.company.addressState);
+  const [addressCity, setAddressCity] = useState(user.company.addressCity);
+  const [addressDistrict, setAddressDistrict] = useState(user.company.addressDistrict);
+
+  const {
+    formField: {
+      firstName,
+      address1,
+      city,
+      state,
+      zipcode,
+      country,
+    },
+    setFieldValue
+  } = props;
+  useEffect(() => {
+    setFieldValue("firstName", billingName)
+    setFieldValue("zipcode", addressZipCode)
+    setFieldValue("address2", addressStreet)
+    setFieldValue("state", addressState)
+    setFieldValue("city", addressCity)
+    setFieldValue("country", addressDistrict)
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [])
+
+  return (
+    <React.Fragment>
+      <Typography variant="h6" gutterBottom>
+        Vamos precisar de algumas informações
+      </Typography>
+      <Grid container spacing={3}>
+
+        <Grid item xs={6} sm={6}>
+          <InputField name={firstName.name} label={firstName.label} fullWidth
+            value={billingName}
+            onChange={(e) => {
+              setBillingName(e.target.value)
+              setFieldValue("firstName", e.target.value)
+            }}
+          />
+        </Grid>
+        <Grid item xs={6} sm={6}>
+          <SelectField
+            name={country.name}
+            label={country.label}
+            data={countries}
+            fullWidth
+            value={addressDistrict}
+            onChange={(e) => {
+              setAddressDistrict(e.target.value)
+              setFieldValue("country", e.target.value)
+            }
+            }
+          />
+        </Grid>
+
+        <Grid item xs={4}>
+          <InputField
+            name={zipcode.name}
+            label={zipcode.label}
+            fullWidth
+            value={addressZipCode}
+            onChange={(e) => {
+              setAddressZipCode(e.target.value)
+              setFieldValue("zipcode", e.target.value)
+            }}
+          />
+        </Grid>
+        <Grid item xs={8}>
+          <InputField
+            name={address1.name}
+            label={address1.label}
+            fullWidth
+            value={addressStreet}
+            onChange={(e) => {
+              setAddressStreet(e.target.value)
+              setFieldValue("address2", e.target.value)
+
+            }}
+          />
+        </Grid>
+
+        <Grid item xs={4}>
+          <InputField
+            name={state.name}
+            label={state.label}
+            fullWidth
+            value={addressState}
+            onChange={(e) => {
+              setAddressState(e.target.value)
+              setFieldValue("state", e.target.value)
+
+            }}
+          />
+        </Grid>
+        <Grid item xs={8}>
+          <InputField
+            name={city.name}
+            label={city.label}
+            fullWidth
+            value={addressCity}
+            onChange={(e) => {
+              setAddressCity(e.target.value)
+              setFieldValue("city", e.target.value)
+            }}
+          />
+        </Grid>
+
+      </Grid>
+    </React.Fragment>
+  );
+}

+ 238 - 0
frontend/src/components/CheckoutPage/Forms/PaymentForm.js

@@ -0,0 +1,238 @@
+import React, { useState, useEffect, useReducer } from 'react';
+import Button from '@material-ui/core/Button';
+import Card from '@material-ui/core/Card';
+import CardActions from '@material-ui/core/CardActions';
+import CardContent from '@material-ui/core/CardContent';
+import CardHeader from '@material-ui/core/CardHeader';
+import Grid from '@material-ui/core/Grid';
+import StarIcon from '@material-ui/icons/StarBorder';
+import Typography from '@material-ui/core/Typography';
+import { makeStyles } from '@material-ui/core/styles';
+
+import IconButton from '@material-ui/core/IconButton';
+import MinimizeIcon from '@material-ui/icons/Minimize';
+import AddIcon from '@material-ui/icons/Add';
+
+import usePlans from "../../../hooks/usePlans";
+import useCompanies from "../../../hooks/useCompanies";
+import { i18n } from '../../../translate/i18n';
+
+const useStyles = makeStyles((theme) => ({
+  '@global': {
+    ul: {
+      margin: 0,
+      padding: 0,
+      listStyle: 'none',
+    },
+  },
+  margin: {
+    margin: theme.spacing(1),
+  },
+
+
+  cardHeader: {
+    backgroundColor:
+      theme.palette.type === 'light' ? theme.palette.grey[200] : theme.palette.grey[700],
+  },
+  cardPricing: {
+    display: 'flex',
+    justifyContent: 'center',
+    alignItems: 'baseline',
+    marginBottom: theme.spacing(2),
+  },
+  footer: {
+    borderTop: `1px solid ${theme.palette.divider}`,
+    marginTop: theme.spacing(8),
+    paddingTop: theme.spacing(3),
+    paddingBottom: theme.spacing(3),
+    [theme.breakpoints.up('sm')]: {
+      paddingTop: theme.spacing(6),
+      paddingBottom: theme.spacing(6),
+    },
+  },
+
+  customCard: {
+    display: "flex",
+    marginTop: "16px",
+    alignItems: "center",
+    flexDirection: "column",
+  },
+  custom: {
+    display: "flex",
+    alignItems: "center",
+    justifyContent: "space-between",
+  }
+}));
+
+
+export default function Pricing(props) {
+  const {
+    setFieldValue,
+    setActiveStep,
+    activeStep,
+  } = props;
+
+  const handleChangeAdd = (event, newValue) => {
+    if (newValue < 3) return
+
+    const newPrice = 11.00;
+
+    setUsersPlans(newValue);
+    setCustomValuePlans(customValuePlans + newPrice);
+  }
+
+  const handleChangeMin = (event, newValue) => {
+    if (newValue < 3) return
+
+    const newPrice = 11;
+
+    setUsersPlans(newValue);
+    setCustomValuePlans(customValuePlans - newPrice);
+  }
+
+  const handleChangeConnectionsAdd = (event, newValue) => {
+    if (newValue < 3) return
+    const newPrice = 20.00;
+    setConnectionsPlans(newValue);
+    setCustomValuePlans(customValuePlans + newPrice);
+  }
+
+  const handleChangeConnectionsMin = (event, newValue) => {
+    if (newValue < 3) return
+    const newPrice = 20;
+    setConnectionsPlans(newValue);
+    setCustomValuePlans(customValuePlans - newPrice);
+  }
+
+  const { list, finder } = usePlans();
+  const { find } = useCompanies();
+
+  const classes = useStyles();
+  const [usersPlans, setUsersPlans] = React.useState(3);
+  const [companiesPlans, setCompaniesPlans] = useState(0);
+  const [connectionsPlans, setConnectionsPlans] = React.useState(3);
+  const [storagePlans, setStoragePlans] = React.useState([]);
+  const [customValuePlans, setCustomValuePlans] = React.useState(49.00);
+  const [loading, setLoading] = React.useState(false);
+  const companyId = localStorage.getItem("companyId");
+
+  useEffect(() => {
+    async function fetchData() {
+      await loadCompanies();
+    }
+    fetchData();
+  }, [])
+
+  const loadCompanies = async () => {
+    setLoading(true);
+    try {
+      const companiesList = await find(companyId);
+      setCompaniesPlans(companiesList.planId);
+      await loadPlans(companiesList.planId);
+    } catch (e) {
+      console.log(e);
+      // toast.error("Não foi possível carregar a lista de registros");
+    }
+    setLoading(false);
+  };
+  const loadPlans = async (companiesPlans) => {
+    setLoading(true);
+    try {
+      const plansCompanies = await finder(companiesPlans);
+      const plans = []
+
+      //plansCompanies.forEach((plan) => {
+      plans.push({
+        title: plansCompanies.name,
+        planId: plansCompanies.id,
+        price: plansCompanies.value,
+        description: [
+          `${plansCompanies.users} ${i18n.t("checkoutPage.pricing.users")}`,
+          `${plansCompanies.connections} ${i18n.t("checkoutPage.pricing.connection")}`,
+          `${plansCompanies.queues} ${i18n.t("checkoutPage.pricing.queues")}`
+        ],
+        users: plansCompanies.users,
+        connections: plansCompanies.connections,
+        queues: plansCompanies.queues,
+        buttonText: i18n.t("checkoutPage.pricing.SELECT"),
+        buttonVariant: 'outlined',
+      })
+
+      // setStoragePlans(data);
+      //});
+      setStoragePlans(plans);
+    } catch (e) {
+      console.log(e);
+      // toast.error("Não foi possível carregar a lista de registros");
+    }
+    setLoading(false);
+  };
+
+
+  const tiers = storagePlans
+  return (
+    <React.Fragment>
+      <Grid container spacing={3}>
+        {tiers.map((tier) => (
+          // Enterprise card is full width at sm breakpoint
+          <Grid item key={tier.title} xs={12} sm={tier.title === 'Enterprise' ? 12 : 12} md={12}>
+            <Card>
+              <CardHeader
+                title={tier.title}
+                subheader={tier.subheader}
+                titleTypographyProps={{ align: 'center' }}
+                subheaderTypographyProps={{ align: 'center' }}
+                action={tier.title === 'Pro' ? <StarIcon /> : null}
+                className={classes.cardHeader}
+              />
+              <CardContent>
+                <div className={classes.cardPricing}>
+                  <Typography component="h2" variant="h3" color="textPrimary">
+                    {
+
+                      <React.Fragment>
+                        R${tier.price.toLocaleString('pt-br', { minimumFractionDigits: 2 })}
+                      </React.Fragment>
+                    }
+                  </Typography>
+                  <Typography variant="h6" color="textSecondary">
+                    /{i18n.t("checkoutPage.pricing.month")}
+                  </Typography>
+                </div>
+                <ul>
+                  {tier.description.map((line) => (
+                    <Typography component="li" variant="subtitle1" align="center" key={line}>
+                      {line}
+                    </Typography>
+                  ))}
+                </ul>
+              </CardContent>
+              <CardActions>
+                <Button
+                  fullWidth
+                  variant={tier.buttonVariant}
+                  color="primary"
+                  onClick={() => {
+                    if (tier.custom) {
+                      setFieldValue("plan", JSON.stringify({
+                        users: usersPlans,
+                        connections: connectionsPlans,
+                        price: customValuePlans,
+                      }));
+                    } else {
+                      setFieldValue("plan", JSON.stringify(tier));
+                    }
+                    setActiveStep(activeStep + 1);
+                  }
+                  }
+                >
+                  {tier.buttonText}
+                </Button>
+              </CardActions>
+            </Card>
+          </Grid>
+        ))}
+      </Grid>
+    </React.Fragment>
+  );
+}

+ 61 - 0
frontend/src/components/CheckoutPage/ReviewOrder/PaymentDetails.js

@@ -0,0 +1,61 @@
+import React, {useContext} from 'react';
+import { Typography, Grid } from '@material-ui/core';
+import useStyles from './styles';
+import { AuthContext } from "../../../context/Auth/AuthContext";
+
+function PaymentDetails(props) {
+  const { formValues } = props;
+  const classes = useStyles();
+  const { firstName, address2, city, zipcode, state, country, plan } = formValues;
+  const { user } = useContext(AuthContext);
+
+
+  const newPlan = JSON.parse(plan);
+  const { price } = newPlan;
+
+  return (
+    <Grid item container direction="column" xs={12} sm={6}>
+      <Typography variant="h6" gutterBottom className={classes.title}>
+        Informação de pagamento
+      </Typography>
+      <Grid container>
+        <React.Fragment>
+          <Grid item xs={6}>
+            <Typography gutterBottom>Email:</Typography>
+          </Grid>
+          <Grid item xs={6}>
+            <Typography gutterBottom>{user.company.email}</Typography>
+          </Grid>
+        </React.Fragment>
+        <React.Fragment>
+          <Grid item xs={6}>
+            <Typography gutterBottom>Nome:</Typography>
+          </Grid>
+          <Grid item xs={6}>
+            <Typography gutterBottom>{firstName}</Typography>
+          </Grid>
+        </React.Fragment>
+        <React.Fragment>
+          <Grid item xs={6}>
+            <Typography gutterBottom>Endereço:</Typography>
+          </Grid>
+          <Grid item xs={6}>
+            <Typography gutterBottom>
+            {address2}, {city} - {state} {zipcode} {country}
+            </Typography>
+          </Grid>
+        </React.Fragment>
+        <React.Fragment>
+          <Grid item xs={6}>
+            <Typography gutterBottom>Total:</Typography>
+          </Grid>
+          <Grid item xs={6}>
+            <Typography gutterBottom>R${price.toLocaleString('pt-br', {minimumFractionDigits: 2})}</Typography>
+          </Grid>
+        </React.Fragment>
+      </Grid>
+    </Grid>
+  );
+}
+
+export default PaymentDetails;

+ 33 - 0
frontend/src/components/CheckoutPage/ReviewOrder/ProductDetails.js

@@ -0,0 +1,33 @@
+import React from 'react';
+import { Typography, List, ListItem, ListItemText } from '@material-ui/core';
+import useStyles from './styles';
+
+const products = [
+  { name: 'Product 1', desc: 'A nice thing', price: '$9.99' },
+  { name: 'Product 2', desc: 'Another thing', price: '$3.45' },
+  { name: 'Product 3', desc: 'Something else', price: '$6.51' },
+  { name: 'Product 4', desc: 'Best thing of all', price: '$14.11' },
+  { name: 'Shipping', desc: '', price: 'Free' }
+];
+
+function ProductDetails() {
+  const classes = useStyles();
+  return (
+    <List disablePadding>
+      {products.map(product => (
+        <ListItem className={classes.listItem} key={product.name}>
+          <ListItemText primary={product.name} secondary={product.desc} />
+          <Typography variant="body2">{product.price}</Typography>
+        </ListItem>
+      ))}
+      <ListItem className={classes.listItem}>
+        <ListItemText primary="Total" />
+        <Typography variant="subtitle1" className={classes.total}>
+          $34.06
+        </Typography>
+      </ListItem>
+    </List>
+  );
+}
+
+export default ProductDetails;

+ 20 - 0
frontend/src/components/CheckoutPage/ReviewOrder/ReviewOrder.js

@@ -0,0 +1,20 @@
+import React from 'react';
+import { useFormikContext } from 'formik';
+import { Typography, Grid } from '@material-ui/core';
+import ShippingDetails from './ShippingDetails';
+import PaymentDetails from './PaymentDetails';
+import { i18n } from '../../../translate/i18n';
+
+export default function ReviewOrder() {
+  const { values: formValues } = useFormikContext();
+  return (
+    <React.Fragment>
+      <Typography variant="h6" gutterBottom>
+        {i18n.t('checkoutPage.review.title')}
+      </Typography>
+      <Grid container spacing={2}>
+        <ShippingDetails formValues={formValues} />
+      </Grid>
+    </React.Fragment>
+  );
+}

+ 26 - 0
frontend/src/components/CheckoutPage/ReviewOrder/ShippingDetails.js

@@ -0,0 +1,26 @@
+import React from 'react';
+import { Typography, Grid } from '@material-ui/core';
+import useStyles from './styles';
+import { i18n } from '../../../translate/i18n';
+
+function PaymentDetails(props) {
+  const { formValues } = props;
+  const classes = useStyles();
+  const { plan } = formValues;
+
+  const newPlan = JSON.parse(plan);
+  const { users, connections, price } = newPlan;
+  return (
+    <Grid item xs={12} sm={12}>
+      <Typography variant="h6" gutterBottom className={classes.title}>
+        {i18n.t('checkoutPage.review.details')}
+      </Typography>
+      <Typography gutterBottom>{i18n.t('checkoutPage.review.users')}: {users}</Typography>
+      <Typography gutterBottom>{i18n.t('checkoutPage.review.whatsapp')}: {connections}</Typography>
+      <Typography gutterBottom>{i18n.t('checkoutPage.review.charges')}</Typography>
+      <Typography gutterBottom>{i18n.t('checkoutPage.review.total')}: R${price.toLocaleString('pt-br', {minimumFractionDigits: 2})}</Typography>
+    </Grid>
+  );
+}
+
+export default PaymentDetails;

+ 2 - 0
frontend/src/components/CheckoutPage/ReviewOrder/index.js

@@ -0,0 +1,2 @@
+import ReviewOrder from './ReviewOrder';
+export default ReviewOrder;

+ 12 - 0
frontend/src/components/CheckoutPage/ReviewOrder/styles.js

@@ -0,0 +1,12 @@
+import { makeStyles } from '@material-ui/core/styles';
+export default makeStyles(theme => ({
+  listItem: {
+    padding: theme.spacing(1, 0)
+  },
+  total: {
+    fontWeight: '700'
+  },
+  title: {
+    marginTop: theme.spacing(2)
+  }
+}));

+ 2 - 0
frontend/src/components/CheckoutPage/index.js

@@ -0,0 +1,2 @@
+import CheckoutPage from './CheckoutPage';
+export default CheckoutPage;

+ 23 - 0
frontend/src/components/CheckoutPage/styles.js

@@ -0,0 +1,23 @@
+import { makeStyles } from '@material-ui/core/styles';
+export default makeStyles(theme => ({
+  stepper: {
+    padding: theme.spacing(3, 0, 5)
+  },
+  buttons: {
+    display: 'flex',
+    justifyContent: 'flex-end'
+  },
+  button: {
+    marginTop: theme.spacing(3),
+    marginLeft: theme.spacing(1)
+  },
+  wrapper: {
+    margin: theme.spacing(1),
+    position: 'relative'
+  },
+  buttonProgress: {
+    position: 'absolute',
+    top: '50%',
+    left: '50%'
+  }
+}));

+ 85 - 0
frontend/src/components/ColorPicker/index.js

@@ -0,0 +1,85 @@
+import { Dialog } from "@material-ui/core";
+import React, { useState } from "react";
+
+import { BlockPicker } from "react-color";
+
+const ColorPicker = ({ onChange, currentColor, handleClose, open }) => {
+	const [selectedColor, setSelectedColor] = useState(currentColor);
+
+	const handleChange = color => {
+		setSelectedColor(color.hex);
+		handleClose();
+	};
+
+	const colors = [
+		"#B80000",
+		"#DB3E00",
+		"#FCCB00",
+		"#008B02",
+		"#006B76",
+		"#1273DE",
+		"#004DCF",
+		"#5300EB",
+		"#EB9694",
+		"#FAD0C3",
+		"#FEF3BD",
+		"#C1E1C5",
+		"#BEDADC",
+		"#C4DEF6",
+		"#BED3F3",
+		"#D4C4FB",
+		"#4D4D4D",
+		"#999999",
+		"#F44E3B",
+		"#FE9200",
+		"#FCDC00",
+		"#DBDF00",
+		"#A4DD00",
+		"#68CCCA",
+		"#73D8FF",
+		"#AEA1FF",
+		"#FDA1FF",
+		"#333333",
+		"#808080",
+		"#cccccc",
+		"#D33115",
+		"#E27300",
+		"#FCC400",
+		"#B0BC00",
+		"#68BC00",
+		"#16A5A5",
+		"#009CE0",
+		"#7B64FF",
+		"#FA28FF",
+		"#666666",
+		"#B3B3B3",
+		"#9F0500",
+		"#C45100",
+		"#FB9E00",
+		"#808900",
+		"#194D33",
+		"#0C797D",
+		"#0062B1",
+		"#653294",
+		"#AB149E",
+	];
+
+	return (
+		<Dialog
+			onClose={handleClose}
+			aria-labelledby="simple-dialog-title"
+			open={open}
+		>
+			<BlockPicker
+				width={"100%"}
+				triangle="hide"
+				color={selectedColor}
+				colors={colors}
+				onChange={handleChange}
+				onChangeComplete={color => onChange(color.hex)}
+			/>
+		</Dialog>
+	);
+};
+
+export default ColorPicker;

+ 634 - 0
frontend/src/components/CompaniesManager/index.js

@@ -0,0 +1,634 @@
+import React, { useState, useEffect } from "react";
+import {
+  makeStyles,
+  Paper,
+  Grid,
+  FormControl,
+  InputLabel,
+  MenuItem,
+  TextField,
+  Table,
+  TableHead,
+  TableBody,
+  TableCell,
+  TableRow,
+  IconButton,
+  Select,
+} from "@material-ui/core";
+import { Formik, Form, Field } from "formik";
+import ButtonWithSpinner from "../ButtonWithSpinner";
+import ConfirmationModal from "../ConfirmationModal";
+
+import { Edit as EditIcon } from "@material-ui/icons";
+
+import { toast } from "react-toastify";
+import useCompanies from "../../hooks/useCompanies";
+import usePlans from "../../hooks/usePlans";
+import ModalUsers from "../ModalUsers";
+import api from "../../services/api";
+import { head, isArray, has } from "lodash";
+import { useDate } from "../../hooks/useDate";
+
+import moment from "moment";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    width: "100%",
+  },
+  mainPaper: {
+    width: "100%",
+    flex: 1,
+    padding: theme.spacing(2),
+  },
+  fullWidth: {
+    width: "100%",
+  },
+  tableContainer: {
+    width: "100%",
+    overflowX: "scroll",
+    ...theme.scrollbarStyles,
+  },
+  textfield: {
+    width: "100%",
+  },
+  textRight: {
+    textAlign: "right",
+  },
+  row: {
+    paddingTop: theme.spacing(2),
+    paddingBottom: theme.spacing(2),
+  },
+  control: {
+    paddingRight: theme.spacing(1),
+    paddingLeft: theme.spacing(1),
+  },
+  buttonContainer: {
+    textAlign: "right",
+    padding: theme.spacing(1),
+  },
+}));
+
+export function CompanyForm(props) {
+  const { onSubmit, onDelete, onCancel, initialValue, loading } = props;
+  const classes = useStyles();
+  const [plans, setPlans] = useState([]);
+  const [modalUser, setModalUser] = useState(false);
+  const [firstUser, setFirstUser] = useState({});
+
+  const [record, setRecord] = useState({
+    name: "",
+    email: "",
+    phone: "",
+    planId: "",
+    status: true,
+    campaignsEnabled: false,
+    dueDate: "",
+    recurrence: "",
+    ...initialValue,
+  });
+
+  const { list: listPlans } = usePlans();
+
+  useEffect(() => {
+    async function fetchData() {
+      const list = await listPlans();
+      setPlans(list);
+    }
+    fetchData();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  useEffect(() => {
+    setRecord((prev) => {
+      if (moment(initialValue).isValid()) {
+        initialValue.dueDate = moment(initialValue.dueDate).format(
+          "YYYY-MM-DD"
+        );
+      }
+      return {
+        ...prev,
+        ...initialValue,
+      };
+    });
+  }, [initialValue]);
+
+  const handleSubmit = async (data) => {
+    if (data.dueDate === "" || moment(data.dueDate).isValid() === false) {
+      data.dueDate = null;
+    }
+    onSubmit(data);
+    setRecord({ ...initialValue, dueDate: "" });
+  };
+
+  const handleOpenModalUsers = async () => {
+    try {
+      const { data } = await api.get("/users/list", {
+        params: {
+          companyId: initialValue.id,
+        },
+      });
+      if (isArray(data) && data.length) {
+        setFirstUser(head(data));
+      }
+      setModalUser(true);
+    } catch (e) {
+      toast.error(e);
+    }
+  };
+
+  const handleCloseModalUsers = () => {
+    setFirstUser({});
+    setModalUser(false);
+  };
+
+  const incrementDueDate = () => {
+    const data = { ...record };
+    if (data.dueDate !== "" && data.dueDate !== null) {
+      switch (data.recurrence) {
+        case "MENSAL":
+          data.dueDate = moment(data.dueDate)
+            .add(1, "month")
+            .format("YYYY-MM-DD");
+          break;
+        case "BIMESTRAL":
+          data.dueDate = moment(data.dueDate)
+            .add(2, "month")
+            .format("YYYY-MM-DD");
+          break;
+        case "TRIMESTRAL":
+          data.dueDate = moment(data.dueDate)
+            .add(3, "month")
+            .format("YYYY-MM-DD");
+          break;
+        case "SEMESTRAL":
+          data.dueDate = moment(data.dueDate)
+            .add(6, "month")
+            .format("YYYY-MM-DD");
+          break;
+        case "ANUAL":
+          data.dueDate = moment(data.dueDate)
+            .add(12, "month")
+            .format("YYYY-MM-DD");
+          break;
+        default:
+          break;
+      }
+    }
+    setRecord(data);
+  };
+
+  return (
+    <>
+      <ModalUsers
+        userId={firstUser.id}
+        companyId={initialValue.id}
+        open={modalUser}
+        onClose={handleCloseModalUsers}
+      />
+      <Formik
+        enableReinitialize
+        className={classes.fullWidth}
+        initialValues={record}
+        onSubmit={(values, { resetForm }) =>
+          setTimeout(() => {
+            handleSubmit(values);
+            resetForm();
+          }, 500)
+        }
+      >
+        {(values, setValues) => (
+          <Form className={classes.fullWidth}>
+            <Grid spacing={2} justifyContent="flex-end" container>
+              <Grid xs={12} sm={6} md={4} item>
+                <Field
+                  as={TextField}
+                  label={i18n.t("settings.company.form.name")}
+                  name="name"
+                  variant="outlined"
+                  className={classes.fullWidth}
+                  margin="dense"
+                />
+              </Grid>
+              <Grid xs={12} sm={6} md={2} item>
+                <Field
+                  as={TextField}
+                  label={i18n.t("settings.company.form.email")}
+                  name="email"
+                  variant="outlined"
+                  className={classes.fullWidth}
+                  margin="dense"
+                  required
+                />
+              </Grid>
+              <Grid xs={12} sm={6} md={2} item>
+                <Field
+                  as={TextField}
+                  label={i18n.t("settings.company.form.phone")}
+                  name="phone"
+                  variant="outlined"
+                  className={classes.fullWidth}
+                  margin="dense"
+                />
+              </Grid>
+              <Grid xs={12} sm={6} md={2} item>
+                <FormControl margin="dense" variant="outlined" fullWidth>
+                  <InputLabel htmlFor="plan-selection">
+                    {i18n.t("settings.company.form.plan")}
+                  </InputLabel>
+                  <Field
+                    as={Select}
+                    id="plan-selection"
+                    label={i18n.t("settings.company.form.plan")}
+                    labelId="plan-selection-label"
+                    name="planId"
+                    margin="dense"
+                    required
+                  >
+                    {plans.map((plan, key) => (
+                      <MenuItem key={key} value={plan.id}>
+                        {plan.name}
+                      </MenuItem>
+                    ))}
+                  </Field>
+                </FormControl>
+              </Grid>
+              <Grid xs={12} sm={6} md={2} item>
+                <FormControl margin="dense" variant="outlined" fullWidth>
+                  <InputLabel htmlFor="status-selection">
+                    {i18n.t("settings.company.form.status")}
+                  </InputLabel>
+                  <Field
+                    as={Select}
+                    id="status-selection"
+                    label={i18n.t("settings.company.form.status")}
+                    labelId="status-selection-label"
+                    name="status"
+                    margin="dense"
+                  >
+                    <MenuItem value={true}>{i18n.t("settings.company.form.yes")}</MenuItem>
+                    <MenuItem value={false}>{i18n.t("settings.company.form.no")}</MenuItem>
+                  </Field>
+                </FormControl>
+              </Grid>
+              <Grid xs={12} sm={6} md={2} item>
+                <FormControl margin="dense" variant="outlined" fullWidth>
+                  <InputLabel htmlFor="status-selection">{i18n.t("settings.company.form.campanhas")}</InputLabel>
+                  <Field
+                    as={Select}
+                    id="campaigns-selection"
+                    label={i18n.t("settings.company.form.campanhas")}
+                    labelId="campaigns-selection-label"
+                    name="campaignsEnabled"
+                    margin="dense"
+                  >
+                    <MenuItem value={true}>{i18n.t("settings.company.form.enabled")}</MenuItem>
+                    <MenuItem value={false}>{i18n.t("settings.company.form.disabled")}</MenuItem>
+                  </Field>
+                </FormControl>
+              </Grid>
+              <Grid xs={12} sm={6} md={2} item>
+                <FormControl variant="outlined" fullWidth>
+                  <Field
+                    as={TextField}
+                    label={i18n.t("settings.company.form.dueDate")}
+                    type="date"
+                    name="dueDate"
+                    InputLabelProps={{
+                      shrink: true,
+                    }}
+                    variant="outlined"
+                    fullWidth
+                    margin="dense"
+                  />
+                </FormControl>
+              </Grid>
+              <Grid xs={12} sm={6} md={2} item>
+                <FormControl margin="dense" variant="outlined" fullWidth>
+                  <InputLabel htmlFor="recorrencia-selection">
+                    {i18n.t("settings.company.form.recurrence")}
+                  </InputLabel>
+                  <Field
+                    as={Select}
+                    label={i18n.t("settings.company.form.recurrence")}
+                    labelId="recorrencia-selection-label"
+                    id="recurrence"
+                    name="recurrence"
+                    margin="dense"
+                  >
+                    <MenuItem value="MENSAL">{i18n.t("settings.company.form.monthly")}</MenuItem>
+                    {/*<MenuItem value="BIMESTRAL">Bimestral</MenuItem>*/}
+                    {/*<MenuItem value="TRIMESTRAL">Trimestral</MenuItem>*/}
+                    {/*<MenuItem value="SEMESTRAL">Semestral</MenuItem>*/}
+                    {/*<MenuItem value="ANUAL">Anual</MenuItem>*/}
+                  </Field>
+                </FormControl>
+              </Grid>
+              <Grid xs={12} item>
+                <Grid justifyContent="flex-end" spacing={1} container>
+                  <Grid xs={4} md={1} item>
+                    <ButtonWithSpinner
+                      className={classes.fullWidth}
+                      style={{ marginTop: 7 }}
+                      loading={loading}
+                      onClick={() => onCancel()}
+                      variant="contained"
+                    >
+                      {i18n.t("settings.company.buttons.clear")}
+                    </ButtonWithSpinner>
+                  </Grid>
+                  {record.id !== undefined ? (
+                    <>
+                      <Grid xs={6} md={1} item>
+                        <ButtonWithSpinner
+                          style={{ marginTop: 7 }}
+                          className={classes.fullWidth}
+                          loading={loading}
+                          onClick={() => onDelete(record)}
+                          variant="contained"
+                          color="secondary"
+                        >
+                          {i18n.t("settings.company.buttons.delete")}
+                        </ButtonWithSpinner>
+                      </Grid>
+                      <Grid xs={6} md={2} item>
+                        <ButtonWithSpinner
+                          style={{ marginTop: 7 }}
+                          className={classes.fullWidth}
+                          loading={loading}
+                          onClick={() => incrementDueDate()}
+                          variant="contained"
+                          color="primary"
+                        >
+                          {i18n.t("settings.company.buttons.expire")}
+                        </ButtonWithSpinner>
+                      </Grid>
+                      <Grid xs={6} md={1} item>
+                        <ButtonWithSpinner
+                          style={{ marginTop: 7 }}
+                          className={classes.fullWidth}
+                          loading={loading}
+                          onClick={() => handleOpenModalUsers()}
+                          variant="contained"
+                          color="primary"
+                        >
+                          {i18n.t("settings.company.buttons.user")}
+                        </ButtonWithSpinner>
+                      </Grid>
+                    </>
+                  ) : null}
+                  <Grid xs={6} md={1} item>
+                    <ButtonWithSpinner
+                      className={classes.fullWidth}
+                      style={{ marginTop: 7 }}
+                      loading={loading}
+                      type="submit"
+                      variant="contained"
+                      color="primary"
+                    >
+                      {i18n.t("settings.company.buttons.save")}
+                    </ButtonWithSpinner>
+                  </Grid>
+                </Grid>
+              </Grid>
+            </Grid>
+          </Form>
+        )}
+      </Formik>
+    </>
+  );
+}
+
+export function CompaniesManagerGrid(props) {
+  const { records, onSelect } = props;
+  const classes = useStyles();
+  const { dateToClient } = useDate();
+
+  const renderStatus = (row) => {
+    return row.status === false ? "Não" : "Sim";
+  };
+
+  const renderPlan = (row) => {
+    return row.planId !== null ? row.plan.name : "-";
+  };
+
+  const renderCampaignsStatus = (row) => {
+    if (
+      has(row, "settings") &&
+      isArray(row.settings) &&
+      row.settings.length > 0
+    ) {
+      const setting = row.settings.find((s) => s.key === "campaignsEnabled");
+      if (setting) {
+        return setting.value === "true" ? i18n.t("settings.company.form.enabled") : i18n.t("settings.company.form.disabled");
+      }
+    }
+    return i18n.t("settings.company.form.disabled")
+  };
+
+  const rowStyle = (record) => {
+    if (moment(record.dueDate).isValid()) {
+      const now = moment();
+      const dueDate = moment(record.dueDate);
+      const diff = dueDate.diff(now, "days");
+      if (diff === 5) {
+        return { backgroundColor: "#fffead" };
+      }
+      if (diff >= -3 && diff <= 4) {
+        return { backgroundColor: "#f7cc8f" };
+      }
+      if (diff === -4) {
+        return { backgroundColor: "#fa8c8c" };
+      }
+    }
+    return {};
+  };
+
+  return (
+    <Paper className={classes.tableContainer}>
+      <Table
+        className={classes.fullWidth}
+        size="small"
+        aria-label="a dense table"
+      >
+        <TableHead>
+          <TableRow>
+            <TableCell align="center" style={{ width: "1%" }}>
+              #
+            </TableCell>
+            <TableCell align="left">{i18n.t("settings.company.form.name")}</TableCell>
+            <TableCell align="left">{i18n.t("settings.company.form.email")}</TableCell>
+            <TableCell align="left">{i18n.t("settings.company.form.phone")}</TableCell>
+            <TableCell align="left">{i18n.t("settings.company.form.plan")}</TableCell>
+            <TableCell align="left">{i18n.t("settings.company.form.campanhas")}</TableCell>
+            <TableCell align="left">{i18n.t("settings.company.form.status")}</TableCell>
+            <TableCell align="left">{i18n.t("settings.company.form.createdAt")}</TableCell>
+            <TableCell align="left">{i18n.t("settings.company.form.expire")}</TableCell>
+          </TableRow>
+        </TableHead>
+        <TableBody>
+          {records.map((row, key) => (
+            <TableRow style={rowStyle(row)} key={key}>
+              <TableCell align="center" style={{ width: "1%" }}>
+                <IconButton onClick={() => onSelect(row)} aria-label="delete">
+                  <EditIcon />
+                </IconButton>
+              </TableCell>
+              <TableCell align="left">{row.name || "-"}</TableCell>
+              <TableCell align="left">{row.email || "-"}</TableCell>
+              <TableCell align="left">{row.phone || "-"}</TableCell>
+              <TableCell align="left">{renderPlan(row)}</TableCell>
+              <TableCell align="left">{renderCampaignsStatus(row)}</TableCell>
+              <TableCell align="left">{renderStatus(row)}</TableCell>
+              <TableCell align="left">{dateToClient(row.createdAt)}</TableCell>
+              <TableCell align="left">
+                {dateToClient(row.dueDate)}
+                <br />
+                <span>{row.recurrence}</span>
+              </TableCell>
+            </TableRow>
+          ))}
+        </TableBody>
+      </Table>
+    </Paper>
+  );
+}
+
+export default function CompaniesManager() {
+  const classes = useStyles();
+  const { list, save, update, remove } = useCompanies();
+
+  const [showConfirmDialog, setShowConfirmDialog] = useState(false);
+  const [loading, setLoading] = useState(false);
+  const [records, setRecords] = useState([]);
+  const [record, setRecord] = useState({
+    name: "",
+    email: "",
+    phone: "",
+    planId: "",
+    status: true,
+    campaignsEnabled: false,
+    dueDate: "",
+    recurrence: "",
+  });
+
+  useEffect(() => {
+    loadPlans();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  const loadPlans = async () => {
+    setLoading(true);
+    try {
+      const companyList = await list();
+      setRecords(companyList);
+    } catch (e) {
+      toast.error(i18n.t("settings.company.toasts.errorList"));
+    }
+    setLoading(false);
+  };
+
+  const handleSubmit = async (data) => {
+    setLoading(true);
+    try {
+      if (data.id !== 0 && data.id !== undefined) {
+        await update(data);
+      } else {
+        await save(data);
+      }
+
+      await loadPlans();
+      handleCancel();
+      toast.success(i18n.t("settings.company.toasts.success"));
+    } catch (e) {
+      toast.error(
+        i18n.t("settings.company.toasts.error")
+      );
+    }
+    setLoading(false);
+  };
+
+  const handleDelete = async () => {
+    setLoading(true);
+    try {
+      await remove(record.id);
+      await loadPlans();
+      handleCancel();
+      toast.success(i18n.t("settings.company.toasts.success"));
+    } catch (e) {
+      toast.error(i18n.t("settings.company.toasts.errorOperation"));
+    }
+    setLoading(false);
+  };
+
+  const handleOpenDeleteDialog = () => {
+    setShowConfirmDialog(true);
+  };
+
+  const handleCancel = () => {
+    setRecord((prev) => ({
+      ...prev,
+      id: undefined,
+      name: "",
+      email: "",
+      phone: "",
+      planId: "",
+      status: true,
+      campaignsEnabled: false,
+      dueDate: "",
+      recurrence: "",
+    }));
+  };
+
+  const handleSelect = (data) => {
+    let campaignsEnabled = false;
+
+    const setting = data.settings.find(
+      (s) => s.key.indexOf("campaignsEnabled") > -1
+    );
+    if (setting) {
+      campaignsEnabled =
+        setting.value === "true" || setting.value === "enabled";
+    }
+
+    setRecord((prev) => ({
+      ...prev,
+      id: data.id,
+      name: data.name || "",
+      phone: data.phone || "",
+      email: data.email || "",
+      planId: data.planId || "",
+      status: data.status === false ? false : true,
+      campaignsEnabled,
+      dueDate: data.dueDate || "",
+      recurrence: data.recurrence || "",
+    }));
+  };
+
+  return (
+    <Paper className={classes.mainPaper} elevation={0}>
+      <Grid spacing={2} container>
+        <Grid xs={12} item>
+          <CompanyForm
+            initialValue={record}
+            onDelete={handleOpenDeleteDialog}
+            onSubmit={handleSubmit}
+            onCancel={handleCancel}
+            loading={loading}
+          />
+        </Grid>
+        <Grid xs={12} item>
+          <CompaniesManagerGrid records={records} onSelect={handleSelect} />
+        </Grid>
+      </Grid>
+      <ConfirmationModal
+        title={i18n.t("settings.company.confirmModal.title")}
+        open={showConfirmDialog}
+        onClose={() => setShowConfirmDialog(false)}
+        onConfirm={() => handleDelete()}
+      >
+        {i18n.t("settings.company.confirmModal.message")}
+      </ConfirmationModal>
+    </Paper>
+  );
+}

+ 45 - 0
frontend/src/components/ConfirmationModal/index.js

@@ -0,0 +1,45 @@
+import React from "react";
+import Button from "@material-ui/core/Button";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import Typography from "@material-ui/core/Typography";
+
+import { i18n } from "../../translate/i18n";
+
+const ConfirmationModal = ({ title, children, open, onClose, onConfirm }) => {
+	return (
+		<Dialog
+			open={open}
+			onClose={() => onClose(false)}
+			aria-labelledby="confirm-dialog"
+		>
+			<DialogTitle id="confirm-dialog">{title}</DialogTitle>
+			<DialogContent dividers>
+				<Typography>{children}</Typography>
+			</DialogContent>
+			<DialogActions>
+				<Button
+					variant="contained"
+					onClick={() => onClose(false)}
+					color="default"
+				>
+					{i18n.t("confirmationModal.buttons.cancel")}
+				</Button>
+				<Button
+					variant="contained"
+					onClick={() => {
+						onClose(false);
+						onConfirm();
+					}}
+					color="secondary"
+				>
+					{i18n.t("confirmationModal.buttons.confirm")}
+				</Button>
+			</DialogActions>
+		</Dialog>
+	);
+};
+
+export default ConfirmationModal;

+ 199 - 0
frontend/src/components/ContactDrawer/index.js

@@ -0,0 +1,199 @@
+import React, { useEffect, useState } from "react";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Typography from "@material-ui/core/Typography";
+import IconButton from "@material-ui/core/IconButton";
+import CloseIcon from "@material-ui/icons/Close";
+import Drawer from "@material-ui/core/Drawer";
+import Link from "@material-ui/core/Link";
+import InputLabel from "@material-ui/core/InputLabel";
+import Avatar from "@material-ui/core/Avatar";
+import Button from "@material-ui/core/Button";
+import Paper from "@material-ui/core/Paper";
+import CreateIcon from '@material-ui/icons/Create';
+
+import { i18n } from "../../translate/i18n";
+
+import ContactDrawerSkeleton from "../ContactDrawerSkeleton";
+import MarkdownWrapper from "../MarkdownWrapper";
+import { CardHeader } from "@material-ui/core";
+import { ContactForm } from "../ContactForm";
+import ContactModal from "../ContactModal";
+import { ContactNotes } from "../ContactNotes";
+
+const drawerWidth = 320;
+
+const useStyles = makeStyles(theme => ({
+	drawer: {
+		width: drawerWidth,
+		flexShrink: 0,
+	},
+	drawerPaper: {
+		width: drawerWidth,
+		display: "flex",
+		borderTop: "1px solid rgba(0, 0, 0, 0.12)",
+		borderRight: "1px solid rgba(0, 0, 0, 0.12)",
+		borderBottom: "1px solid rgba(0, 0, 0, 0.12)",
+		borderTopRightRadius: 4,
+		borderBottomRightRadius: 4,
+	},
+	header: {
+		display: "flex",
+		borderBottom: "1px solid rgba(0, 0, 0, 0.12)",
+		backgroundColor: theme.palette.contactdrawer, //DARK MODE PLW DESIGN//
+		alignItems: "center",
+		padding: theme.spacing(0, 1),
+		minHeight: "73px",
+		justifyContent: "flex-start",
+	},
+	content: {
+		display: "flex",
+		backgroundColor: theme.palette.contactdrawer, //DARK MODE PLW DESIGN//
+		flexDirection: "column",
+		padding: "8px 0px 8px 8px",
+		height: "100%",
+		overflowY: "scroll",
+		...theme.scrollbarStyles,
+	},
+
+	contactAvatar: {
+		margin: 15,
+		width: 100,
+		height: 100,
+	},
+
+	contactHeader: {
+		display: "flex",
+		padding: 8,
+		flexDirection: "column",
+		alignItems: "center",
+		justifyContent: "center",
+		"& > *": {
+			margin: 4,
+		},
+	},
+
+	contactDetails: {
+		marginTop: 8,
+		padding: 8,
+		display: "flex",
+		flexDirection: "column",
+	},
+	contactExtraInfo: {
+		marginTop: 4,
+		padding: 6,
+	},
+}));
+
+const ContactDrawer = ({ open, handleDrawerClose, contact, ticket, loading }) => {
+	const classes = useStyles();
+
+	const [modalOpen, setModalOpen] = useState(false);
+	const [openForm, setOpenForm] = useState(false);
+
+	useEffect(() => {
+		setOpenForm(false);
+	}, [open, contact]);
+
+	return (
+		<>
+			<Drawer
+				className={classes.drawer}
+				variant="persistent"
+				anchor="right"
+				open={open}
+				PaperProps={{ style: { position: "absolute" } }}
+				BackdropProps={{ style: { position: "absolute" } }}
+				ModalProps={{
+					container: document.getElementById("drawer-container"),
+					style: { position: "absolute" },
+				}}
+				classes={{
+					paper: classes.drawerPaper,
+				}}
+			>
+				<div className={classes.header}>
+					<IconButton onClick={handleDrawerClose}>
+						<CloseIcon />
+					</IconButton>
+					<Typography style={{ justifySelf: "center" }}>
+						{i18n.t("contactDrawer.header")}
+					</Typography>
+				</div>
+				{loading ? (
+					<ContactDrawerSkeleton classes={classes} />
+				) : (
+					<div className={classes.content}>
+						<Paper square variant="outlined" className={classes.contactHeader}>
+							<CardHeader
+								onClick={() => {}}
+								style={{ cursor: "pointer", width: '100%' }}
+								titleTypographyProps={{ noWrap: true }}
+								subheaderTypographyProps={{ noWrap: true }}
+								avatar={<Avatar src={contact.profilePicUrl} alt="contact_image" style={{ width: 60, height: 60 }} />}
+								title={
+									<>
+										<Typography onClick={() => setOpenForm(true)}>
+											{contact.name}
+											<CreateIcon style={{fontSize: 16, marginLeft: 5}} />
+										</Typography>
+									</>
+								}
+								subheader={
+									<>
+										<Typography style={{fontSize: 12}}>
+											<Link href={`tel:${contact.number}`}>{contact.number}</Link>
+										</Typography>
+										<Typography style={{fontSize: 12}}>
+											<Link href={`mailto:${contact.email}`}>{contact.email}</Link>
+										</Typography>
+									</>
+								}
+							/>
+							<Button
+								variant="outlined"
+								color="primary"
+								onClick={() => setModalOpen(!openForm)}
+								style={{fontSize: 12}}
+							>
+								{i18n.t("contactDrawer.buttons.edit")}
+							</Button>
+							{(contact.id && openForm) && <ContactForm initialContact={contact} onCancel={() => setOpenForm(false)} />}
+						</Paper>
+						<Paper square variant="outlined" className={classes.contactDetails}>
+							<Typography variant="subtitle1" style={{marginBottom: 10}}>
+								{i18n.t("ticketOptionsMenu.appointmentsModal.title")}
+							</Typography>
+							<ContactNotes ticket={ticket} />
+						</Paper>
+						<Paper square variant="outlined" className={classes.contactDetails}>
+							<ContactModal
+								open={modalOpen}
+								onClose={() => setModalOpen(false)}
+								contactId={contact.id}
+							></ContactModal>
+							<Typography variant="subtitle1">
+								{i18n.t("contactDrawer.extraInfo")}
+							</Typography>
+							{contact?.extraInfo?.map(info => (
+								<Paper
+									key={info.id}
+									square
+									variant="outlined"
+									className={classes.contactExtraInfo}
+								>
+									<InputLabel>{info.name}</InputLabel>
+									<Typography component="div" noWrap style={{ paddingTop: 2 }}>
+										<MarkdownWrapper>{info.value}</MarkdownWrapper>
+									</Typography>
+								</Paper>
+							))}
+						</Paper>
+					</div>
+				)}
+			</Drawer>
+		</>
+	);
+};
+
+export default ContactDrawer;

+ 50 - 0
frontend/src/components/ContactDrawerSkeleton/index.js

@@ -0,0 +1,50 @@
+import React from "react";
+import Skeleton from "@material-ui/lab/Skeleton";
+import Typography from "@material-ui/core/Typography";
+import Paper from "@material-ui/core/Paper";
+import { i18n } from "../../translate/i18n";
+import { Grid } from "@material-ui/core";
+
+const ContactDrawerSkeleton = ({ classes }) => {
+	return (
+		<div className={classes.content}>
+			<Paper square variant="outlined" className={classes.contactHeader}>
+				<Grid container>
+					<Grid item>
+						<Skeleton
+							animation="wave"
+							variant="circle"
+							width={60}
+							height={60}
+							className={classes.contactAvatar}
+						/>
+					</Grid>
+					<Grid item>
+						<Skeleton animation="wave" height={25} width={90} />
+						<Skeleton animation="wave" height={25} width={80} />
+						<Skeleton animation="wave" height={25} width={80} />
+					</Grid>
+				</Grid>
+			</Paper>
+			<Paper square className={classes.contactDetails}>
+				<Typography variant="subtitle1">
+					{i18n.t("contactDrawer.extraInfo")}
+				</Typography>
+				<Paper square variant="outlined" className={classes.contactExtraInfo}>
+					<Skeleton animation="wave" height={20} width={60} />
+					<Skeleton animation="wave" height={20} width={160} />
+				</Paper>
+				<Paper square variant="outlined" className={classes.contactExtraInfo}>
+					<Skeleton animation="wave" height={20} width={60} />
+					<Skeleton animation="wave" height={20} width={160} />
+				</Paper>
+				<Paper square variant="outlined" className={classes.contactExtraInfo}>
+					<Skeleton animation="wave" height={20} width={60} />
+					<Skeleton animation="wave" height={20} width={160} />
+				</Paper>
+			</Paper>
+		</div>
+	);
+};
+
+export default ContactDrawerSkeleton;

+ 187 - 0
frontend/src/components/ContactForm/index.js

@@ -0,0 +1,187 @@
+import React, { useState, useEffect } from "react";
+
+import * as Yup from "yup";
+import { Formik, Form, Field } from "formik";
+import { toast } from "react-toastify";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import CircularProgress from "@material-ui/core/CircularProgress";
+
+import { i18n } from "../../translate/i18n";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+import { Grid } from "@material-ui/core";
+
+const useStyles = makeStyles(theme => ({
+	root: {
+		display: "flex",
+		flexWrap: "wrap",
+	},
+	textField: {
+		marginRight: theme.spacing(1),
+		flex: 1,
+	},
+
+	extraAttr: {
+		display: "flex",
+		justifyContent: "center",
+		alignItems: "center",
+	},
+
+	btnWrapper: {
+		position: "relative",
+	},
+
+	buttonProgress: {
+		color: green[500],
+		position: "absolute",
+		top: "50%",
+		left: "50%",
+		marginTop: -12,
+		marginLeft: -12,
+	},
+
+    textCenter: {
+        backgroundColor: 'red'
+    }
+}));
+
+const ContactSchema = Yup.object().shape({
+	name: Yup.string()
+		.min(2, "Too Short!")
+		.max(50, "Too Long!")
+		.required("Required"),
+	number: Yup.string().min(8, "Too Short!").max(50, "Too Long!"),
+	email: Yup.string().email("Invalid email"),
+});
+
+export function ContactForm ({ initialContact, onSave, onCancel }) {
+	const classes = useStyles();
+
+	const [contact, setContact] = useState(initialContact);
+
+    useEffect(() => {
+        setContact(initialContact);
+    }, [initialContact]);
+
+	const handleSaveContact = async values => {
+		try {
+			if (contact.id) {
+				await api.put(`/contacts/${contact.id}`, values);
+			} else {
+				const { data } = await api.post("/contacts", values);
+				if (onSave) {
+					onSave(data);
+				}
+			}
+			toast.success(i18n.t("contactModal.success"));
+		} catch (err) {
+			toastError(err);
+		}
+	};
+
+    return (
+        <Formik
+            initialValues={contact}
+            enableReinitialize={true}
+            validationSchema={ContactSchema}
+            onSubmit={(values, actions) => {
+                setTimeout(() => {
+                    handleSaveContact(values);
+                    actions.setSubmitting(false);
+                }, 400);
+            }}
+        >
+            {({ values, errors, touched, isSubmitting }) => (
+                <Form>
+                    <Grid container spacing={1}>
+                        {/* <Grid item xs={12}>
+                            <Typography variant="subtitle1" gutterBottom>
+                                {i18n.t("contactModal.form.mainInfo")}
+                            </Typography>
+                        </Grid> */}
+                        <Grid item xs={12}>
+                            <Field
+                                as={TextField}
+                                label={i18n.t("contactModal.form.name")}
+                                name="name"
+                                autoFocus
+                                error={touched.name && Boolean(errors.name)}
+                                helperText={touched.name && errors.name}
+                                variant="outlined"
+                                margin="dense"
+                                className={classes.textField}
+                                fullWidth
+                            />
+                        </Grid>
+                        <Grid item xs={12}>
+                            <Field
+                                as={TextField}
+                                label={i18n.t("contactModal.form.number")}
+                                name="number"
+                                error={touched.number && Boolean(errors.number)}
+                                helperText={touched.number && errors.number}
+                                placeholder="5513912344321"
+                                variant="outlined"
+                                margin="dense"
+                                fullWidth
+                            />
+                        </Grid>
+                        <Grid item xs={12}>
+                            <Field
+                                as={TextField}
+                                label={i18n.t("contactModal.form.email")}
+                                name="email"
+                                error={touched.email && Boolean(errors.email)}
+                                helperText={touched.email && errors.email}
+                                placeholder="Email address"
+                                fullWidth
+                                margin="dense"
+                                variant="outlined"
+                            />
+                        </Grid>
+                        <Grid item xs={12} spacing={1}>
+                            <Grid container spacing={1}>
+                                <Grid xs={6} item>
+                                    <Button
+                                        onClick={onCancel}
+                                        color="secondary"
+                                        disabled={isSubmitting}
+                                        variant="outlined"
+                                        fullWidth
+                                    >
+                                        {i18n.t("contactModal.buttons.cancel")}
+                                    </Button>
+                                </Grid>
+                                <Grid classes={classes.textCenter} xs={6} item>
+                                    <Button
+                                        type="submit"
+                                        color="primary"
+                                        disabled={isSubmitting}
+                                        variant="contained"
+                                        className={classes.btnWrapper}
+                                        fullWidth
+                                    >
+                                        {contact.id
+                                            ? `${i18n.t("contactModal.buttons.okEdit")}`
+                                            : `${i18n.t("contactModal.buttons.okAdd")}`}
+                                        {isSubmitting && (
+                                            <CircularProgress
+                                                size={24}
+                                                className={classes.buttonProgress}
+                                            />
+                                        )}
+                                    </Button>
+                                </Grid>
+                            </Grid>
+                        </Grid>
+                    </Grid>
+                </Form>
+            )}
+        </Formik>
+    )
+}

+ 181 - 0
frontend/src/components/ContactListDialog/index.js

@@ -0,0 +1,181 @@
+import React, { useState, useEffect } from "react";
+
+import * as Yup from "yup";
+import { Formik, Form, Field } from "formik";
+import { toast } from "react-toastify";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import CircularProgress from "@material-ui/core/CircularProgress";
+
+import { i18n } from "../../translate/i18n";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    display: "flex",
+    flexWrap: "wrap",
+  },
+  multFieldLine: {
+    display: "flex",
+    "& > *:not(:last-child)": {
+      marginRight: theme.spacing(1),
+    },
+  },
+
+  btnWrapper: {
+    position: "relative",
+  },
+
+  buttonProgress: {
+    color: green[500],
+    position: "absolute",
+    top: "50%",
+    left: "50%",
+    marginTop: -12,
+    marginLeft: -12,
+  },
+  formControl: {
+    margin: theme.spacing(1),
+    minWidth: 120,
+  },
+}));
+
+const ContactListSchema = Yup.object().shape({
+  name: Yup.string()
+    .min(2, i18n.t("contactLists.dialog.nameShort"))
+    .max(50, i18n.t("contactLists.dialog.nameLong"))
+    .required(i18n.t("contactLists.dialog.nameRequired")),
+});
+
+const ContactListModal = ({ open, onClose, contactListId }) => {
+  const classes = useStyles();
+
+  const initialState = {
+    name: "",
+  };
+
+  const [contactList, setContactList] = useState(initialState);
+
+  useEffect(() => {
+    const fetchContactList = async () => {
+      if (!contactListId) return;
+      try {
+        const { data } = await api.get(`/contact-lists/${contactListId}`);
+        setContactList((prevState) => {
+          return { ...prevState, ...data };
+        });
+      } catch (err) {
+        toastError(err);
+      }
+    };
+
+    fetchContactList();
+  }, [contactListId, open]);
+
+  const handleClose = () => {
+    onClose();
+    setContactList(initialState);
+  };
+
+  const handleSaveContactList = async (values) => {
+    const contactListData = { ...values };
+    try {
+      if (contactListId) {
+        await api.put(`/contact-lists/${contactListId}`, contactListData);
+      } else {
+        await api.post("/contact-lists", contactListData);
+      }
+      toast.success(i18n.t("contactList.toasts.success"));
+    } catch (err) {
+      toastError(err);
+    }
+    handleClose();
+  };
+
+  return (
+    <div className={classes.root}>
+      <Dialog
+        open={open}
+        onClose={handleClose}
+        maxWidth="xs"
+        fullWidth
+        scroll="paper"
+      >
+        <DialogTitle id="form-dialog-title">
+          {contactListId
+            ? `${i18n.t("contactLists.dialog.edit")}`
+            : `${i18n.t("contactLists.dialog.add")}`}
+        </DialogTitle>
+        <Formik
+          initialValues={contactList}
+          enableReinitialize={true}
+          validationSchema={ContactListSchema}
+          onSubmit={(values, actions) => {
+            setTimeout(() => {
+              handleSaveContactList(values);
+              actions.setSubmitting(false);
+            }, 400);
+          }}
+        >
+          {({ touched, errors, isSubmitting }) => (
+            <Form>
+              <DialogContent dividers>
+                <div className={classes.multFieldLine}>
+                  <Field
+                    as={TextField}
+                    label={i18n.t("contactLists.dialog.name")}
+                    autoFocus
+                    name="name"
+                    error={touched.name && Boolean(errors.name)}
+                    helperText={touched.name && errors.name}
+                    variant="outlined"
+                    margin="dense"
+                    fullWidth
+                  />
+                </div>
+              </DialogContent>
+              <DialogActions>
+                <Button
+                  onClick={handleClose}
+                  color="secondary"
+                  disabled={isSubmitting}
+                  variant="outlined"
+                >
+                  {i18n.t("contactLists.dialog.cancel")}
+                </Button>
+                <Button
+                  type="submit"
+                  color="primary"
+                  disabled={isSubmitting}
+                  variant="contained"
+                  className={classes.btnWrapper}
+                >
+                  {contactListId
+                    ? `${i18n.t("contactLists.dialog.okEdit")}`
+                    : `${i18n.t("contactLists.dialog.okAdd")}`}
+                  {isSubmitting && (
+                    <CircularProgress
+                      size={24}
+                      className={classes.buttonProgress}
+                    />
+                  )}
+                </Button>
+              </DialogActions>
+            </Form>
+          )}
+        </Formik>
+      </Dialog>
+    </div>
+  );
+};
+
+export default ContactListModal;

+ 242 - 0
frontend/src/components/ContactListItemModal/index.js

@@ -0,0 +1,242 @@
+import React, { useState, useEffect, useRef, useContext } from "react";
+
+import * as Yup from "yup";
+import { Formik, Form, Field } from "formik";
+import { toast } from "react-toastify";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import Typography from "@material-ui/core/Typography";
+import CircularProgress from "@material-ui/core/CircularProgress";
+
+import { i18n } from "../../translate/i18n";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+import { useParams } from "react-router-dom";
+import { AuthContext } from "../../context/Auth/AuthContext";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    display: "flex",
+    flexWrap: "wrap",
+  },
+  textField: {
+    marginRight: theme.spacing(1),
+    flex: 1,
+  },
+
+  extraAttr: {
+    display: "flex",
+    justifyContent: "center",
+    alignItems: "center",
+  },
+
+  btnWrapper: {
+    position: "relative",
+  },
+
+  buttonProgress: {
+    color: green[500],
+    position: "absolute",
+    top: "50%",
+    left: "50%",
+    marginTop: -12,
+    marginLeft: -12,
+  },
+}));
+
+const ContactSchema = Yup.object().shape({
+  name: Yup.string()
+    .min(2, i18n.t("contactListItems.dialog.nameShort"))
+    .max(50, i18n.t("contactListItems.dialog.nameLong"))
+    .required(i18n.t("contactListItems.dialog.nameRequired")),
+  number: Yup.string().min(8, i18n.t("contactListItems.dialog.numberShort")).max(50, i18n.t("contactListItems.dialog.numberLong")),
+  email: Yup.string().email(i18n.t("contactListItems.dialog.emailInvalid")),
+});
+
+const ContactListItemModal = ({
+  open,
+  onClose,
+  contactId,
+  initialValues,
+  onSave,
+}) => {
+  const classes = useStyles();
+  const isMounted = useRef(true);
+
+  const {
+    user: { companyId },
+  } = useContext(AuthContext);
+  const { contactListId } = useParams();
+
+  const initialState = {
+    name: "",
+    number: "",
+    email: "",
+  };
+
+  const [contact, setContact] = useState(initialState);
+
+  useEffect(() => {
+    return () => {
+      isMounted.current = false;
+    };
+  }, []);
+
+  useEffect(() => {
+    const fetchContact = async () => {
+      if (initialValues) {
+        setContact((prevState) => {
+          return { ...prevState, ...initialValues };
+        });
+      }
+
+      if (!contactId) return;
+
+      try {
+        const { data } = await api.get(`/contact-list-items/${contactId}`);
+        if (isMounted.current) {
+          setContact(data);
+        }
+      } catch (err) {
+        toastError(err);
+      }
+    };
+
+    fetchContact();
+  }, [contactId, open, initialValues]);
+
+  const handleClose = () => {
+    onClose();
+    setContact(initialState);
+  };
+
+  const handleSaveContact = async (values) => {
+    try {
+      if (contactId) {
+        await api.put(`/contact-list-items/${contactId}`, {
+          ...values,
+          companyId,
+          contactListId,
+        });
+        handleClose();
+      } else {
+        const { data } = await api.post("/contact-list-items", {
+          ...values,
+          companyId,
+          contactListId,
+        });
+        if (onSave) {
+          onSave(data);
+        }
+        handleClose();
+      }
+      toast.success(i18n.t("contactModal.success"));
+    } catch (err) {
+      toastError(err);
+    }
+  };
+
+  return (
+    <div className={classes.root}>
+      <Dialog open={open} onClose={handleClose} maxWidth="lg" scroll="paper">
+        <DialogTitle id="form-dialog-title">
+          {contactId
+            ? `${i18n.t("contactModal.title.edit")}`
+            : `${i18n.t("contactModal.title.add")}`}
+        </DialogTitle>
+        <Formik
+          initialValues={contact}
+          enableReinitialize={true}
+          validationSchema={ContactSchema}
+          onSubmit={(values, actions) => {
+            setTimeout(() => {
+              handleSaveContact(values);
+              actions.setSubmitting(false);
+            }, 400);
+          }}
+        >
+          {({ values, errors, touched, isSubmitting }) => (
+            <Form>
+              <DialogContent dividers>
+                <Typography variant="subtitle1" gutterBottom>
+                  {i18n.t("contactModal.form.mainInfo")}
+                </Typography>
+                <Field
+                  as={TextField}
+                  label={i18n.t("contactModal.form.name")}
+                  name="name"
+                  autoFocus
+                  error={touched.name && Boolean(errors.name)}
+                  helperText={touched.name && errors.name}
+                  variant="outlined"
+                  margin="dense"
+                  className={classes.textField}
+                />
+                <Field
+                  as={TextField}
+                  label={i18n.t("contactModal.form.number")}
+                  name="number"
+                  error={touched.number && Boolean(errors.number)}
+                  helperText={touched.number && errors.number}
+                  placeholder="5513912344321"
+                  variant="outlined"
+                  margin="dense"
+                />
+                <div>
+                  <Field
+                    as={TextField}
+                    label={i18n.t("contactModal.form.email")}
+                    name="email"
+                    error={touched.email && Boolean(errors.email)}
+                    helperText={touched.email && errors.email}
+                    placeholder={i18n.t("contactModal.form.email")}
+                    fullWidth
+                    margin="dense"
+                    variant="outlined"
+                  />
+                </div>
+              </DialogContent>
+              <DialogActions>
+                <Button
+                  onClick={handleClose}
+                  color="secondary"
+                  disabled={isSubmitting}
+                  variant="outlined"
+                >
+                  {i18n.t("contactModal.buttons.cancel")}
+                </Button>
+                <Button
+                  type="submit"
+                  color="primary"
+                  disabled={isSubmitting}
+                  variant="contained"
+                  className={classes.btnWrapper}
+                >
+                  {contactId
+                    ? `${i18n.t("contactModal.buttons.okEdit")}`
+                    : `${i18n.t("contactModal.buttons.okAdd")}`}
+                  {isSubmitting && (
+                    <CircularProgress
+                      size={24}
+                      className={classes.buttonProgress}
+                    />
+                  )}
+                </Button>
+              </DialogActions>
+            </Form>
+          )}
+        </Formik>
+      </Dialog>
+    </div>
+  );
+};
+
+export default ContactListItemModal;

+ 103 - 0
frontend/src/components/ContactListTable/index.js

@@ -0,0 +1,103 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import {
+  Table,
+  TableHead,
+  TableBody,
+  TableCell,
+  TableRow,
+  IconButton,
+} from "@material-ui/core";
+import {
+  Edit as EditIcon,
+  DeleteOutline as DeleteOutlineIcon,
+  People as PeopleIcon,
+} from "@material-ui/icons";
+
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+
+function ContactListsTable(props) {
+  const {
+    contactLists,
+    showLoading,
+    editContactList,
+    deleteContactList,
+    readOnly,
+  } = props;
+  const [loading, setLoading] = useState(true);
+  const [rows, setRows] = useState([]);
+
+  useEffect(() => {
+    if (Array.isArray(contactLists)) {
+      setRows(contactLists);
+    }
+    if (showLoading !== undefined) {
+      setLoading(showLoading);
+    }
+  }, [contactLists, showLoading]);
+
+  const handleEdit = (contactList) => {
+    editContactList(contactList);
+  };
+
+  const handleDelete = (contactList) => {
+    deleteContactList(contactList);
+  };
+
+  const renderRows = () => {
+    return rows.map((contactList) => {
+      return (
+        <TableRow key={contactList.id}>
+          <TableCell align="left">{contactList.name}</TableCell>
+          <TableCell align="center"></TableCell>
+          {!readOnly ? (
+            <TableCell align="center">
+              <IconButton size="small">
+                <PeopleIcon />
+              </IconButton>
+
+              <IconButton size="small" onClick={() => handleEdit(contactList)}>
+                <EditIcon />
+              </IconButton>
+
+              <IconButton
+                size="small"
+                onClick={() => handleDelete(contactList)}
+              >
+                <DeleteOutlineIcon />
+              </IconButton>
+            </TableCell>
+          ) : null}
+        </TableRow>
+      );
+    });
+  };
+
+  return (
+    <Table size="small">
+      <TableHead>
+        <TableRow>
+          <TableCell align="left">Nome</TableCell>
+          <TableCell align="center">Contatos</TableCell>
+          {!readOnly ? <TableCell align="center">Ações</TableCell> : null}
+        </TableRow>
+      </TableHead>
+      <TableBody>
+        {loading ? (
+          <TableRowSkeleton columns={readOnly ? 2 : 3} />
+        ) : (
+          renderRows()
+        )}
+      </TableBody>
+    </Table>
+  );
+}
+
+ContactListsTable.propTypes = {
+  contactLists: PropTypes.array.isRequired,
+  showLoading: PropTypes.bool,
+  editContactList: PropTypes.func.isRequired,
+  deleteContactList: PropTypes.func.isRequired,
+};
+
+export default ContactListsTable;

+ 300 - 0
frontend/src/components/ContactModal/index.js

@@ -0,0 +1,300 @@
+import React, { useState, useEffect, useRef } from "react";
+
+import * as Yup from "yup";
+import { Formik, FieldArray, Form, Field } from "formik";
+import { toast } from "react-toastify";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import Typography from "@material-ui/core/Typography";
+import IconButton from "@material-ui/core/IconButton";
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import CircularProgress from "@material-ui/core/CircularProgress";
+
+import { i18n } from "../../translate/i18n";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+
+import InputMask from 'react-input-mask';
+
+const useStyles = makeStyles(theme => ({
+	root: {
+		display: "flex",
+		flexWrap: "wrap",
+	},
+	textField: {
+		marginRight: theme.spacing(1),
+		flex: 1,
+	},
+
+	extraAttr: {
+		display: "flex",
+		justifyContent: "center",
+		alignItems: "center",
+	},
+
+	btnWrapper: {
+		position: "relative",
+	},
+
+	buttonProgress: {
+		color: green[500],
+		position: "absolute",
+		top: "50%",
+		left: "50%",
+		marginTop: -12,
+		marginLeft: -12,
+	},
+}));
+
+const MaskedTextField = ({ field, form, ...props }) => {
+	return (
+	  <InputMask {...field} {...props}>
+		{(inputProps) => <TextField {...inputProps} />}
+	  </InputMask>
+	);
+};
+
+const ContactSchema = Yup.object().shape({
+	name: Yup.string()
+		.min(2, i18n.t("contactModal.formErrors.name.short"))
+		.max(50, i18n.t("contactModal.formErrors.name.long"))
+		.required(i18n.t("contactModal.formErrors.name.required")),
+	number: Yup.string().min(8, 
+		i18n.t("contactModal.formErrors.phone.short")).max(50, 
+		i18n.t("contactModal.formErrors.phone.long")),
+	email: Yup.string().email(i18n.t("contactModal.formErrors.email.invalid")),
+});
+
+const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
+	const classes = useStyles();
+	const isMounted = useRef(true);
+
+	const initialState = {
+		name: "",
+		number: "",
+		email: "",
+	};
+
+	const [contact, setContact] = useState(initialState);
+
+	useEffect(() => {
+		return () => {
+			isMounted.current = false;
+		};
+	}, []);
+
+	useEffect(() => {
+		const fetchContact = async () => {
+			if (initialValues) {
+				setContact(prevState => {
+					return { ...prevState, ...initialValues };
+				});
+			}
+
+			if (!contactId) return;
+
+			try {
+				const { data } = await api.get(`/contacts/${contactId}`);
+				if (isMounted.current) {
+					console.log(data)
+					setContact({
+						...data,
+						number: data.number,
+					});
+				}
+			} catch (err) {
+				toastError(err);
+			}
+		};
+
+		fetchContact();
+	}, [contactId, open, initialValues]);
+
+	const handleClose = () => {
+		onClose();
+		setContact(initialState);
+	};
+
+	const handleSaveContact = async values => {
+		try {
+			if (contactId) {
+				await api.put(`/contacts/${contactId}`, values);
+				handleClose();
+			} else {
+				const { data } = await api.post("/contacts", values);
+				if (onSave) {
+					onSave(data);
+				}
+				handleClose();
+			}
+			toast.success(i18n.t("contactModal.success"));
+		} catch (e) {	
+			toastError(e);
+		}
+	};
+
+	return (
+		<div className={classes.root}>
+			<Dialog open={open} onClose={handleClose} maxWidth="lg" scroll="paper">
+				<DialogTitle id="form-dialog-title">
+					{contactId
+						? `${i18n.t("contactModal.title.edit")}`
+						: `${i18n.t("contactModal.title.add")}`}
+				</DialogTitle>
+				<Formik
+					initialValues={contact}
+					enableReinitialize={true}
+					validationSchema={ContactSchema}
+					onSubmit={(values, actions) => {
+						setTimeout(() => {
+							handleSaveContact(values);
+							actions.setSubmitting(false);
+						}, 400);
+					}}
+				>
+					{({ values, errors, touched, isSubmitting }) => (
+						<Form>
+							<DialogContent dividers>
+								<Typography variant="subtitle1" gutterBottom>
+									{i18n.t("contactModal.form.mainInfo")}
+								</Typography>
+								<Field
+									as={TextField}
+									label={i18n.t("contactModal.form.name")}
+									name="name"
+									autoFocus
+									error={touched.name && Boolean(errors.name)}
+									helperText={touched.name && errors.name}
+									variant="outlined"
+									margin="dense"
+									className={classes.textField}
+								/>
+								<Field
+									as={TextField}
+									name="number"
+									label={i18n.t("contactModal.form.number")}
+									error={touched.number && Boolean(errors.number)}
+									helperText={touched.number && errors.number}
+									placeholder=""
+									variant="outlined"
+									margin="dense"
+								/>
+
+								<div>
+									<Field
+										as={TextField}
+										label={i18n.t("contactModal.form.email")}
+										name="email"
+										error={touched.email && Boolean(errors.email)}
+										helperText={touched.email && errors.email}
+										placeholder="Email address"
+										fullWidth
+										margin="dense"
+										variant="outlined"
+									/>
+								</div>
+								<Typography
+									style={{ marginBottom: 8, marginTop: 12 }}
+									variant="subtitle1"
+								>
+									{i18n.t("contactModal.form.whatsapp")} {contact?.whatsapp ? contact?.whatsapp.name : ""}
+								</Typography>
+								<Typography
+									style={{ marginBottom: 8, marginTop: 12 }}
+									variant="subtitle1"
+								>
+									{i18n.t("contactModal.form.extraInfo")}
+								</Typography>
+
+								<FieldArray name="extraInfo">
+									{({ push, remove }) => (
+										<>
+											{values.extraInfo &&
+												values.extraInfo.length > 0 &&
+												values.extraInfo.map((info, index) => (
+													<div
+														className={classes.extraAttr}
+														key={`${index}-info`}
+													>
+														<Field
+															as={TextField}
+															label={i18n.t("contactModal.form.extraName")}
+															name={`extraInfo[${index}].name`}
+															variant="outlined"
+															margin="dense"
+															className={classes.textField}
+														/>
+														<Field
+															as={TextField}
+															label={i18n.t("contactModal.form.extraValue")}
+															name={`extraInfo[${index}].value`}
+															variant="outlined"
+															margin="dense"
+															className={classes.textField}
+														/>
+														<IconButton
+															size="small"
+															onClick={() => remove(index)}
+														>
+															<DeleteOutlineIcon />
+														</IconButton>
+													</div>
+												))}
+											<div className={classes.extraAttr}>
+												<Button
+													style={{ flex: 1, marginTop: 8 }}
+													variant="outlined"
+													color="primary"
+													onClick={() => push({ name: "", value: "" })}
+												>
+													{`+ ${i18n.t("contactModal.buttons.addExtraInfo")}`}
+												</Button>
+											</div>
+										</>
+									)}
+								</FieldArray>
+							</DialogContent>
+							<DialogActions>
+								<Button
+									onClick={handleClose}
+									color="secondary"
+									disabled={isSubmitting}
+									variant="outlined"
+								>
+									{i18n.t("contactModal.buttons.cancel")}
+								</Button>
+								<Button
+									type="submit"
+									color="primary"
+									disabled={isSubmitting}
+									variant="contained"
+									className={classes.btnWrapper}
+								>
+									{contactId
+										? `${i18n.t("contactModal.buttons.okEdit")}`
+										: `${i18n.t("contactModal.buttons.okAdd")}`}
+									{isSubmitting && (
+										<CircularProgress
+											size={24}
+											className={classes.buttonProgress}
+										/>
+									)}
+								</Button>
+							</DialogActions>
+						</Form>
+					)}
+				</Formik>
+			</Dialog>
+		</div>
+	);
+};
+
+export default ContactModal;

+ 204 - 0
frontend/src/components/ContactNotes/index.js

@@ -0,0 +1,204 @@
+import React, { useState, useEffect } from 'react';
+import Button from '@material-ui/core/Button';
+import TextField from '@material-ui/core/TextField';
+import List from '@material-ui/core/List';
+import { makeStyles } from '@material-ui/core/styles';
+import * as Yup from "yup";
+import { Formik, Form, Field } from "formik";
+
+import ContactNotesDialogListItem from '../ContactNotesDialogListItem';
+import ConfirmationModal from '../ConfirmationModal';
+
+import { toast } from "react-toastify";
+
+import { i18n } from "../../translate/i18n";
+
+import ButtonWithSpinner from '../ButtonWithSpinner';
+
+import useTicketNotes from '../../hooks/useTicketNotes';
+import { Grid } from '@material-ui/core';
+
+const useStyles = makeStyles((theme) => ({
+    root: {
+        '& .MuiTextField-root': {
+            margin: theme.spacing(1),
+            width: '350px',
+        },
+    },
+    list: {
+        width: '100%',
+        maxWidth: '350px',
+        maxHeight: '200px',
+        backgroundColor: theme.palette.background.paper,
+        overflow: 'auto'
+    },
+    inline: {
+        width: '100%'
+    }
+}));
+
+const NoteSchema = Yup.object().shape({
+	note: Yup.string()
+		.min(2, "Too Short!")
+		.required("Required")
+});
+export function ContactNotes ({ ticket }) {
+    const { id: ticketId, contactId } = ticket
+    const classes = useStyles()
+    const [newNote, setNewNote] = useState({ note: "" });
+    const [loading, setLoading] = useState(false)
+    const [showOnDeleteDialog, setShowOnDeleteDialog] = useState(false)
+    const [selectedNote, setSelectedNote] = useState({})
+    const [notes, setNotes] = useState([])
+    const { saveNote, deleteNote, listNotes } = useTicketNotes()
+
+    useEffect(() => {
+        async function openAndFetchData () {
+            handleResetState()
+            await loadNotes()
+        }
+        openAndFetchData()
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [])
+
+    const handleResetState = () => {
+        setNewNote({ note: "" })
+        setLoading(false)
+    }
+
+    const handleChangeComment = (e) => {
+        setNewNote({ note: e.target.value })
+    }
+
+    const handleSave = async values => {
+        setLoading(true)
+        try {
+            await saveNote({
+                ...values,
+                ticketId,
+                contactId
+            })
+            await loadNotes()
+            setNewNote({ note: '' })
+            toast.success('Observação adicionada com sucesso!')
+        } catch (e) {
+            toast.error(e)
+        }
+        setLoading(false)
+    }
+
+    const handleOpenDialogDelete = (item) => {
+        setSelectedNote(item)
+        setShowOnDeleteDialog(true)
+    }
+
+    const handleDelete = async () => {
+        setLoading(true)
+        try {
+            await deleteNote(selectedNote.id)
+            await loadNotes()
+            setSelectedNote({})
+            toast.success('Observação excluída com sucesso!')
+        } catch (e) {
+            toast.error(e)
+        }
+        setLoading(false)
+    }
+
+    const loadNotes = async () => {
+        setLoading(true)
+        try {
+            const notes = await listNotes({ ticketId, contactId })
+            setNotes(notes)
+        } catch (e) {
+            toast.error(e)
+        }
+        setLoading(false)
+    }
+
+    const renderNoteList = () => {
+        return notes.map((note) => {
+            return <ContactNotesDialogListItem
+                note={note}
+                key={note.id}
+                deleteItem={handleOpenDialogDelete}
+            />
+        })
+    }
+
+    return (
+        <>
+            <ConfirmationModal
+                title="Excluir Registro"
+                open={showOnDeleteDialog}
+                onClose={setShowOnDeleteDialog}
+                onConfirm={handleDelete}
+            >
+                Deseja realmente excluir este registro?
+            </ConfirmationModal>
+            <Formik
+                initialValues={newNote}
+                enableReinitialize={true}
+                validationSchema={NoteSchema}
+                onSubmit={(values, actions) => {
+                    setTimeout(() => {
+                        handleSave(values);
+                        actions.setSubmitting(false);
+                    }, 400);
+                }}
+            >
+
+                {({ touched, errors, setErrors }) => (
+                    <Form>
+                        <Grid container spacing={2}>
+                            <Grid xs={12} item>
+                                <Field
+                                    as={TextField}
+                                    name="note"
+                                    rows={3}
+                                    label={i18n.t("ticketOptionsMenu.appointmentsModal.textarea")}
+                                    placeholder={i18n.t("ticketOptionsMenu.appointmentsModal.placeholder")}
+                                    multiline={true}
+                                    error={touched.note && Boolean(errors.note)}
+                                    helperText={touched.note && errors.note}
+                                    variant="outlined"
+                                    onChange={handleChangeComment}
+                                    fullWidth
+                                />
+                            </Grid>
+                            { notes.length > 0 && (
+                                <Grid xs={12} item>
+                                    <List className={classes.list}>
+                                        { renderNoteList() }
+                                    </List>
+                                </Grid>
+                            ) }
+                            <Grid xs={12} item>
+                                <Grid container spacing={2}>
+                                    <Grid xs={6} item>
+                                        <Button
+                                            onClick={() => {
+                                                setNewNote("");
+                                                setErrors({});
+                                            }}
+                                            color="primary"
+                                            variant="outlined"
+                                            fullWidth
+                                        >
+                                            Cancelar
+                                        </Button>
+                                    </Grid>
+                                    <Grid xs={6} item>
+                                        <ButtonWithSpinner loading={loading} color="primary" type="submit" variant="contained" autoFocus fullWidth>
+                                            Salvar
+                                        </ButtonWithSpinner>
+                                    </Grid>
+                                </Grid>
+                            </Grid>
+                        </Grid>
+                    </Form>
+                )}
+            </Formik>
+        </>
+    );
+}

+ 206 - 0
frontend/src/components/ContactNotesDialog/index.js

@@ -0,0 +1,206 @@
+import React, { useState, useEffect } from 'react';
+import Button from '@material-ui/core/Button';
+import Dialog from '@material-ui/core/Dialog';
+import DialogActions from '@material-ui/core/DialogActions';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import TextField from '@material-ui/core/TextField';
+import List from '@material-ui/core/List';
+import { makeStyles } from '@material-ui/core/styles';
+import * as Yup from "yup";
+import { Formik, Form, Field } from "formik";
+
+import ContactNotesDialogListItem from '../ContactNotesDialogListItem';
+import ConfirmationModal from '../ConfirmationModal';
+
+import { toast } from "react-toastify";
+
+import { i18n } from "../../translate/i18n";
+
+import ButtonWithSpinner from '../ButtonWithSpinner';
+
+import useTicketNotes from '../../hooks/useTicketNotes';
+
+const useStyles = makeStyles((theme) => ({
+    root: {
+        '& .MuiTextField-root': {
+            margin: theme.spacing(1),
+            width: '350px',
+        },
+    },
+    list: {
+        width: '100%',
+        maxWidth: '350px',
+        maxHeight: '200px',
+        backgroundColor: theme.palette.background.paper,
+    },
+    inline: {
+        width: '100%'
+    }
+}));
+
+const NoteSchema = Yup.object().shape({
+	note: Yup.string()
+		.min(2, "Too Short!")
+		.required("Required")
+});
+
+export default function ContactNotesDialog ({ modalOpen, onClose, ticket }) {
+    const { id: ticketId, contactId } = ticket
+    const classes = useStyles()
+    const [open, setOpen] = useState(false);
+    const [newNote, setNewNote] = useState({ note: "" });
+    const [loading, setLoading] = useState(false)
+    const [showOnDeleteDialog, setShowOnDeleteDialog] = useState(false)
+    const [selectedNote, setSelectedNote] = useState({})
+    const [notes, setNotes] = useState([])
+    const { saveNote, deleteNote, listNotes } = useTicketNotes()
+
+    useEffect(() => {
+        async function openAndFetchData () {
+            if (modalOpen) {
+                setOpen(true)
+                handleResetState()
+                await loadNotes()
+            }
+        }
+        openAndFetchData()
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [modalOpen])
+
+    const handleResetState = () => {
+        setNewNote({ note: "" })
+        setLoading(false)
+    }
+
+    const handleChangeComment = (e) => {
+        setNewNote({ note: e.target.value })
+    }
+    
+    const handleClose = () => {
+        setOpen(false);
+        onClose()
+    };
+
+    const handleSave = async values => {
+        setLoading(true)
+        try {
+            await saveNote({
+                ...values,
+                ticketId, 
+                contactId
+            })
+            await loadNotes()
+            setNewNote({ note: '' })
+            toast.success('Observação adicionada com sucesso!')
+        } catch (e) {
+            toast.error(e)
+        }
+        setLoading(false)
+    }
+
+    const handleOpenDialogDelete = (item) => {
+        setSelectedNote(item)
+        setShowOnDeleteDialog(true)
+    }
+
+    const handleDelete = async () => {
+        setLoading(true)
+        try {
+            await deleteNote(selectedNote.id)
+            await loadNotes()
+            setSelectedNote({})
+            toast.success('Observação excluída com sucesso!')
+        } catch (e) {
+            toast.error(e)
+        }
+        setLoading(false)
+    }
+
+    const loadNotes = async () => {
+        setLoading(true)
+        try {
+            const notes = await listNotes({ ticketId, contactId })
+            setNotes(notes)
+        } catch (e) {
+            toast.error(e)
+        }
+        setLoading(false)
+    }
+
+    const renderNoteList = () => {
+        return notes.map((note) => {
+            return <ContactNotesDialogListItem 
+                note={note} 
+                key={note.id}
+                deleteItem={handleOpenDialogDelete}
+            />
+        })
+    }
+
+    return (
+        <>
+            <ConfirmationModal
+                title="Excluir Registro"
+                open={showOnDeleteDialog}
+                onClose={setShowOnDeleteDialog}
+                onConfirm={handleDelete}
+            >
+                Deseja realmente excluir este registro?
+            </ConfirmationModal>
+            <Dialog
+                open={open}
+                onClose={handleClose}
+                aria-labelledby="alert-dialog-title"
+                aria-describedby="alert-dialog-description"
+            >
+                <DialogTitle id="alert-dialog-title">
+                    { i18n.t("ticketOptionsMenu.appointmentsModal.title") }
+                </DialogTitle>
+                <Formik
+                    initialValues={newNote}
+                    enableReinitialize={true}
+                    validationSchema={NoteSchema}
+                    onSubmit={(values, actions) => {
+                        setTimeout(() => {
+                            handleSave(values);
+                            actions.setSubmitting(false);
+                        }, 400);
+                    }}
+                >
+
+                    {({ touched, errors }) => (
+                        <Form>
+                            <DialogContent className={classes.root} dividers>
+                                <Field
+                                    as={TextField}
+                                    name="note"
+                                    rows={3}
+                                    label={i18n.t("ticketOptionsMenu.appointmentsModal.textarea")}
+                                    placeholder={i18n.t("ticketOptionsMenu.appointmentsModal.placeholder")}
+                                    multiline={true}
+                                    error={touched.note && Boolean(errors.note)}
+                                    helperText={touched.note && errors.note}
+                                    variant="outlined"
+                                    onChange={handleChangeComment}
+                                />
+
+                                <List className={classes.list}>
+                                    { renderNoteList() }
+                                </List>
+                            </DialogContent>
+                            <DialogActions>
+                                <Button onClick={handleClose} color="primary">
+                                    Fechar
+                                </Button>
+                                <ButtonWithSpinner loading={loading} color="primary" type="submit" variant="contained" autoFocus>
+                                    Salvar
+                                </ButtonWithSpinner>
+                            </DialogActions>
+                        </Form>
+                    )}
+                </Formik>
+            </Dialog>
+        </>
+    );
+}

+ 64 - 0
frontend/src/components/ContactNotesDialogListItem/index.js

@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import IconButton from '@material-ui/core/IconButton';
+import ListItem from '@material-ui/core/ListItem';
+import ListItemText from '@material-ui/core/ListItemText';
+import ListItemAvatar from '@material-ui/core/ListItemAvatar';
+import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
+import Avatar from '@material-ui/core/Avatar';
+import Typography from '@material-ui/core/Typography';
+import { makeStyles } from '@material-ui/core/styles';
+import DeleteIcon from '@material-ui/icons/Delete';
+import moment from 'moment';
+
+const useStyles = makeStyles((theme) => ({
+    inline: {
+        width: '100%'
+    }
+}));
+
+export default function ContactNotesDialogListItem (props) {
+    const { note, deleteItem } = props;
+    const classes = useStyles();
+
+    const handleDelete = (item) => {
+        deleteItem(item);
+    }
+
+    return (
+        <ListItem alignItems="flex-start">
+            <ListItemAvatar>
+                <Avatar alt={note.user.name} src="/static/images/avatar/1.jpg" />
+            </ListItemAvatar>
+            <ListItemText
+                primary={
+                    <>
+                        <Typography
+                            component="span"
+                            variant="body2"
+                            className={classes.inline}
+                            color="textPrimary"
+                        >
+                            {note.note}
+                        </Typography>
+                    </>
+                }
+                secondary={
+                    <>
+                        {note.user.name}, {moment(note.createdAt).format('DD/MM/YY HH:mm')}
+                    </>
+                }
+            />
+            <ListItemSecondaryAction>
+                <IconButton onClick={() => handleDelete(note)} edge="end" aria-label="delete">
+                    <DeleteIcon />
+                </IconButton>
+            </ListItemSecondaryAction>
+        </ListItem>
+    )   
+}
+
+ContactNotesDialogListItem.propTypes = {
+    note: PropTypes.object.isRequired,
+    deleteItem: PropTypes.func.isRequired
+}

+ 26 - 0
frontend/src/components/ContactTag/index.js

@@ -0,0 +1,26 @@
+import { makeStyles } from "@material-ui/styles";
+import React from "react";
+
+const useStyles = makeStyles(theme => ({
+    tag: {
+        padding: "1px 5px",
+        borderRadius: "3px",
+        fontSize: "0.8em",
+        fontWeight: "bold",
+        color: "#FFF",
+        marginRight: "5px",
+        whiteSpace: "nowrap"
+    }
+}));
+
+const ContactTag = ({ tag }) => {
+    const classes = useStyles();
+
+    return (
+        <div className={classes.tag} style={{ backgroundColor: tag.color, marginTop: "2px" }}>
+            {tag.name.toUpperCase()}
+        </div>
+    )
+}
+
+export default ContactTag;

+ 50 - 0
frontend/src/components/CurrencyInput/index.js

@@ -0,0 +1,50 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import MaskedInput from 'react-text-mask'
+import createNumberMask from 'text-mask-addons/dist/createNumberMask'
+
+const defaultMaskOptions = {
+  prefix: 'R$',
+  suffix: '',
+  includeThousandsSeparator: true,
+  thousandsSeparatorSymbol: '.',
+  allowDecimal: true,
+  decimalSymbol: ',',
+  decimalLimit: 2, // how many digits allowed after the decimal
+  integerLimit: 7, // limit length of integer numbers
+  allowNegative: false,
+  allowLeadingZeroes: false,
+}
+
+const CurrencyInput = ({ maskOptions, ...inputProps }) => {
+  const currencyMask = createNumberMask({
+    ...defaultMaskOptions,
+    ...maskOptions,
+  })
+
+  return <MaskedInput mask={currencyMask} {...inputProps} />
+}
+
+CurrencyInput.defaultProps = {
+  inputMode: 'numeric',
+  maskOptions: {},
+}
+
+CurrencyInput.propTypes = {
+  inputmode: PropTypes.string,
+  maskOptions: PropTypes.shape({
+    prefix: PropTypes.string,
+    suffix: PropTypes.string,
+    includeThousandsSeparator: PropTypes.bool,
+    thousandsSeparatorSymbol: PropTypes.string,
+    allowDecimal: PropTypes.bool,
+    decimalSymbol: PropTypes.string,
+    decimalLimit: PropTypes.string,
+    requireDecimal: PropTypes.bool,
+    allowNegative: PropTypes.bool,
+    allowLeadingZeroes: PropTypes.bool,
+    integerLimit: PropTypes.number,
+  }),
+}
+
+export default CurrencyInput

+ 70 - 0
frontend/src/components/DarkMode/index.js

@@ -0,0 +1,70 @@
+import React, { useState } from "react";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { CssBaseline, IconButton } from "@material-ui/core";
+import Brightness4Icon from "@material-ui/icons/Brightness4";
+import Brightness7Icon from "@material-ui/icons/Brightness7";
+
+const useStyles = makeStyles((theme) => ({
+    icons: {
+        color: "#fff",
+    },
+    switch: {
+        color: "#fff",
+    },
+    visible: {
+        display: "none",
+    },
+    btnHeader: {
+        color: "#fff",
+    },
+}));
+
+const DarkMode = (props) => {
+    const classes = useStyles();
+
+    const [theme, setTheme] = useState("light");
+
+    const themeToggle = () => {
+        theme === "light" ? setTheme("dark") : setTheme("light");
+    };
+
+    const handleClick = () => {
+        props.themeToggle();
+        themeToggle();
+    };
+
+    return (
+        <>
+            {theme === "light" ? (
+                <>
+                    <CssBaseline />
+                    <IconButton
+                        className={classes.icons}
+                        onClick={handleClick}
+                        // ref={anchorEl}
+                        aria-label="Dark Mode"
+                        color="inherit"
+                    >
+                        <Brightness4Icon />
+                    </IconButton>
+                </>
+            ) : (
+                <>
+                    <CssBaseline />
+                    <IconButton
+                        className={classes.icons}
+                        onClick={handleClick}
+                        // ref={anchorEl}
+                        aria-label="Dark Mode"
+                        color="inherit"
+                    >
+                        <Brightness7Icon />
+                    </IconButton>
+                </>
+            )}
+        </>
+    );
+};
+
+export default DarkMode;

+ 53 - 0
frontend/src/components/Dashboard/CardCounter.js

@@ -0,0 +1,53 @@
+import React from "react";
+
+import { Avatar, Card, CardHeader, Typography } from "@material-ui/core";
+import Skeleton from "@material-ui/lab/Skeleton";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { grey } from '@material-ui/core/colors';
+
+const useStyles = makeStyles(theme => ({
+	cardAvatar: {
+		fontSize: '55px',
+		color: grey[500],
+		backgroundColor: '#ffffff',
+		width: theme.spacing(7),
+		height: theme.spacing(7)
+	},
+	cardTitle: {
+		fontSize: '18px',
+		color: theme.palette.text.primary
+	},
+	cardSubtitle: {
+		color: grey[600],
+		fontSize: '14px'
+	}
+}));
+
+export default function CardCounter(props) {
+    const { icon, title, value, loading } = props
+	const classes = useStyles();
+    return ( !loading ? 
+        <Card>
+            <CardHeader
+                avatar={
+                    <Avatar className={classes.cardAvatar}>
+                        {icon}
+                    </Avatar>
+                }
+                title={
+                    <Typography variant="h6" component="h2" className={classes.cardTitle}>
+                        { title }
+                    </Typography>
+                }
+                subheader={
+                    <Typography variant="subtitle1" component="p" className={classes.cardSubtitle}>
+                        { value }
+                    </Typography>
+                }
+            />
+        </Card>
+        : <Skeleton variant="rect" height={80} />
+    )
+    
+}

+ 89 - 0
frontend/src/components/Dashboard/TableAttendantsStatus.js

@@ -0,0 +1,89 @@
+import React from "react";
+
+import Paper from "@material-ui/core/Paper";
+import Table from '@material-ui/core/Table';
+import TableBody from '@material-ui/core/TableBody';
+import TableCell from '@material-ui/core/TableCell';
+import TableContainer from '@material-ui/core/TableContainer';
+import TableHead from '@material-ui/core/TableHead';
+import TableRow from '@material-ui/core/TableRow';
+import Skeleton from "@material-ui/lab/Skeleton";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green, red } from '@material-ui/core/colors';
+
+import CheckCircleIcon from '@material-ui/icons/CheckCircle';
+import ErrorIcon from '@material-ui/icons/Error';
+import moment from 'moment';
+
+import Rating from '@material-ui/lab/Rating';
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles(theme => ({
+	on: {
+		color: green[600],
+		fontSize: '20px'
+	},
+	off: {
+		color: red[600],
+		fontSize: '20px'
+	},
+    pointer: {
+        cursor: "pointer"
+    }
+}));
+
+export function RatingBox ({ rating }) {
+    const ratingTrunc = rating === null ? 0 : Math.trunc(rating);
+    return <Rating
+        defaultValue={ratingTrunc}
+        max={3}
+        readOnly
+    />
+}
+
+export default function TableAttendantsStatus(props) {
+    const { loading, attendants } = props
+	const classes = useStyles();
+
+    function renderList () {
+        return attendants.map((a, k) => (
+            <TableRow key={k}>
+                <TableCell>{a.name}</TableCell>
+                <TableCell align="center" title={i18n.t("dashboard.onlineTable.ratingLabel")} className={classes.pointer}>
+                    <RatingBox rating={a.rating} />
+                </TableCell>
+                <TableCell align="center">{formatTime(a.avgSupportTime, 2)}</TableCell>
+                <TableCell align="center">
+                    { a.online ?
+                        <CheckCircleIcon className={classes.on} />
+                        : <ErrorIcon className={classes.off} />
+                    }
+                </TableCell>
+            </TableRow>
+        ))
+    }
+
+	function formatTime(minutes){
+		return moment().startOf('day').add(minutes, 'minutes').format('HH[h] mm[m]');
+	}
+
+    return ( !loading ?
+        <TableContainer component={Paper}>
+            <Table>
+                <TableHead>
+                    <TableRow>
+                        <TableCell>{i18n.t("dashboard.onlineTable.name")}</TableCell>
+                        <TableCell align="center">{i18n.t("dashboard.onlineTable.ratings")}</TableCell>
+                        <TableCell align="center">{i18n.t("dashboard.onlineTable.avgSupportTime")}</TableCell>
+                        <TableCell align="center">{i18n.t("dashboard.onlineTable.status")}</TableCell>
+                    </TableRow>
+                </TableHead>
+                <TableBody>
+                    { renderList() }
+                </TableBody>
+            </Table>
+        </TableContainer>
+        : <Skeleton variant="rect" height={150} />
+    )
+}

+ 34 - 0
frontend/src/components/Dialog/index.js

@@ -0,0 +1,34 @@
+import React, { useState, useEffect } from 'react';
+import CoreDialog from '@material-ui/core/Dialog';
+import DialogTitle from '@material-ui/core/DialogTitle';
+
+function Dialog ({ title, modalOpen, onClose, children }) {
+    const [open, setOpen] = useState(false);
+
+    useEffect(() => {
+        setOpen(modalOpen)
+    }, [modalOpen])
+    
+    const handleClose = () => {
+        setOpen(false);
+        onClose()
+    };
+
+    return (
+        <>
+            <CoreDialog
+                open={open}
+                onClose={handleClose}
+                aria-labelledby="alert-dialog-title"
+                aria-describedby="alert-dialog-description"
+            >
+                <DialogTitle id="alert-dialog-title">
+                    {title}
+                </DialogTitle>
+                {children}
+            </CoreDialog>
+        </>
+    );
+}
+
+export default Dialog;

+ 350 - 0
frontend/src/components/FileModal/index.js

@@ -0,0 +1,350 @@
+import React, { useState, useEffect, useContext } from "react";
+
+import * as Yup from "yup";
+import {
+    Formik,
+    Form,
+    Field,
+    FieldArray
+} from "formik";
+import { toast } from "react-toastify";
+
+import {
+    Box,
+    Button,
+    CircularProgress,
+    Dialog,
+    DialogActions,
+    DialogContent,
+    DialogTitle,
+    Divider,
+    Grid,
+    makeStyles,
+    TextField
+} from "@material-ui/core";
+import IconButton from "@material-ui/core/IconButton";
+import Typography from "@material-ui/core/Typography";
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import AttachFileIcon from "@material-ui/icons/AttachFile";
+
+import { green } from "@material-ui/core/colors";
+
+import { i18n } from "../../translate/i18n";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+import { AuthContext } from "../../context/Auth/AuthContext";
+
+const useStyles = makeStyles(theme => ({
+    root: {
+        display: "flex",
+        flexWrap: "wrap",
+        gap: 4
+    },
+    multFieldLine: {
+        display: "flex",
+        "& > *:not(:last-child)": {
+            marginRight: theme.spacing(1),
+        },
+    },
+    textField: {
+        marginRight: theme.spacing(1),
+        flex: 1,
+    },
+
+    extraAttr: {
+        display: "flex",
+        justifyContent: "center",
+        alignItems: "center",
+    },
+
+    btnWrapper: {
+        position: "relative",
+    },
+
+    buttonProgress: {
+        color: green[500],
+        position: "absolute",
+        top: "50%",
+        left: "50%",
+        marginTop: -12,
+        marginLeft: -12,
+    },
+    formControl: {
+        margin: theme.spacing(1),
+        minWidth: 2000,
+    },
+    colorAdorment: {
+        width: 20,
+        height: 20,
+    },
+}));
+
+const FileListSchema = Yup.object().shape({
+    name: Yup.string()
+        .min(3, i18n.t("fileModal.formErrors.name.short"))
+        .required(i18n.t("fileModal.formErrors.name.required")),
+    message: Yup.string()
+        .required(i18n.t("fileModal.formErrors.message.required"))
+});
+
+const FilesModal = ({ open, onClose, fileListId, reload }) => {
+    const classes = useStyles();
+    const { user } = useContext(AuthContext);
+    const [ files, setFiles ] = useState([]);
+    const [selectedFileNames, setSelectedFileNames] = useState([]);
+
+
+    const initialState = {
+        name: "",
+        message: "",
+        options: [{ name: "", path:"", mediaType:"" }],
+    };
+
+    const [fileList, setFileList] = useState(initialState);
+
+    useEffect(() => {
+        try {
+            (async () => {
+                if (!fileListId) return;
+
+                const { data } = await api.get(`/files/${fileListId}`);
+                setFileList(data);
+            })()
+        } catch (err) {
+            toastError(err);
+        }
+    }, [fileListId, open]);
+
+    const handleClose = () => {
+        setFileList(initialState);
+        setFiles([]);
+        onClose();
+    };
+
+    const handleSaveFileList = async (values) => {
+
+        const uploadFiles = async (options, filesOptions, id) => {
+                const formData = new FormData();
+                formData.append("fileId", id);
+                formData.append("typeArch", "fileList")
+                filesOptions.forEach((fileOption, index) => {
+                    if (fileOption.file) {
+                        formData.append("files", fileOption.file);
+                        formData.append("mediaType", fileOption.file.type)
+                        formData.append("name", options[index].name);
+                        formData.append("id", options[index].id);
+                    }
+                });
+      
+              try {
+                const { data } = await api.post(`/files/uploadList/${id}`, formData);
+                setFiles([]);
+                return data;
+              } catch (err) {
+                toastError(err);
+              }
+            return null;
+        }
+
+        const fileData = { ...values, userId: user.id };
+        
+        try {
+            if (fileListId) {
+                const { data } = await api.put(`/files/${fileListId}`, fileData)
+                if (data.options.length > 0)
+
+                    uploadFiles(data.options, values.options, fileListId)
+            } else {
+                const { data } = await api.post("/files", fileData);
+                if (data.options.length > 0)
+                    uploadFiles(data.options, values.options, data.id)
+            }
+            toast.success(i18n.t("fileModal.success"));
+            if (typeof reload == 'function') {
+                reload();
+            }            
+        } catch (err) {
+            toastError(err);
+        }
+        handleClose();
+    };
+
+    return (
+        <div className={classes.root}>
+            <Dialog
+                open={open}
+                onClose={handleClose}
+                maxWidth="md"
+                fullWidth
+                scroll="paper">
+                <DialogTitle id="form-dialog-title">
+                    {(fileListId ? `${i18n.t("fileModal.title.edit")}` : `${i18n.t("fileModal.title.add")}`)}
+                </DialogTitle>
+                <Formik
+                    initialValues={fileList}
+                    enableReinitialize={true}
+                    validationSchema={FileListSchema}
+                    onSubmit={(values, actions) => {
+                        setTimeout(() => {
+                            handleSaveFileList(values);
+                            actions.setSubmitting(false);
+                        }, 400);
+                    }}
+                >
+                    {({ touched, errors, isSubmitting, values }) => (
+                        <Form>
+                            <DialogContent dividers>
+                                <div className={classes.multFieldLine}>
+                                    <Field
+                                        as={TextField}
+                                        label={i18n.t("fileModal.form.name")}
+                                        name="name"
+                                        error={touched.name && Boolean(errors.name)}
+                                        helperText={touched.name && errors.name}
+                                        variant="outlined"
+                                        margin="dense"
+                                        fullWidth
+                                    />
+                                </div>
+                                <br />
+                                <div className={classes.multFieldLine}>
+                                    <Field
+                                        as={TextField}
+                                        label={i18n.t("fileModal.form.message")}
+                                        type="message"
+                                        multiline
+                                        minRows={5}
+                                        fullWidth
+                                        name="message"
+                                        error={
+                                            touched.message && Boolean(errors.message)
+                                        }
+                                        helperText={
+                                            touched.message && errors.message
+                                        }
+                                        variant="outlined"
+                                        margin="dense"
+                                    />
+                                </div>
+                                <Typography
+                                    style={{ marginBottom: 8, marginTop: 12 }}
+                                    variant="subtitle1"
+                                >
+                                    {i18n.t("fileModal.form.fileOptions")}
+                                </Typography>
+
+                                <FieldArray name="options">
+                                    {({ push, remove }) => (
+                                        <>
+                                            {values.options &&
+                                                values.options.length > 0 &&
+                                                values.options.map((info, index) => (    
+                                                    <div
+                                                        className={classes.extraAttr}
+                                                        key={`${index}-info`}
+                                                    >
+                                                        <Grid container  spacing={0}>
+                                                            <Grid xs={6} md={10} item> 
+                                                                <Field
+                                                                    as={TextField}
+                                                                    label={i18n.t("fileModal.form.extraName")}
+                                                                    name={`options[${index}].name`}
+                                                                    variant="outlined"
+                                                                    margin="dense"
+                                                                    multiline
+                                                                    fullWidth
+                                                                    minRows={2}
+                                                                    className={classes.textField}
+                                                                />
+                                                            </Grid>     
+                                                            <Grid xs={2} md={2} item style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
+                                                                <input
+                                                                    type="file"
+                                                                    onChange={(e) => {
+                                                                        const selectedFile = e.target.files[0];
+                                                                        const updatedOptions = [...values.options];                                                                
+                                                                        updatedOptions[index].file = selectedFile;
+                                                                       
+                                                                        setFiles('options', updatedOptions);
+
+                                                                        // Atualize a lista selectedFileNames para o campo específico
+                                                                        const updatedFileNames = [...selectedFileNames];
+                                                                        updatedFileNames[index] = selectedFile ? selectedFile.name : '';
+                                                                        setSelectedFileNames(updatedFileNames);
+                                                                    }}
+                                                                    style={{ display: 'none' }}
+                                                                    name={`options[${index}].file`}
+                                                                    id={`file-upload-${index}`}
+                                                                />
+                                                                <label htmlFor={`file-upload-${index}`}>
+                                                                    <IconButton component="span">
+                                                                        <AttachFileIcon />
+                                                                    </IconButton>
+                                                                </label>
+                                                                <IconButton
+                                                                    size="small"
+                                                                    onClick={() => remove(index)}
+                                                                >
+                                                                    <DeleteOutlineIcon />
+                                                                </IconButton>    
+                                                            </Grid>
+                                                            <Grid xs={12} md={12} item>
+                                                                {info.path? info.path : selectedFileNames[index]}                               
+                                                            </Grid> 
+                                                        </Grid>                                                    
+                                                </div>                     
+                                                                                           
+                                                ))}
+                                            <div className={classes.extraAttr}>
+                                                <Button
+                                                    style={{ flex: 1, marginTop: 8 }}
+                                                    variant="outlined"
+                                                    color="primary"
+                                                    onClick={() => {push({ name: "", path: ""});
+                                                    setSelectedFileNames([...selectedFileNames, ""]);
+                                                }}
+                                                >
+                                                    {`+ ${i18n.t("fileModal.buttons.fileOptions")}`}
+                                                </Button>
+                                            </div>
+                                        </>
+                                    )}
+                                </FieldArray>
+                            </DialogContent>
+                            <DialogActions>
+                                <Button
+                                    onClick={handleClose}
+                                    color="secondary"
+                                    disabled={isSubmitting}
+                                    variant="outlined"
+                                >
+                                    {i18n.t("fileModal.buttons.cancel")}
+                                </Button>
+                                <Button
+                                    type="submit"
+                                    color="primary"
+                                    disabled={isSubmitting}
+                                    variant="contained"
+                                    className={classes.btnWrapper}
+                                >
+                                    {fileListId
+                                        ? `${i18n.t("fileModal.buttons.okEdit")}`
+                                        : `${i18n.t("fileModal.buttons.okAdd")}`}
+                                    {isSubmitting && (
+                                        <CircularProgress
+                                            size={24}
+                                            className={classes.buttonProgress}
+                                        />
+                                    )}
+                                </Button>
+                            </DialogActions>
+                        </Form>
+                    )}
+                </Formik>
+            </Dialog>
+        </div>
+    );
+};
+
+export default FilesModal;

+ 38 - 0
frontend/src/components/FormFields/CheckboxField.js

@@ -0,0 +1,38 @@
+import React from 'react';
+import { at } from 'lodash';
+import { useField } from 'formik';
+import {
+  Checkbox,
+  FormControl,
+  FormControlLabel,
+  FormHelperText
+} from '@material-ui/core';
+
+export default function CheckboxField(props) {
+  const { label, ...rest } = props;
+  const [field, meta, helper] = useField(props);
+  const { setValue } = helper;
+
+  function _renderHelperText() {
+    const [touched, error] = at(meta, 'touched', 'error');
+    if (touched && error) {
+      return <FormHelperText>{error}</FormHelperText>;
+    }
+  }
+
+  function _onChange(e) {
+    setValue(e.target.checked);
+  }
+
+  return (
+    <FormControl {...rest}>
+      <FormControlLabel
+        value={field.checked}
+        checked={field.checked}
+        control={<Checkbox {...field} onChange={_onChange} />}
+        label={label}
+      />
+      {_renderHelperText()}
+    </FormControl>
+  );
+}

+ 54 - 0
frontend/src/components/FormFields/DatePickerField.js

@@ -0,0 +1,54 @@
+import React, { useState, useEffect } from 'react';
+import { useField } from 'formik';
+import Grid from '@material-ui/core/Grid';
+import {
+  MuiPickersUtilsProvider,
+  KeyboardDatePicker
+} from '@material-ui/pickers';
+import DateFnsUtils from '@date-io/date-fns';
+
+export default function DatePickerField(props) {
+  const [field, meta, helper] = useField(props);
+  const { touched, error } = meta;
+  const { setValue } = helper;
+  const isError = touched && error && true;
+  const { value } = field;
+  const [selectedDate, setSelectedDate] = useState(null);
+
+  useEffect(() => {
+    if (value) {
+      const date = new Date(value);
+      setSelectedDate(date);
+    }
+  }, [value]);
+
+  function _onChange(date) {
+    if (date) {
+      setSelectedDate(date);
+      try {
+        const ISODateString = date.toISOString();
+        setValue(ISODateString);
+      } catch (error) {
+        setValue(date);
+      }
+    } else {
+      setValue(date);
+    }
+  }
+
+  return (
+    <Grid container>
+      <MuiPickersUtilsProvider utils={DateFnsUtils}>
+        <KeyboardDatePicker
+          {...field}
+          {...props}
+          value={selectedDate}
+          onChange={_onChange}
+          error={isError}
+          invalidDateMessage={isError && error}
+          helperText={isError && error}
+        />
+      </MuiPickersUtilsProvider>
+    </Grid>
+  );
+}

+ 26 - 0
frontend/src/components/FormFields/InputField.js

@@ -0,0 +1,26 @@
+import React from 'react';
+import { at } from 'lodash';
+import { useField } from 'formik';
+import { TextField } from '@material-ui/core';
+
+export default function InputField(props) {
+  const { errorText, ...rest } = props;
+  const [field, meta] = useField(props);
+
+  function _renderHelperText() {
+    const [touched, error] = at(meta, 'touched', 'error');
+    if (touched && error) {
+      return error;
+    }
+  }
+
+  return (
+    <TextField
+      type="text"
+      error={meta.touched && meta.error && true}
+      helperText={_renderHelperText()}
+      {...field}
+      {...rest}
+    />
+  );
+}

+ 48 - 0
frontend/src/components/FormFields/SelectField.js

@@ -0,0 +1,48 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { at } from 'lodash';
+import { useField } from 'formik';
+import {
+  InputLabel,
+  FormControl,
+  Select,
+  MenuItem,
+  FormHelperText
+} from '@material-ui/core';
+
+function SelectField(props) {
+  const { label, data, ...rest } = props;
+  const [field, meta] = useField(props);
+  const { value: selectedValue } = field;
+  const [touched, error] = at(meta, 'touched', 'error');
+  const isError = touched && error && true;
+  function _renderHelperText() {
+    if (isError) {
+      return <FormHelperText>{error}</FormHelperText>;
+    }
+  }
+
+  return (
+    <FormControl {...rest} error={isError}>
+      <InputLabel>{label}</InputLabel>
+      <Select {...field} value={selectedValue ? selectedValue : ''}>
+        {data.map((item, index) => (
+          <MenuItem key={index} value={item.value}>
+            {item.label}
+          </MenuItem>
+        ))}
+      </Select>
+      {_renderHelperText()}
+    </FormControl>
+  );
+}
+
+SelectField.defaultProps = {
+  data: []
+};
+
+SelectField.propTypes = {
+  data: PropTypes.array.isRequired
+};
+
+export default SelectField;

+ 5 - 0
frontend/src/components/FormFields/index.js

@@ -0,0 +1,5 @@
+import InputField from './InputField';
+import CheckboxField from './CheckboxField';
+import SelectField from './SelectField';
+import DatePickerField from './DatePickerField';
+export { InputField, CheckboxField, SelectField, DatePickerField };

+ 291 - 0
frontend/src/components/HelpsManager/index.js

@@ -0,0 +1,291 @@
+import React, { useState, useEffect } from "react";
+import {
+    makeStyles,
+    Paper,
+    Grid,
+    TextField,
+    Table,
+    TableHead,
+    TableBody,
+    TableCell,
+    TableRow,
+    IconButton
+} from "@material-ui/core";
+import { Formik, Form, Field } from 'formik';
+import ButtonWithSpinner from "../ButtonWithSpinner";
+import ConfirmationModal from "../ConfirmationModal";
+
+import { Edit as EditIcon } from "@material-ui/icons";
+
+import { toast } from "react-toastify";
+import useHelps from "../../hooks/useHelps";
+import { i18n } from "../../translate/i18n";
+
+
+const useStyles = makeStyles(theme => ({
+	root: {
+		width: '100%'
+	},
+    mainPaper: {
+		width: '100%',
+		flex: 1,
+		padding: theme.spacing(2)
+    },
+	fullWidth: {
+		width: '100%'
+	},
+    tableContainer: {
+		width: '100%',
+		overflowX: "scroll",
+		...theme.scrollbarStyles
+    },
+	textfield: {
+		width: '100%'
+	},
+    textRight: {
+        textAlign: 'right'
+    },
+    row: {
+		paddingTop: theme.spacing(2),
+		paddingBottom: theme.spacing(2)
+    },
+    control: {
+		paddingRight: theme.spacing(1),
+		paddingLeft: theme.spacing(1)
+	},
+    buttonContainer: {
+        textAlign: 'right',
+		padding: theme.spacing(1)
+	}
+}));
+
+export function HelpManagerForm (props) {
+    const { onSubmit, onDelete, onCancel, initialValue, loading } = props;
+    const classes = useStyles()
+
+    const [record, setRecord] = useState(initialValue);
+
+    useEffect(() => {
+        setRecord(initialValue)
+    }, [initialValue])
+
+    const handleSubmit = async(data) => {
+        onSubmit(data)
+    }
+
+    return (
+        <Formik
+            enableReinitialize
+            className={classes.fullWidth}
+            initialValues={record}
+            onSubmit={(values, { resetForm }) =>
+                setTimeout(() => {
+                    handleSubmit(values)
+                    resetForm()
+                }, 500)
+            }
+        >
+            {(values) => (
+                <Form className={classes.fullWidth}>
+                    <Grid spacing={2} justifyContent="flex-end" container>
+                        <Grid xs={12} sm={6} md={3} item>
+                            <Field
+                                as={TextField}
+                                label="Título"
+                                name="title"
+                                variant="outlined"
+                                className={classes.fullWidth}
+                                margin="dense"
+                            />
+                        </Grid>
+                        <Grid xs={12} sm={6} md={3} item>
+                            <Field
+                                as={TextField}
+                                label="Código do Vídeo"
+                                name="video"
+                                variant="outlined"
+                                className={classes.fullWidth}
+                                margin="dense"
+                            />
+                        </Grid>
+                        <Grid xs={12} sm={12} md={6} item>
+                            <Field
+                                as={TextField}
+                                label="Descrição"
+                                name="description"
+                                variant="outlined"
+                                className={classes.fullWidth}
+                                margin="dense"
+                            />
+                        </Grid>
+                        <Grid sm={3} md={1} item>
+                            <ButtonWithSpinner className={classes.fullWidth} loading={loading} onClick={() => onCancel()} variant="contained">
+                                {i18n.t('settings.helps.buttons.clean')}
+                            </ButtonWithSpinner>
+                        </Grid>
+                        { record.id !== undefined ? (
+                            <Grid sm={3} md={1} item>
+                                <ButtonWithSpinner className={classes.fullWidth} loading={loading} onClick={() => onDelete(record)} variant="contained" color="secondary">
+                                    {i18n.t('settings.helps.buttons.delete')}
+                                </ButtonWithSpinner>
+                            </Grid>
+                        ) : null}
+                        <Grid sm={3} md={1} item>
+                            <ButtonWithSpinner className={classes.fullWidth} loading={loading} type="submit" variant="contained" color="primary">
+                                {i18n.t('settings.helps.buttons.save')}
+                            </ButtonWithSpinner>
+                        </Grid>
+                    </Grid>
+                </Form>
+            )}
+        </Formik>
+    )
+}
+
+export function HelpsManagerGrid (props) {
+    const { records, onSelect } = props
+    const classes = useStyles()
+
+    return (
+        <Paper className={classes.tableContainer}>
+            <Table className={classes.fullWidth} size="small" aria-label="a dense table">
+                <TableHead>
+                <TableRow>
+                    <TableCell align="center" style={{width: '1%'}}>#</TableCell>
+                    <TableCell align="left">{i18n.t("settings.helps.grid.title")}</TableCell>
+                    <TableCell align="left">{i18n.t("settings.helps.grid.description")}</TableCell>
+                    <TableCell align="left">{i18n.t("settings.helps.grid.video")}</TableCell>
+                </TableRow>
+                </TableHead>
+                <TableBody>
+                {records.map((row) => (
+                    <TableRow key={row.id}>
+                        <TableCell align="center" style={{width: '1%'}}>
+                            <IconButton onClick={() => onSelect(row)} aria-label="delete">
+                                <EditIcon />
+                            </IconButton>
+                        </TableCell>
+                        <TableCell align="left">{row.title || '-'}</TableCell>
+                        <TableCell align="left">{row.description || '-'}</TableCell>
+                        <TableCell align="left">{row.video || '-'}</TableCell>
+                    </TableRow>
+                ))}
+                </TableBody>
+            </Table>
+        </Paper>
+    )
+}
+
+export default function HelpsManager () {
+    const classes = useStyles()
+    const { list, save, update, remove } = useHelps()
+    
+    const [showConfirmDialog, setShowConfirmDialog] = useState(false)
+    const [loading, setLoading] = useState(false)
+    const [records, setRecords] = useState([])
+    const [record, setRecord] = useState({
+        title: '',
+        description: '',
+        video: ''
+    })
+
+    useEffect(() => {
+        async function fetchData () {
+            await loadHelps()
+        }
+        fetchData()
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [])
+
+    const loadHelps = async () => {
+        setLoading(true)
+        try {
+            const helpList = await list()
+            setRecords(helpList)
+        } catch (e) {
+            toast.error(i18n.t('settings.helps.toasts.errorList'))
+        }
+        setLoading(false)
+    }
+
+    const handleSubmit = async (data) => {
+        setLoading(true)
+        try {
+            if (data.id !== undefined) {
+                await update(data)
+            } else {
+                await save(data)
+            }
+            await loadHelps()
+            handleCancel()
+            toast.success(i18n.t('settings.helps.toasts.success'))
+        } catch (e) {
+            toast.error(i18n.t('settings.helps.toasts.error'))
+        }
+        setLoading(false)
+    }
+
+    const handleDelete = async () => {
+        setLoading(true)
+        try {
+            await remove(record.id)
+            await loadHelps()
+            handleCancel()
+            toast.success(i18n.t('settings.helps.toasts.success'))
+        } catch (e) {
+            toast.error(i18n.t('settings.helps.toasts.errorOperation'))
+        }
+        setLoading(false)
+    }
+
+    const handleOpenDeleteDialog = () => {
+        setShowConfirmDialog(true)
+    }
+
+    const handleCancel = () => {
+        setRecord({
+            title: '',
+            description: '',
+            video: ''
+        })
+    }
+
+    const handleSelect = (data) => {
+        setRecord({
+            id: data.id,
+            title: data.title || '',
+            description: data.description || '',
+            video: data.video || ''
+        })
+    }
+
+    return (
+        <Paper className={classes.mainPaper} elevation={0}>
+            <Grid spacing={2} container>
+                <Grid xs={12} item>
+                    <HelpManagerForm 
+                        initialValue={record} 
+                        onDelete={handleOpenDeleteDialog} 
+                        onSubmit={handleSubmit} 
+                        onCancel={handleCancel} 
+                        loading={loading}
+                    />
+                </Grid>
+                <Grid xs={12} item>
+                    <HelpsManagerGrid 
+                        records={records}
+                        onSelect={handleSelect}
+                    />
+                </Grid>
+            </Grid>
+            <ConfirmationModal
+                title={i18n.t('settings.helps.confirmModal.title')}
+                open={showConfirmDialog}
+                onClose={() => setShowConfirmDialog(false)}
+                onConfirm={() => handleDelete()}
+            >
+                {i18n.t('settings.helps.confirmModal.confirm')}
+            </ConfirmationModal>
+        </Paper>
+    )
+}

+ 217 - 0
frontend/src/components/ImportContactsModal/index.js

@@ -0,0 +1,217 @@
+import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Grid, IconButton, makeStyles, Modal, Typography } from "@material-ui/core";
+import { CloseOutlined, FontDownload, ImportContacts } from "@material-ui/icons";
+import React, { useState } from "react";
+import { FaDownload } from "react-icons/fa";
+import * as XLSX from 'xlsx';
+import { array } from "yup";
+import toastError from "../../errors/toastError";
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles((theme) => ({
+    root: {
+        display: "flex",
+        flexWrap: "wrap",
+    },
+    dialogImport: {
+        minWidth: 500,
+    },
+    iFile: {
+        display: "none"
+    },
+    lbFile: {
+        border: "dashed",
+        borderWidth: 2,
+        padding: 18,
+        cursor: "pointer",
+        display: "inline-block",
+        width: "70%"
+    },
+    btnWrapper: {
+        position: "relative"
+    },
+    iconPlanilha: {
+        marginRight: 10
+    },
+    cLbFile: {
+        textAlign: "center"
+    },
+    titleLb: {
+        textTransform: "uppercase",
+        fontWeight: "bolder",
+        marginTop:10
+    },
+    iconDownload: {
+        color: theme.palette.primary.main,
+        fontSize: 18
+    },
+    cModal: {
+        paddingTop: 50,
+        paddingBottom: 50
+    },
+    cSuccessContacts: {
+        backgroundColor: "#AAEE9C80",
+        padding: 10,
+        borderRadius: 8,
+        marginTop:30
+    },
+    cErrorContacts: {
+        backgroundColor: "#DD011B40",
+        padding: 10,
+        borderRadius: 8,
+        marginTop:30
+    },
+    titleResult: {
+        fontWeight: "bolder"
+    },
+    cCloseModal: {
+        textAlign: "end"
+    }
+}))
+
+const ImportContactsModal = ( props ) => {
+
+    const classes = useStyles();
+
+    const {
+        open,
+        onClose
+    } = props;
+    const [isSubmitting, setIsSubmitting] = useState(false);
+    const [nameFile, setNameFile] = useState('');
+    const [listcontacts, setListContacts] = useState([]);
+    const [successUpload, setSuccessUpload] = useState([]);
+    const [errorUpload, setErrorUpload] = useState([]);
+
+    const handleNewFile = ( e ) => {
+
+        const file = e.target.files[0];
+
+        if(!file) return;
+
+        setNameFile( file.name );
+        readXlsx( file );
+    }
+
+    const readXlsx = ( file ) => {
+
+        const reader = new FileReader();
+        reader.onload = ( e ) => {
+
+            const ab = e.target.result;
+            const wb = XLSX.read(ab,{type: 'array'})
+
+            const wsname = wb.SheetNames[0];
+            const ws = wb.Sheets[wsname];
+
+            const data = XLSX.utils.sheet_to_json(ws);
+            setListContacts(data);
+        }
+
+        reader.readAsArrayBuffer(file);
+    }
+
+    const handleSaveListContacts = async (  ) => {
+
+        setIsSubmitting(true);
+        try{
+
+            const {data: responseData} = await api.post("/contacts/upload", listcontacts);
+            setSuccessUpload(responseData.newContacts);
+            setErrorUpload(responseData.errorBag);
+
+        }catch(e){
+            toastError(e);
+        }finally{
+            setIsSubmitting(false);
+        }
+    }
+
+    const handleDownloadModel = (  ) => {
+        
+        window.location.href = `${window.location.protocol}//${window.location.host}/import-contatos.xlsx`;
+    }
+
+    return (
+        <div >
+            <Dialog open={open} maxWidth="sm" fullWidth scroll="paper" >
+                <DialogTitle>
+                    <Grid container alignItems="center">
+                        <Grid item xs={6}>
+                            {i18n.t("contactImportModal.title")}
+                        </Grid>
+                        <Grid item xs={6} className={classes.cCloseModal}>
+                            <IconButton onClick={onClose}>
+                                <CloseOutlined />  
+                            </IconButton>                                                                          
+                        </Grid>
+                    </Grid>
+                </DialogTitle>
+                <DialogContent dividers className={classes.cModal}>
+                    <div className={classes.cLbFile}>
+                        <label className={classes.lbFile} htmlFor="i-import-contacts">
+                            <FaDownload className={classes.iconDownload} />
+                            <div className={classes.titleLb}>
+                                {i18n.t("contactImportModal.labels.import")}
+                            </div>
+                            {nameFile !== '' && (
+                                <div>
+                                    ({ nameFile } - {listcontacts.length} {i18n.t("contactImportModal.labels.result")})
+                                </div> 
+                            )}                                                      
+                        </label>
+                        <input onChange={handleNewFile} className={classes.iFile} type="file" accept=".xlsx" id="i-import-contacts"/>
+                    </div>
+                    {successUpload.length > 0 && (
+                        <div className={classes.cSuccessContacts}>
+                            <Typography className={classes.titleResult}>
+                                {i18n.t("contactImportModal.labels.added")}:
+                            </Typography>
+                            {successUpload.map((contact) => (
+                                <div>
+                                    {contact.contactId} | {contact.contactName} - {i18n.t("contactImportModal.labels.savedContact")}
+                                </div>
+                            ))}
+                        </div>
+                    )}                    
+                    {errorUpload.length > 0 && (
+                        <div className={classes.cErrorContacts}>
+                            <Typography className={classes.titleResult}>
+                                {i18n.t("contactImportModal.labels.errors")}:
+                            </Typography>
+                            <ul>
+                                {errorUpload.map((contact) => (
+                                    <li>
+                                        {contact.contactName} - {contact.error.message} 
+                                    </li>
+                                ))}
+                            </ul>
+                        </div>
+                    )}                    
+                </DialogContent>
+                <DialogActions>
+                    <Button
+                        color="primary"
+                        disabled={isSubmitting}
+                        variant="outlined"
+                        onClick={handleDownloadModel}
+                    >
+                        <ImportContacts className={classes.iconPlanilha} />
+                        {i18n.t("contactImportModal.buttons.download")}
+                    </Button>
+                    <Button
+                        color="primary"
+                        disabled={isSubmitting}
+                        variant="contained"
+                        className={classes.btnWrapper}
+                        onClick={handleSaveListContacts}
+                    >
+                        {i18n.t("contactImportModal.buttons.import")}
+                    </Button>
+                </DialogActions>
+            </Dialog>
+        </div>
+    );
+}
+
+export default ImportContactsModal;

+ 45 - 0
frontend/src/components/LanguageControl/index.js

@@ -0,0 +1,45 @@
+import React, { useEffect, useState } from 'react';
+import { changeLanguage, i18n } from "../../translate/i18n";
+import { RadioGroup, FormControlLabel, Radio } from '@material-ui/core';
+import api from "../../services/api";
+
+const LanguageControl = () => {
+    const [selectedLanguage, setSelectedLanguage] = useState('en');
+
+    const handleLanguageChange = async (event) => {
+        const newLanguage = event.target.value;
+        setSelectedLanguage(newLanguage);
+        changeLanguage(newLanguage);
+
+        try{
+            await api.post(`/users/set-language/${newLanguage}`);
+        }catch(error){
+            console.error(error);
+        }
+
+    };
+
+    useEffect(() => {
+        const saveLanguage = localStorage.getItem('i18nextLng');
+        setSelectedLanguage(saveLanguage);
+    }, []);
+
+    return (
+        <div>
+            <label htmlFor="language-select">{i18n.t("selectLanguage")}</label>
+            <RadioGroup
+                aria-label="language"
+                name="language-radio-group"
+                value={selectedLanguage}
+                onChange={handleLanguageChange}
+                row
+            >
+                <FormControlLabel value="pt" control={<Radio />} label="Português (BR)" />
+                <FormControlLabel value="en" control={<Radio />} label="English" />
+                <FormControlLabel value="es" control={<Radio />} label="Español" />
+            </RadioGroup>
+        </div>
+    );
+};
+
+export default LanguageControl;

+ 53 - 0
frontend/src/components/LocationPreview/index.js

@@ -0,0 +1,53 @@
+import React, { useEffect } from 'react';
+import toastError from "../../errors/toastError";
+
+import { Button, Divider, Typography} from "@material-ui/core";
+import { i18n } from '../../translate/i18n';
+
+const LocationPreview = ({ image, link, description }) => {
+    useEffect(() => {}, [image, link, description]);
+
+    const handleLocation = async() => {
+        try {
+            window.open(link);
+        } catch (err) {
+            toastError(err);
+        }
+    }
+
+    return (
+		<>
+			<div style={{
+				minWidth: "250px",
+			}}>
+				<div>
+					<div style={{ float: "left" }}>
+						<img src={image} alt="loc" onClick={handleLocation} style={{ width: "100px" }} />
+					</div>
+					{ description && (
+					<div style={{ display: "flex", flexWrap: "wrap" }}>
+						<Typography style={{ marginTop: "12px", marginLeft: "15px", marginRight: "15px", float: "left" }} variant="subtitle1" color="primary" gutterBottom>
+							<div dangerouslySetInnerHTML={{ __html: description.replace('\\n', '<br />') }}></div>
+						</Typography>
+					</div>
+					)}
+					<div style={{ display: "block", content: "", clear: "both" }}></div>
+					<div>
+						<Divider />
+						<Button
+							fullWidth
+							color="primary"
+							onClick={handleLocation}
+							disabled={!link}
+						>
+							{i18n.t("locationPreview.button")}
+						</Button>
+					</div>
+				</div>
+			</div>
+		</>
+	);
+
+};
+
+export default LocationPreview;

+ 31 - 0
frontend/src/components/MainContainer/index.js

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

+ 19 - 0
frontend/src/components/MainHeader/index.js

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

+ 21 - 0
frontend/src/components/MainHeaderButtonsWrapper/index.js

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

+ 186 - 0
frontend/src/components/MarkdownWrapper/index.js

@@ -0,0 +1,186 @@
+import React from "react";
+import Markdown from "markdown-to-jsx";
+
+const elements = [
+	"a",
+	"abbr",
+	"address",
+	"area",
+	"article",
+	"aside",
+	"audio",
+	"b",
+	"base",
+	"bdi",
+	"bdo",
+	"big",
+	"blockquote",
+	"body",
+	"br",
+	"button",
+	"canvas",
+	"caption",
+	"cite",
+	"code",
+	"col",
+	"colgroup",
+	"data",
+	"datalist",
+	"dd",
+	"del",
+	"details",
+	"dfn",
+	"dialog",
+	"div",
+	"dl",
+	"dt",
+	"em",
+	"embed",
+	"fieldset",
+	"figcaption",
+	"figure",
+	"footer",
+	"form",
+	"h1",
+	"h2",
+	"h3",
+	"h4",
+	"h5",
+	"h6",
+	"head",
+	"header",
+	"hgroup",
+	"hr",
+	"html",
+	"i",
+	"iframe",
+	"img",
+	"input",
+	"ins",
+	"kbd",
+	"keygen",
+	"label",
+	"legend",
+	"li",
+	"link",
+	"main",
+	"map",
+	"mark",
+	"marquee",
+	"menu",
+	"menuitem",
+	"meta",
+	"meter",
+	"nav",
+	"noscript",
+	"object",
+	"ol",
+	"optgroup",
+	"option",
+	"output",
+	"p",
+	"param",
+	"picture",
+	"pre",
+	"progress",
+	"q",
+	"rp",
+	"rt",
+	"ruby",
+	"s",
+	"samp",
+	"script",
+	"section",
+	"select",
+	"small",
+	"source",
+	"span",
+	"strong",
+	"style",
+	"sub",
+	"summary",
+	"sup",
+	"table",
+	"tbody",
+	"td",
+	"textarea",
+	"tfoot",
+	"th",
+	"thead",
+	"time",
+	"title",
+	"tr",
+	"track",
+	"u",
+	"ul",
+	"var",
+	"video",
+	"wbr",
+
+	// SVG
+	"circle",
+	"clipPath",
+	"defs",
+	"ellipse",
+	"foreignObject",
+	"g",
+	"image",
+	"line",
+	"linearGradient",
+	"marker",
+	"mask",
+	"path",
+	"pattern",
+	"polygon",
+	"polyline",
+	"radialGradient",
+	"rect",
+	"stop",
+	"svg",
+	"text",
+	"tspan",
+];
+
+const allowedElements = ["a", "b", "strong", "em", "u", "code", "del"];
+
+const CustomLink = ({ children, ...props }) => (
+	<a {...props} target="_blank" rel="noopener noreferrer">
+		{children}
+	</a>
+);
+
+const MarkdownWrapper = ({ children }) => {
+	const boldRegex = /\*(.*?)\*/g;
+	const tildaRegex = /~(.*?)~/g;
+
+	if (children && boldRegex.test(children)) {
+		children = children.replace(boldRegex, "**$1**");
+	}
+	if (children && tildaRegex.test(children)) {
+		children = children.replace(tildaRegex, "~~$1~~");
+	}
+
+	const options = React.useMemo(() => {
+		const markdownOptions = {
+			disableParsingRawHTML: true,
+			forceInline: true,
+			overrides: {
+				a: { component: CustomLink },
+			},
+		};
+
+		elements.forEach(element => {
+			if (!allowedElements.includes(element)) {
+				markdownOptions.overrides[element] = el => el.children || null;
+			}
+		});
+
+		return markdownOptions;
+	}, []);
+
+	if (!children) return null;
+
+	return <Markdown options={options}>{children}</Markdown>;
+};
+
+export default MarkdownWrapper;

+ 48 - 0
frontend/src/components/MessageInput/RecordingTimer.js

@@ -0,0 +1,48 @@
+import React, { useState, useEffect } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+
+const useStyles = makeStyles(theme => ({
+	timerBox: {
+		display: "flex",
+		marginLeft: 10,
+		marginRight: 10,
+		alignItems: "center",
+	},
+}));
+
+const RecordingTimer = () => {
+	const classes = useStyles();
+	const initialState = {
+		minutes: 0,
+		seconds: 0,
+	};
+	const [timer, setTimer] = useState(initialState);
+
+	useEffect(() => {
+		const interval = setInterval(
+			() =>
+				setTimer(prevState => {
+					if (prevState.seconds === 59) {
+						return { ...prevState, minutes: prevState.minutes + 1, seconds: 0 };
+					}
+					return { ...prevState, seconds: prevState.seconds + 1 };
+				}),
+			1000
+		);
+		return () => {
+			clearInterval(interval);
+		};
+	}, []);
+
+	const addZero = n => {
+		return n < 10 ? "0" + n : n;
+	};
+
+	return (
+		<div className={classes.timerBox}>
+			<span>{`${addZero(timer.minutes)}:${addZero(timer.seconds)}`}</span>
+		</div>
+	);
+};
+
+export default RecordingTimer;

+ 513 - 0
frontend/src/components/MessageInput/index.js

@@ -0,0 +1,513 @@
+import React, { useState, useEffect, useContext, useRef } from "react";
+import "emoji-mart/css/emoji-mart.css";
+import { useParams } from "react-router-dom";
+import { Picker } from "emoji-mart";
+import MicRecorder from "mic-recorder-to-mp3";
+import clsx from "clsx";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+import InputBase from "@material-ui/core/InputBase";
+import CircularProgress from "@material-ui/core/CircularProgress";
+import { green } from "@material-ui/core/colors";
+import AttachFileIcon from "@material-ui/icons/AttachFile";
+import IconButton from "@material-ui/core/IconButton";
+import MoodIcon from "@material-ui/icons/Mood";
+import SendIcon from "@material-ui/icons/Send";
+import CancelIcon from "@material-ui/icons/Cancel";
+import ClearIcon from "@material-ui/icons/Clear";
+import MicIcon from "@material-ui/icons/Mic";
+import CheckCircleOutlineIcon from "@material-ui/icons/CheckCircleOutline";
+import HighlightOffIcon from "@material-ui/icons/HighlightOff";
+import { FormControlLabel, Switch } from "@material-ui/core";
+
+import { i18n } from "../../translate/i18n";
+import api from "../../services/api";
+import RecordingTimer from "./RecordingTimer";
+import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { useLocalStorage } from "../../hooks/useLocalStorage";
+import toastError from "../../errors/toastError";
+
+const Mp3Recorder = new MicRecorder({ bitRate: 128 });
+
+const useStyles = makeStyles(theme => ({
+	mainWrapper: {
+		backgroundColor: theme.palette.bordabox, //DARK MODE PLW DESIGN//
+		display: "flex",
+		flexDirection: "column",
+		alignItems: "center",
+		borderTop: "1px solid rgba(0, 0, 0, 0.12)",
+	},
+
+	newMessageBox: {
+		background: "#eee",
+		width: "100%",
+		display: "flex",
+		padding: "7px",
+		alignItems: "center",
+	},
+
+	messageInputWrapper: {
+		padding: 6,
+		marginRight: 7,
+		background: "#fff",
+		display: "flex",
+		borderRadius: 20,
+		flex: 1,
+	},
+
+	messageInput: {
+		paddingLeft: 10,
+		flex: 1,
+		border: "none",
+	},
+
+	sendMessageIcons: {
+		color: "grey",
+	},
+
+	uploadInput: {
+		display: "none",
+	},
+
+	viewMediaInputWrapper: {
+		display: "flex",
+		padding: "10px 13px",
+		position: "relative",
+		justifyContent: "space-between",
+		alignItems: "center",
+		backgroundColor: "#eee",
+		borderTop: "1px solid rgba(0, 0, 0, 0.12)",
+	},
+
+	emojiBox: {
+		position: "absolute",
+		bottom: 63,
+		width: 40,
+		borderTop: "1px solid #e8e8e8",
+	},
+
+	circleLoading: {
+		color: green[500],
+		opacity: "70%",
+		position: "absolute",
+		top: "20%",
+		left: "50%",
+		marginLeft: -12,
+	},
+
+	audioLoading: {
+		color: green[500],
+		opacity: "70%",
+	},
+
+	recorderWrapper: {
+		display: "flex",
+		alignItems: "center",
+		alignContent: "middle",
+	},
+
+	cancelAudioIcon: {
+		color: "red",
+	},
+
+	sendAudioIcon: {
+		color: "green",
+	},
+
+	replyginMsgWrapper: {
+		display: "flex",
+		width: "100%",
+		alignItems: "center",
+		justifyContent: "center",
+		paddingTop: 8,
+		paddingLeft: 73,
+		paddingRight: 7,
+	},
+
+	replyginMsgContainer: {
+		flex: 1,
+		marginRight: 5,
+		overflowY: "hidden",
+		backgroundColor: "rgba(0, 0, 0, 0.05)",
+		borderRadius: "7.5px",
+		display: "flex",
+		position: "relative",
+	},
+
+	replyginMsgBody: {
+		padding: 10,
+		height: "auto",
+		display: "block",
+		whiteSpace: "pre-wrap",
+		overflow: "hidden",
+	},
+
+	replyginContactMsgSideColor: {
+		flex: "none",
+		width: "4px",
+		backgroundColor: "#35cd96",
+	},
+
+	replyginSelfMsgSideColor: {
+		flex: "none",
+		width: "4px",
+		backgroundColor: "#6bcbef",
+	},
+
+	messageContactName: {
+		display: "flex",
+		color: "#6bcbef",
+		fontWeight: 500,
+	},
+}));
+
+const MessageInput = ({ ticketStatus }) => {
+	const classes = useStyles();
+	const { ticketId } = useParams();
+
+	const [medias, setMedias] = useState([]);
+	const [inputMessage, setInputMessage] = useState("");
+	const [showEmoji, setShowEmoji] = useState(false);
+	const [loading, setLoading] = useState(false);
+	const [recording, setRecording] = useState(false);
+	const inputRef = useRef();
+	const { setReplyingMessage, replyingMessage } = useContext(
+		ReplyMessageContext
+	);
+	const { user } = useContext(AuthContext);
+
+	const [signMessage, setSignMessage] = useLocalStorage("signOption", true);
+
+	useEffect(() => {
+		inputRef.current.focus();
+	}, [replyingMessage]);
+
+	useEffect(() => {
+		inputRef.current.focus();
+		return () => {
+			setInputMessage("");
+			setShowEmoji(false);
+			setMedias([]);
+			setReplyingMessage(null);
+		};
+	}, [ticketId, setReplyingMessage]);
+
+	const handleChangeInput = e => {
+		setInputMessage(e.target.value);
+	};
+
+	const handleAddEmoji = e => {
+		let emoji = e.native;
+		setInputMessage(prevState => prevState + emoji);
+	};
+
+	const handleChangeMedias = e => {
+		if (!e.target.files) {
+			return;
+		}
+
+		const selectedMedias = Array.from(e.target.files);
+		setMedias(selectedMedias);
+	};
+
+	const handleInputPaste = e => {
+		if (e.clipboardData.files[0]) {
+			setMedias([e.clipboardData.files[0]]);
+		}
+	};
+
+	const handleUploadMedia = async e => {
+		setLoading(true);
+		e.preventDefault();
+
+		const formData = new FormData();
+		formData.append("fromMe", true);
+		medias.forEach(media => {
+			formData.append("medias", media);
+			formData.append("body", media.name);
+		});
+
+		try {
+			await api.post(`/messages/${ticketId}`, formData);
+		} catch (err) {
+			toastError(err);
+		}
+
+		setLoading(false);
+		setMedias([]);
+	};
+
+	const handleSendMessage = async () => {
+		if (inputMessage.trim() === "") return;
+		setLoading(true);
+
+		const message = {
+			read: 1,
+			fromMe: true,
+			mediaUrl: "",
+			body: signMessage
+				? `*${user?.name}:*\n${inputMessage.trim()}`
+				: inputMessage.trim(),
+			quotedMsg: replyingMessage,
+		};
+		try {
+			await api.post(`/messages/${ticketId}`, message);
+		} catch (err) {
+			toastError(err);
+		}
+
+		setInputMessage("");
+		setShowEmoji(false);
+		setLoading(false);
+		setReplyingMessage(null);
+	};
+
+	const handleStartRecording = async () => {
+		setLoading(true);
+		try {
+			await navigator.mediaDevices.getUserMedia({ audio: true });
+			await Mp3Recorder.start();
+			setRecording(true);
+			setLoading(false);
+		} catch (err) {
+			toastError(err);
+			setLoading(false);
+		}
+	};
+
+	const handleUploadAudio = async () => {
+		setLoading(true);
+		try {
+			const [, blob] = await Mp3Recorder.stop().getMp3();
+			if (blob.size < 10000) {
+				setLoading(false);
+				setRecording(false);
+				return;
+			}
+
+			const formData = new FormData();
+			const filename = `${new Date().getTime()}.mp3`;
+			formData.append("medias", blob, filename);
+			formData.append("body", filename);
+			formData.append("fromMe", true);
+
+			await api.post(`/messages/${ticketId}`, formData);
+		} catch (err) {
+			toastError(err);
+		}
+
+		setRecording(false);
+		setLoading(false);
+	};
+
+	const handleCancelAudio = async () => {
+		try {
+			await Mp3Recorder.stop().getMp3();
+			setRecording(false);
+		} catch (err) {
+			toastError(err);
+		}
+	};
+
+	const renderReplyingMessage = message => {
+		return (
+			<div className={classes.replyginMsgWrapper}>
+				<div className={classes.replyginMsgContainer}>
+					<span
+						className={clsx(classes.replyginContactMsgSideColor, {
+							[classes.replyginSelfMsgSideColor]: !message.fromMe,
+						})}
+					></span>
+					<div className={classes.replyginMsgBody}>
+						{!message.fromMe && (
+							<span className={classes.messageContactName}>
+								{message.contact?.name}
+							</span>
+						)}
+						{message.body}
+					</div>
+				</div>
+				<IconButton
+					aria-label="showRecorder"
+					component="span"
+					disabled={loading || ticketStatus !== "open"}
+					onClick={() => setReplyingMessage(null)}
+				>
+					<ClearIcon className={classes.sendMessageIcons} />
+				</IconButton>
+			</div>
+		);
+	};
+
+	if (medias.length > 0)
+		return (
+			<Paper elevation={0} square className={classes.viewMediaInputWrapper}>
+				<IconButton
+					aria-label="cancel-upload"
+					component="span"
+					onClick={e => setMedias([])}
+				>
+					<CancelIcon className={classes.sendMessageIcons} />
+				</IconButton>
+
+				{loading ? (
+					<div>
+						<CircularProgress className={classes.circleLoading} />
+					</div>
+				) : (
+					<span>
+						{medias[0]?.name}
+						{/* <img src={media.preview} alt=""></img> */}
+					</span>
+				)}
+				<IconButton
+					aria-label="send-upload"
+					component="span"
+					onClick={handleUploadMedia}
+					disabled={loading}
+				>
+					<SendIcon className={classes.sendMessageIcons} />
+				</IconButton>
+			</Paper>
+		);
+	else {
+		return (
+			<Paper square elevation={0} className={classes.mainWrapper}>
+				{replyingMessage && renderReplyingMessage(replyingMessage)}
+				<div className={classes.newMessageBox}>
+					<IconButton
+						aria-label="emojiPicker"
+						component="span"
+						disabled={loading || recording || ticketStatus !== "open"}
+						onClick={e => setShowEmoji(prevState => !prevState)}
+					>
+						<MoodIcon className={classes.sendMessageIcons} />
+					</IconButton>
+					{showEmoji ? (
+						<div className={classes.emojiBox}>
+							<Picker
+								perLine={16}
+								showPreview={false}
+								showSkinTones={false}
+								onSelect={handleAddEmoji}
+							/>
+						</div>
+					) : null}
+
+					<input
+						multiple
+						type="file"
+						id="upload-button"
+						disabled={loading || recording || ticketStatus !== "open"}
+						className={classes.uploadInput}
+						onChange={handleChangeMedias}
+					/>
+					<label htmlFor="upload-button">
+						<IconButton
+							aria-label="upload"
+							component="span"
+							disabled={loading || recording || ticketStatus !== "open"}
+						>
+							<AttachFileIcon className={classes.sendMessageIcons} />
+						</IconButton>
+					</label>
+					<FormControlLabel
+						style={{ marginRight: 7, color: "gray" }}
+						label={i18n.t("messagesInput.signMessage")}
+						labelPlacement="start"
+						control={
+							<Switch
+								size="small"
+								checked={signMessage}
+								onChange={e => {
+									setSignMessage(e.target.checked);
+								}}
+								name="showAllTickets"
+								color="primary"
+							/>
+						}
+					/>
+					<div className={classes.messageInputWrapper}>
+						<InputBase
+							inputRef={input => {
+								input && input.focus();
+								input && (inputRef.current = input);
+							}}
+							className={classes.messageInput}
+							placeholder={
+								ticketStatus === "open"
+									? i18n.t("messagesInput.placeholderOpen")
+									: i18n.t("messagesInput.placeholderClosed")
+							}
+							multiline
+							maxRows={5}
+							value={inputMessage}
+							onChange={handleChangeInput}
+							disabled={recording || loading || ticketStatus !== "open"}
+							onPaste={e => {
+								ticketStatus === "open" && handleInputPaste(e);
+							}}
+							onKeyPress={e => {
+								if (loading || e.shiftKey) return;
+								else if (e.key === "Enter") {
+									handleSendMessage();
+								}
+							}}
+						/>
+					</div>
+					{inputMessage ? (
+						<IconButton
+							aria-label="sendMessage"
+							component="span"
+							onClick={handleSendMessage}
+							disabled={loading}
+						>
+							<SendIcon className={classes.sendMessageIcons} />
+						</IconButton>
+					) : recording ? (
+						<div className={classes.recorderWrapper}>
+							<IconButton
+								aria-label="cancelRecording"
+								component="span"
+								fontSize="large"
+								disabled={loading}
+								onClick={handleCancelAudio}
+							>
+								<HighlightOffIcon className={classes.cancelAudioIcon} />
+							</IconButton>
+							{loading ? (
+								<div>
+									<CircularProgress className={classes.audioLoading} />
+								</div>
+							) : (
+								<RecordingTimer />
+							)}
+
+							<IconButton
+								aria-label="sendRecordedAudio"
+								component="span"
+								onClick={handleUploadAudio}
+								disabled={loading}
+							>
+								<CheckCircleOutlineIcon className={classes.sendAudioIcon} />
+							</IconButton>
+						</div>
+					) : (
+						<IconButton
+							aria-label="showRecorder"
+							component="span"
+							disabled={loading || ticketStatus !== "open"}
+							onClick={handleStartRecording}
+						>
+							<MicIcon className={classes.sendMessageIcons} />
+						</IconButton>
+					)}
+				</div>
+			</Paper>
+		);
+	}
+};
+
+export default MessageInput;

+ 48 - 0
frontend/src/components/MessageInputCustom/RecordingTimer.js

@@ -0,0 +1,48 @@
+import React, { useState, useEffect } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+
+const useStyles = makeStyles(theme => ({
+	timerBox: {
+		display: "flex",
+		marginLeft: 10,
+		marginRight: 10,
+		alignItems: "center",
+	},
+}));
+
+const RecordingTimer = () => {
+	const classes = useStyles();
+	const initialState = {
+		minutes: 0,
+		seconds: 0,
+	};
+	const [timer, setTimer] = useState(initialState);
+
+	useEffect(() => {
+		const interval = setInterval(
+			() =>
+				setTimer(prevState => {
+					if (prevState.seconds === 59) {
+						return { ...prevState, minutes: prevState.minutes + 1, seconds: 0 };
+					}
+					return { ...prevState, seconds: prevState.seconds + 1 };
+				}),
+			1000
+		);
+		return () => {
+			clearInterval(interval);
+		};
+	}, []);
+
+	const addZero = n => {
+		return n < 10 ? "0" + n : n;
+	};
+
+	return (
+		<div className={classes.timerBox}>
+			<span>{`${addZero(timer.minutes)}:${addZero(timer.seconds)}`}</span>
+		</div>
+	);
+};
+
+export default RecordingTimer;

+ 773 - 0
frontend/src/components/MessageInputCustom/index.js

@@ -0,0 +1,773 @@
+import React, { useState, useEffect, useContext, useRef } from "react";
+import withWidth, { isWidthUp } from "@material-ui/core/withWidth";
+import "emoji-mart/css/emoji-mart.css";
+import { Picker } from "emoji-mart";
+import MicRecorder from "mic-recorder-to-mp3";
+import clsx from "clsx";
+import { isNil } from "lodash";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+import InputBase from "@material-ui/core/InputBase";
+import CircularProgress from "@material-ui/core/CircularProgress";
+import { green } from "@material-ui/core/colors";
+import AttachFileIcon from "@material-ui/icons/AttachFile";
+import IconButton from "@material-ui/core/IconButton";
+import MoodIcon from "@material-ui/icons/Mood";
+import SendIcon from "@material-ui/icons/Send";
+import CancelIcon from "@material-ui/icons/Cancel";
+import ClearIcon from "@material-ui/icons/Clear";
+import MicIcon from "@material-ui/icons/Mic";
+import CheckCircleOutlineIcon from "@material-ui/icons/CheckCircleOutline";
+import HighlightOffIcon from "@material-ui/icons/HighlightOff";
+import { FormControlLabel, Switch } from "@material-ui/core";
+import Autocomplete from "@material-ui/lab/Autocomplete";
+import { isString, isEmpty, isObject, has } from "lodash";
+
+import { i18n } from "../../translate/i18n";
+import api from "../../services/api";
+import axios from "axios";
+
+import RecordingTimer from "./RecordingTimer";
+import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { useLocalStorage } from "../../hooks/useLocalStorage";
+import toastError from "../../errors/toastError";
+
+import useQuickMessages from "../../hooks/useQuickMessages";
+
+const Mp3Recorder = new MicRecorder({ bitRate: 128 });
+
+const useStyles = makeStyles((theme) => ({
+  mainWrapper: {
+    backgroundColor: theme.palette.bordabox, //DARK MODE PLW DESIGN//
+    display: "flex",
+    flexDirection: "column",
+    alignItems: "center",
+    borderTop: "1px solid rgba(0, 0, 0, 0.12)",
+  },
+
+  newMessageBox: {
+    backgroundColor: theme.palette.newmessagebox, //DARK MODE PLW DESIGN//
+    width: "100%",
+    display: "flex",
+    padding: "7px",
+    alignItems: "center",
+  },
+
+  messageInputWrapper: {
+    padding: 6,
+    marginRight: 7,
+    backgroundColor: theme.palette.inputdigita, //DARK MODE PLW DESIGN//
+    display: "flex",
+    borderRadius: 20,
+    flex: 1,
+  },
+
+  messageInput: {
+    paddingLeft: 10,
+    flex: 1,
+    border: "none",
+  },
+
+  sendMessageIcons: {
+    color: "grey",
+  },
+
+  uploadInput: {
+    display: "none",
+  },
+
+  viewMediaInputWrapper: {
+    display: "flex",
+    padding: "10px 13px",
+    position: "relative",
+    justifyContent: "space-between",
+    alignItems: "center",
+    backgroundColor: "#eee",
+    borderTop: "1px solid rgba(0, 0, 0, 0.12)",
+  },
+
+  emojiBox: {
+    position: "absolute",
+    bottom: 63,
+    width: 40,
+    borderTop: "1px solid #e8e8e8",
+  },
+
+  circleLoading: {
+    color: green[500],
+    opacity: "70%",
+    position: "absolute",
+    top: "20%",
+    left: "50%",
+    marginLeft: -12,
+  },
+
+  audioLoading: {
+    color: green[500],
+    opacity: "70%",
+  },
+
+  recorderWrapper: {
+    display: "flex",
+    alignItems: "center",
+    alignContent: "middle",
+  },
+
+  cancelAudioIcon: {
+    color: "red",
+  },
+
+  sendAudioIcon: {
+    color: "green",
+  },
+
+  replyginMsgWrapper: {
+    display: "flex",
+    width: "100%",
+    alignItems: "center",
+    justifyContent: "center",
+    paddingTop: 8,
+    paddingLeft: 73,
+    paddingRight: 7,
+  },
+
+  replyginMsgContainer: {
+    flex: 1,
+    marginRight: 5,
+    overflowY: "hidden",
+    backgroundColor: "rgba(0, 0, 0, 0.05)",
+    borderRadius: "7.5px",
+    display: "flex",
+    position: "relative",
+  },
+
+  replyginMsgBody: {
+    padding: 10,
+    height: "auto",
+    display: "block",
+    whiteSpace: "pre-wrap",
+    overflow: "hidden",
+  },
+
+  replyginContactMsgSideColor: {
+    flex: "none",
+    width: "4px",
+    backgroundColor: "#35cd96",
+  },
+
+  replyginSelfMsgSideColor: {
+    flex: "none",
+    width: "4px",
+    backgroundColor: "#6bcbef",
+  },
+
+  messageContactName: {
+    display: "flex",
+    color: "#6bcbef",
+    fontWeight: 500,
+  },
+}));
+
+const EmojiOptions = (props) => {
+  const { disabled, showEmoji, setShowEmoji, handleAddEmoji } = props;
+  const classes = useStyles();
+  return (
+    <>
+      <IconButton
+        aria-label="emojiPicker"
+        component="span"
+        disabled={disabled}
+        onClick={(e) => setShowEmoji((prevState) => !prevState)}
+      >
+        <MoodIcon className={classes.sendMessageIcons} />
+      </IconButton>
+      {showEmoji ? (
+        <div className={classes.emojiBox}>
+          <Picker
+            perLine={16}
+            showPreview={false}
+            showSkinTones={false}
+            onSelect={handleAddEmoji}
+          />
+        </div>
+      ) : null}
+    </>
+  );
+};
+
+const SignSwitch = (props) => {
+  const { width, setSignMessage, signMessage } = props;
+  if (isWidthUp("md", width)) {
+    return (
+      <FormControlLabel
+        style={{ marginRight: 7, color: "gray" }}
+        label={i18n.t("messagesInput.signMessage")}
+        labelPlacement="start"
+        control={
+          <Switch
+            size="small"
+            checked={signMessage}
+            onChange={(e) => {
+              setSignMessage(e.target.checked);
+            }}
+            name="showAllTickets"
+            color="primary"
+          />
+        }
+      />
+    );
+  }
+  return null;
+};
+
+const FileInput = (props) => {
+  const { handleChangeMedias, disableOption } = props;
+  const classes = useStyles();
+  return (
+    <>
+      <input
+        multiple
+        type="file"
+        id="upload-button"
+        disabled={disableOption()}
+        className={classes.uploadInput}
+        onChange={handleChangeMedias}
+      />
+      <label htmlFor="upload-button">
+        <IconButton
+          aria-label="upload"
+          component="span"
+          disabled={disableOption()}
+        >
+          <AttachFileIcon className={classes.sendMessageIcons} />
+        </IconButton>
+      </label>
+    </>
+  );
+};
+
+const ActionButtons = (props) => {
+  const {
+    inputMessage,
+    loading,
+    recording,
+    ticketStatus,
+    handleSendMessage,
+    handleCancelAudio,
+    handleUploadAudio,
+    handleStartRecording,
+  } = props;
+  const classes = useStyles();
+  if (inputMessage) {
+    return (
+      <IconButton
+        aria-label="sendMessage"
+        component="span"
+        onClick={handleSendMessage}
+        disabled={loading}
+      >
+        <SendIcon className={classes.sendMessageIcons} />
+      </IconButton>
+    );
+  } else if (recording) {
+    return (
+      <div className={classes.recorderWrapper}>
+        <IconButton
+          aria-label="cancelRecording"
+          component="span"
+          fontSize="large"
+          disabled={loading}
+          onClick={handleCancelAudio}
+        >
+          <HighlightOffIcon className={classes.cancelAudioIcon} />
+        </IconButton>
+        {loading ? (
+          <div>
+            <CircularProgress className={classes.audioLoading} />
+          </div>
+        ) : (
+          <RecordingTimer />
+        )}
+
+        <IconButton
+          aria-label="sendRecordedAudio"
+          component="span"
+          onClick={handleUploadAudio}
+          disabled={loading}
+        >
+          <CheckCircleOutlineIcon className={classes.sendAudioIcon} />
+        </IconButton>
+      </div>
+    );
+  } else {
+    return (
+      <IconButton
+        aria-label="showRecorder"
+        component="span"
+        disabled={loading || ticketStatus !== "open"}
+        onClick={handleStartRecording}
+      >
+        <MicIcon className={classes.sendMessageIcons} />
+      </IconButton>
+    );
+  }
+};
+
+const CustomInput = (props) => {
+  const {
+    loading,
+    inputRef,
+    ticketStatus,
+    inputMessage,
+    setInputMessage,
+    handleSendMessage,
+    handleInputPaste,
+    disableOption,
+    handleQuickAnswersClick,
+  } = props;
+  const classes = useStyles();
+  const [quickMessages, setQuickMessages] = useState([]);
+  const [options, setOptions] = useState([]);
+  const [popupOpen, setPopupOpen] = useState(false);
+
+  const { user } = useContext(AuthContext);
+
+  const { list: listQuickMessages } = useQuickMessages();
+
+  useEffect(() => {
+    async function fetchData() {
+      const companyId = localStorage.getItem("companyId");
+      const messages = await listQuickMessages({ companyId, userId: user.id });
+      const options = messages.map((m) => {
+        let truncatedMessage = m.message;
+        if (isString(truncatedMessage) && truncatedMessage.length > 35) {
+          truncatedMessage = m.message.substring(0, 35) + "...";
+        }
+        return {
+          value: m.message,
+          label: `/${m.shortcode} - ${truncatedMessage}`,
+          mediaPath: m.mediaPath,
+        };
+      });
+      setQuickMessages(options);
+    }
+    fetchData();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  useEffect(() => {
+    if (
+      isString(inputMessage) &&
+      !isEmpty(inputMessage) &&
+      inputMessage.length > 1
+    ) {
+      const firstWord = inputMessage.charAt(0);
+      setPopupOpen(firstWord.indexOf("/") > -1);
+
+      const filteredOptions = quickMessages.filter(
+        (m) => m.label.indexOf(inputMessage) > -1
+      );
+      setOptions(filteredOptions);
+    } else {
+      setPopupOpen(false);
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [inputMessage]);
+
+  const onKeyPress = (e) => {
+    if (loading || e.shiftKey) return;
+    else if (e.key === "Enter") {
+      handleSendMessage();
+    }
+  };
+
+  const onPaste = (e) => {
+    if (ticketStatus === "open") {
+      handleInputPaste(e);
+    }
+  };
+
+  const renderPlaceholder = () => {
+    if (ticketStatus === "open") {
+      return i18n.t("messagesInput.placeholderOpen");
+    }
+    return i18n.t("messagesInput.placeholderClosed");
+  };
+
+
+  const setInputRef = (input) => {
+    if (input) {
+      input.focus();
+      inputRef.current = input;
+    }
+  };
+
+  return (
+    <div className={classes.messageInputWrapper}>
+      <Autocomplete
+        freeSolo
+        open={popupOpen}
+        id="grouped-demo"
+        value={inputMessage}
+        options={options}
+        closeIcon={null}
+        getOptionLabel={(option) => {
+          if (isObject(option)) {
+            return option.label;
+          } else {
+            return option;
+          }
+        }}
+        onChange={(event, opt) => {
+         
+          if (isObject(opt) && has(opt, "value") && isNil(opt.mediaPath)) {
+            setInputMessage(opt.value);
+            setTimeout(() => {
+              inputRef.current.scrollTop = inputRef.current.scrollHeight;
+            }, 200);
+          } else if (isObject(opt) && has(opt, "value") && !isNil(opt.mediaPath)) {
+            handleQuickAnswersClick(opt);
+
+            setTimeout(() => {
+              inputRef.current.scrollTop = inputRef.current.scrollHeight;
+            }, 200);
+          }
+        }}
+        onInputChange={(event, opt, reason) => {
+          if (reason === "input") {
+            setInputMessage(event.target.value);
+          }
+        }}
+        onPaste={onPaste}
+        onKeyPress={onKeyPress}
+        style={{ width: "100%" }}
+        renderInput={(params) => {
+          const { InputLabelProps, InputProps, ...rest } = params;
+          return (
+            <InputBase
+              {...params.InputProps}
+              {...rest}
+              disabled={disableOption()}
+              inputRef={setInputRef}
+              placeholder={renderPlaceholder()}
+              multiline
+              className={classes.messageInput}
+              maxRows={5}
+            />
+          );
+        }}
+      />
+    </div>
+  );
+};
+
+const MessageInputCustom = (props) => {
+  const { ticketStatus, ticketId } = props;
+  const classes = useStyles();
+
+  const [medias, setMedias] = useState([]);
+  const [inputMessage, setInputMessage] = useState("");
+  const [showEmoji, setShowEmoji] = useState(false);
+  const [loading, setLoading] = useState(false);
+  const [recording, setRecording] = useState(false);
+  const inputRef = useRef();
+  const { setReplyingMessage, replyingMessage } =
+    useContext(ReplyMessageContext);
+  const { user } = useContext(AuthContext);
+
+  const [signMessage, setSignMessage] = useLocalStorage("signOption", true);
+
+  useEffect(() => {
+    inputRef.current.focus();
+  }, [replyingMessage]);
+
+  useEffect(() => {
+    inputRef.current.focus();
+    return () => {
+      setInputMessage("");
+      setShowEmoji(false);
+      setMedias([]);
+      setReplyingMessage(null);
+    };
+  }, [ticketId, setReplyingMessage]);
+
+  // const handleChangeInput = e => {
+  // 	if (isObject(e) && has(e, 'value')) {
+  // 		setInputMessage(e.value);
+  // 	} else {
+  // 		setInputMessage(e.target.value)
+  // 	}
+  // };
+
+  const handleAddEmoji = (e) => {
+    let emoji = e.native;
+    setInputMessage((prevState) => prevState + emoji);
+  };
+
+  const handleChangeMedias = (e) => {
+    if (!e.target.files) {
+      return;
+    }
+
+    const selectedMedias = Array.from(e.target.files);
+    setMedias(selectedMedias);
+  };
+
+  const handleInputPaste = (e) => {
+    if (e.clipboardData.files[0]) {
+      setMedias([e.clipboardData.files[0]]);
+    }
+  };
+
+  const handleUploadQuickMessageMedia = async (blob, message) => {
+    setLoading(true);
+    try {
+      const extension = blob.type.split("/")[1];
+
+      const formData = new FormData();
+      const filename = `${new Date().getTime()}.${extension}`;
+      formData.append("medias", blob, filename);
+      formData.append("body",  message);
+      formData.append("fromMe", true);
+
+      await api.post(`/messages/${ticketId}`, formData);
+    } catch (err) {
+      toastError(err);
+      setLoading(false);
+    }
+    setLoading(false);
+  };
+  
+  const handleQuickAnswersClick = async (value) => {
+    if (value.mediaPath) {
+      try {
+        const { data } = await axios.get(value.mediaPath, {
+          responseType: "blob",
+        });
+
+        handleUploadQuickMessageMedia(data, value.value);
+        setInputMessage("");
+        return;
+        //  handleChangeMedias(response)
+      } catch (err) {
+        toastError(err);
+      }
+    }
+
+    setInputMessage("");
+    setInputMessage(value.value);
+  };
+
+  const handleUploadMedia = async (e) => {
+    setLoading(true);
+    e.preventDefault();
+
+    const formData = new FormData();
+    formData.append("fromMe", true);
+    medias.forEach((media) => {
+      formData.append("medias", media);
+      formData.append("body", media.name);
+    });
+
+    try {
+      await api.post(`/messages/${ticketId}`, formData);
+    } catch (err) {
+      toastError(err);
+    }
+
+    setLoading(false);
+    setMedias([]);
+  };
+
+  const handleSendMessage = async () => {
+    if (inputMessage.trim() === "") return;
+    setLoading(true);
+
+    const message = {
+      read: 1,
+      fromMe: true,
+      mediaUrl: "",
+      body: signMessage
+        ? `*${user?.name}:*\n${inputMessage.trim()}`
+        : inputMessage.trim(),
+      quotedMsg: replyingMessage,
+    };
+    try {
+      await api.post(`/messages/${ticketId}`, message);
+    } catch (err) {
+      toastError(err);
+    }
+
+    setInputMessage("");
+    setShowEmoji(false);
+    setLoading(false);
+    setReplyingMessage(null);
+  };
+
+  const handleStartRecording = async () => {
+    setLoading(true);
+    try {
+      await navigator.mediaDevices.getUserMedia({ audio: true });
+      await Mp3Recorder.start();
+      setRecording(true);
+      setLoading(false);
+    } catch (err) {
+      toastError(err);
+      setLoading(false);
+    }
+  };
+
+  const handleUploadAudio = async () => {
+    setLoading(true);
+    try {
+      const [, blob] = await Mp3Recorder.stop().getMp3();
+      if (blob.size < 10000) {
+        setLoading(false);
+        setRecording(false);
+        return;
+      }
+
+      const formData = new FormData();
+      const filename = `audio-record-site-${new Date().getTime()}.mp3`;
+      formData.append("medias", blob, filename);
+      formData.append("body", filename);
+      formData.append("fromMe", true);
+
+      await api.post(`/messages/${ticketId}`, formData);
+    } catch (err) {
+      toastError(err);
+    }
+
+    setRecording(false);
+    setLoading(false);
+  };
+
+  const handleCancelAudio = async () => {
+    try {
+      await Mp3Recorder.stop().getMp3();
+      setRecording(false);
+    } catch (err) {
+      toastError(err);
+    }
+  };
+
+  const disableOption = () => {
+    return loading || recording || ticketStatus !== "open";
+  };
+
+  const renderReplyingMessage = (message) => {
+    return (
+      <div className={classes.replyginMsgWrapper}>
+        <div className={classes.replyginMsgContainer}>
+          <span
+            className={clsx(classes.replyginContactMsgSideColor, {
+              [classes.replyginSelfMsgSideColor]: !message.fromMe,
+            })}
+          ></span>
+          <div className={classes.replyginMsgBody}>
+            {!message.fromMe && (
+              <span className={classes.messageContactName}>
+                {message.contact?.name}
+              </span>
+            )}
+            {message.body}
+          </div>
+        </div>
+        <IconButton
+          aria-label="showRecorder"
+          component="span"
+          disabled={loading || ticketStatus !== "open"}
+          onClick={() => setReplyingMessage(null)}
+        >
+          <ClearIcon className={classes.sendMessageIcons} />
+        </IconButton>
+      </div>
+    );
+  };
+
+  if (medias.length > 0)
+    return (
+      <Paper elevation={0} square className={classes.viewMediaInputWrapper}>
+        <IconButton
+          aria-label="cancel-upload"
+          component="span"
+          onClick={(e) => setMedias([])}
+        >
+          <CancelIcon className={classes.sendMessageIcons} />
+        </IconButton>
+
+        {loading ? (
+          <div>
+            <CircularProgress className={classes.circleLoading} />
+          </div>
+        ) : (
+          <span>
+            {medias[0]?.name}
+            {/* <img src={media.preview} alt=""></img> */}
+          </span>
+        )}
+        <IconButton
+          aria-label="send-upload"
+          component="span"
+          onClick={handleUploadMedia}
+          disabled={loading}
+        >
+          <SendIcon className={classes.sendMessageIcons} />
+        </IconButton>
+      </Paper>
+    );
+  else {
+    return (
+      <Paper square elevation={0} className={classes.mainWrapper}>
+        {replyingMessage && renderReplyingMessage(replyingMessage)}
+        <div className={classes.newMessageBox}>
+          <EmojiOptions
+            disabled={disableOption()}
+            handleAddEmoji={handleAddEmoji}
+            showEmoji={showEmoji}
+            setShowEmoji={setShowEmoji}
+          />
+
+          <FileInput
+            disableOption={disableOption}
+            handleChangeMedias={handleChangeMedias}
+          />
+
+          <SignSwitch
+            width={props.width}
+            setSignMessage={setSignMessage}
+            signMessage={signMessage}
+          />
+
+          <CustomInput
+            loading={loading}
+            inputRef={inputRef}
+            ticketStatus={ticketStatus}
+            inputMessage={inputMessage}
+            setInputMessage={setInputMessage}
+            // handleChangeInput={handleChangeInput}
+            handleSendMessage={handleSendMessage}
+            handleInputPaste={handleInputPaste}
+            disableOption={disableOption}
+            handleQuickAnswersClick={handleQuickAnswersClick}
+          />
+
+          <ActionButtons
+            inputMessage={inputMessage}
+            loading={loading}
+            recording={recording}
+            ticketStatus={ticketStatus}
+            handleSendMessage={handleSendMessage}
+            handleCancelAudio={handleCancelAudio}
+            handleUploadAudio={handleUploadAudio}
+            handleStartRecording={handleStartRecording}
+          />
+        </div>
+      </Paper>
+    );
+  }
+};
+
+export default withWidth()(MessageInputCustom);

+ 71 - 0
frontend/src/components/MessageOptionsMenu/index.js

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

+ 66 - 0
frontend/src/components/MessageVariablesPicker/index.js

@@ -0,0 +1,66 @@
+import React from "react";
+import { Chip, makeStyles } from "@material-ui/core";
+import { i18n } from "../../translate/i18n";
+import OutlinedDiv from "../OutlinedDiv";
+
+const useStyles = makeStyles(theme => ({
+    chip: {
+        margin: theme.spacing(0.5),
+        cursor: "pointer"
+    }
+}));
+
+const MessageVariablesPicker = ({ onClick, disabled }) => {
+    const classes = useStyles();
+
+    const handleClick = (e, value) => {
+        e.preventDefault();
+        if (disabled) return;
+        onClick(value);
+    };
+
+    const msgVars = [
+        {
+            name: i18n.t("messageVariablesPicker.vars.contactFirstName"),
+            value: "{{firstName}}"
+        },
+        {
+            name: i18n.t("messageVariablesPicker.vars.contactName"),
+            value: "{{name}} "
+        },
+        {
+            name: i18n.t("messageVariablesPicker.vars.greeting"),
+            value: "{{ms}} "
+        },
+        {
+            name: i18n.t("messageVariablesPicker.vars.protocolNumber"),
+            value: "{{protocol}} "
+        },
+        {
+            name: i18n.t("messageVariablesPicker.vars.hour"),
+            value: "{{hora}} "
+        },
+    ];
+
+    return (
+        <OutlinedDiv
+            margin="dense"
+            fullWidth
+            label={i18n.t("messageVariablesPicker.label")}
+            disabled={disabled}
+        >
+            {msgVars.map(msgVar => (
+                <Chip
+                    key={msgVar.value}
+                    onMouseDown={e => handleClick(e, msgVar.value)}
+                    label={msgVar.name}
+                    size="small"
+                    className={classes.chip}
+                    color="primary"
+                />
+            ))}
+        </OutlinedDiv>
+    );
+};
+
+export default MessageVariablesPicker;

+ 837 - 0
frontend/src/components/MessagesList/index.js

@@ -0,0 +1,837 @@
+import React, { useState, useEffect, useReducer, useRef, useContext } from "react";
+
+import { isSameDay, parseISO, format } from "date-fns";
+import clsx from "clsx";
+
+import { green } from "@material-ui/core/colors";
+import {
+  Button,
+  CircularProgress,
+  Divider,
+  IconButton,
+  makeStyles,
+} from "@material-ui/core";
+
+import {
+  AccessTime,
+  Block,
+  Done,
+  DoneAll,
+  ExpandMore,
+  GetApp,
+} from "@material-ui/icons";
+
+import MarkdownWrapper from "../MarkdownWrapper";
+import ModalImageCors from "../ModalImageCors";
+import MessageOptionsMenu from "../MessageOptionsMenu";
+import whatsBackground from "../../assets/wa-background.png";
+import LocationPreview from "../LocationPreview";
+
+import whatsBackgroundDark from "../../assets/wa-background-dark.png"; //DARK MODE PLW DESIGN//
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+import { SocketContext } from "../../context/Socket/SocketContext";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles((theme) => ({
+  messagesListWrapper: {
+    overflow: "hidden",
+    position: "relative",
+    display: "flex",
+    flexDirection: "column",
+    flexGrow: 1,
+    width: "100%",
+    minWidth: 300,
+    minHeight: 200,
+  },
+
+  messagesList: {
+    backgroundImage: theme.mode === 'light' ? `url(${whatsBackground})` : `url(${whatsBackgroundDark})`, //DARK MODE PLW DESIGN//
+    display: "flex",
+    flexDirection: "column",
+    flexGrow: 1,
+    padding: "20px 20px 20px 20px",
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+
+  circleLoading: {
+    color: green[500],
+    position: "absolute",
+    opacity: "70%",
+    top: 0,
+    left: "50%",
+    marginTop: 12,
+  },
+
+  messageLeft: {
+    marginRight: 20,
+    marginTop: 2,
+    minWidth: 100,
+    maxWidth: 600,
+    height: "auto",
+    display: "block",
+    position: "relative",
+    "&:hover #messageActionsButton": {
+      display: "flex",
+      position: "absolute",
+      top: 0,
+      right: 0,
+    },
+
+    whiteSpace: "pre-wrap",
+    backgroundColor: "#ffffff",
+    color: "#303030",
+    alignSelf: "flex-start",
+    borderTopLeftRadius: 0,
+    borderTopRightRadius: 8,
+    borderBottomLeftRadius: 8,
+    borderBottomRightRadius: 8,
+    paddingLeft: 5,
+    paddingRight: 5,
+    paddingTop: 5,
+    paddingBottom: 0,
+    boxShadow: "0 1px 1px #b3b3b3",
+  },
+
+  quotedContainerLeft: {
+    margin: "-3px -80px 6px -6px",
+    overflow: "hidden",
+    backgroundColor: "#f0f0f0",
+    borderRadius: "7.5px",
+    display: "flex",
+    position: "relative",
+  },
+
+  quotedMsg: {
+    padding: 10,
+    maxWidth: 300,
+    height: "auto",
+    display: "block",
+    whiteSpace: "pre-wrap",
+    overflow: "hidden",
+  },
+
+  quotedSideColorLeft: {
+    flex: "none",
+    width: "4px",
+    backgroundColor: "#6bcbef",
+  },
+
+  messageRight: {
+    marginLeft: 20,
+    marginTop: 2,
+    minWidth: 100,
+    maxWidth: 600,
+    height: "auto",
+    display: "block",
+    position: "relative",
+    "&:hover #messageActionsButton": {
+      display: "flex",
+      position: "absolute",
+      top: 0,
+      right: 0,
+    },
+
+    whiteSpace: "pre-wrap",
+    backgroundColor: "#dcf8c6",
+    color: "#303030",
+    alignSelf: "flex-end",
+    borderTopLeftRadius: 8,
+    borderTopRightRadius: 8,
+    borderBottomLeftRadius: 8,
+    borderBottomRightRadius: 0,
+    paddingLeft: 5,
+    paddingRight: 5,
+    paddingTop: 5,
+    paddingBottom: 0,
+    boxShadow: "0 1px 1px #b3b3b3",
+  },
+
+  quotedContainerRight: {
+    margin: "-3px -80px 6px -6px",
+    overflowY: "hidden",
+    backgroundColor: "#cfe9ba",
+    borderRadius: "7.5px",
+    display: "flex",
+    position: "relative",
+  },
+
+  quotedMsgRight: {
+    padding: 10,
+    maxWidth: 300,
+    height: "auto",
+    whiteSpace: "pre-wrap",
+  },
+
+  quotedSideColorRight: {
+    flex: "none",
+    width: "4px",
+    backgroundColor: "#35cd96",
+  },
+
+  messageActionsButton: {
+    display: "none",
+    position: "relative",
+    color: "#999",
+    zIndex: 1,
+    backgroundColor: "inherit",
+    opacity: "90%",
+    "&:hover, &.Mui-focusVisible": { backgroundColor: "inherit" },
+  },
+
+  messageContactName: {
+    display: "flex",
+    color: "#6bcbef",
+    fontWeight: 500,
+  },
+
+  textContentItem: {
+    overflowWrap: "break-word",
+    padding: "3px 80px 6px 6px",
+  },
+  
+  textContentItemEdited: {
+    overflowWrap: "break-word",
+    padding: "3px 120px 6px 6px",
+  },
+
+  textContentItemDeleted: {
+    fontStyle: "italic",
+    color: "rgba(0, 0, 0, 0.36)",
+    overflowWrap: "break-word",
+    padding: "3px 80px 6px 6px",
+  },
+
+  messageMedia: {
+    objectFit: "cover",
+    width: 250,
+    height: 200,
+    borderTopLeftRadius: 8,
+    borderTopRightRadius: 8,
+    borderBottomLeftRadius: 8,
+    borderBottomRightRadius: 8,
+  },
+
+  timestamp: {
+    fontSize: 11,
+    position: "absolute",
+    bottom: 0,
+    right: 5,
+    color: "#999",
+  },
+
+  dailyTimestamp: {
+    alignItems: "center",
+    textAlign: "center",
+    alignSelf: "center",
+    width: "110px",
+    backgroundColor: "#e1f3fb",
+    margin: "10px",
+    borderRadius: "10px",
+    boxShadow: "0 1px 1px #b3b3b3",
+  },
+
+  dailyTimestampText: {
+    color: "#808888",
+    padding: 8,
+    alignSelf: "center",
+    marginLeft: "0px",
+  },
+
+  ackIcons: {
+    fontSize: 18,
+    verticalAlign: "middle",
+    marginLeft: 4,
+  },
+
+  deletedIcon: {
+    fontSize: 18,
+    verticalAlign: "middle",
+    marginRight: 4,
+  },
+
+  ackDoneAllIcon: {
+    color: green[500],
+    fontSize: 18,
+    verticalAlign: "middle",
+    marginLeft: 4,
+  },
+
+  downloadMedia: {
+    display: "flex",
+    alignItems: "center",
+    justifyContent: "center",
+    backgroundColor: "inherit",
+    padding: 10,
+  },
+}));
+
+const reducer = (state, action) => {
+  if (action.type === "LOAD_MESSAGES") {
+    const messages = action.payload;
+    const newMessages = [];
+
+    messages.forEach((message) => {
+      const messageIndex = state.findIndex((m) => m.id === message.id);
+      if (messageIndex !== -1) {
+        state[messageIndex] = message;
+      } else {
+        newMessages.push(message);
+      }
+    });
+
+    return [...newMessages, ...state];
+  }
+
+  if (action.type === "ADD_MESSAGE") {
+    const newMessage = action.payload;
+    const messageIndex = state.findIndex((m) => m.id === newMessage.id);
+
+    if (messageIndex !== -1) {
+      state[messageIndex] = newMessage;
+    } else {
+      state.push(newMessage);
+    }
+
+    return [...state];
+  }
+
+  if (action.type === "UPDATE_MESSAGE") {
+    const messageToUpdate = action.payload;
+    const messageIndex = state.findIndex((m) => m.id === messageToUpdate.id);
+
+    if (messageIndex !== -1) {
+      state[messageIndex] = messageToUpdate;
+    }
+
+    return [...state];
+  }
+
+  if (action.type === "RESET") {
+    return [];
+  }
+};
+
+const MessagesList = ({ ticket, ticketId, isGroup }) => {
+  const classes = useStyles();
+
+  const [messagesList, dispatch] = useReducer(reducer, []);
+  const [pageNumber, setPageNumber] = useState(1);
+  const [hasMore, setHasMore] = useState(false);
+  const [loading, setLoading] = useState(false);
+  const lastMessageRef = useRef();
+
+  const [selectedMessage, setSelectedMessage] = useState({});
+  const [anchorEl, setAnchorEl] = useState(null);
+  const messageOptionsMenuOpen = Boolean(anchorEl);
+  const currentTicketId = useRef(ticketId);
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+
+    currentTicketId.current = ticketId;
+  }, [ticketId]);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      const fetchMessages = async () => {
+        if (ticketId === undefined) return;
+        try {
+          const { data } = await api.get("/messages/" + ticketId, {
+            params: { pageNumber },
+          });
+
+          if (currentTicketId.current === ticketId) {
+            dispatch({ type: "LOAD_MESSAGES", payload: data.messages });
+            setHasMore(data.hasMore);
+            setLoading(false);
+          }
+
+          if (pageNumber === 1 && data.messages.length > 1) {
+            scrollToBottom();
+          }
+        } catch (err) {
+          setLoading(false);
+          toastError(err);
+        }
+      };
+      fetchMessages();
+    }, 500);
+    return () => {
+      clearTimeout(delayDebounceFn);
+    };
+  }, [pageNumber, ticketId]);
+
+  useEffect(() => {
+    const companyId = localStorage.getItem("companyId");
+    const socket = socketManager.getSocket(companyId);
+
+    socket.on("ready", () => socket.emit("joinChatBox", `${ticket.id}`));
+
+    socket.on(`company-${companyId}-appMessage`, (data) => {
+      if (data.action === "create" && data.message.ticketId === currentTicketId.current) {
+        dispatch({ type: "ADD_MESSAGE", payload: data.message });
+        scrollToBottom();
+      }
+
+      if (data.action === "update" && data.message.ticketId === currentTicketId.current) {
+        dispatch({ type: "UPDATE_MESSAGE", payload: data.message });
+      }
+    });
+
+    return () => {
+      socket.disconnect();
+    };
+  }, [ticketId, ticket, socketManager]);
+
+  const loadMore = () => {
+    setPageNumber((prevPageNumber) => prevPageNumber + 1);
+  };
+
+  const scrollToBottom = () => {
+    if (lastMessageRef.current) {
+      lastMessageRef.current.scrollIntoView({});
+    }
+  };
+
+  const handleScroll = (e) => {
+    if (!hasMore) return;
+    const { scrollTop } = e.currentTarget;
+
+    if (scrollTop === 0) {
+      document.getElementById("messagesList").scrollTop = 1;
+    }
+
+    if (loading) {
+      return;
+    }
+
+    if (scrollTop < 50) {
+      loadMore();
+    }
+  };
+
+  const handleOpenMessageOptionsMenu = (e, message) => {
+    setAnchorEl(e.currentTarget);
+    setSelectedMessage(message);
+  };
+
+  const handleCloseMessageOptionsMenu = (e) => {
+    setAnchorEl(null);
+  };
+
+  const checkMessageMedia = (message) => {
+
+    if (message.mediaType === "locationMessage" && message.body.split('|').length >= 2) {
+      let locationParts = message.body.split('|')
+      let imageLocation = locationParts[0]
+      let linkLocation = locationParts[1]
+
+      let descriptionLocation = null
+
+      if (locationParts.length > 2)
+        descriptionLocation = message.body.split('|')[2]
+
+      return <LocationPreview image={imageLocation} link={linkLocation} description={descriptionLocation} />
+    }
+    /* else if (message.mediaType === "vcard") {
+      let array = message.body.split("\n");
+      let obj = [];
+      let contact = "";
+      for (let index = 0; index < array.length; index++) {
+        const v = array[index];
+        let values = v.split(":");
+        for (let ind = 0; ind < values.length; ind++) {
+          if (values[ind].indexOf("+") !== -1) {
+            obj.push({ number: values[ind] });
+          }
+          if (values[ind].indexOf("FN") !== -1) {
+            contact = values[ind + 1];
+          }
+        }
+      }
+      return <VcardPreview contact={contact} numbers={obj[0].number} />
+    } */
+    /*else if (message.mediaType === "multi_vcard") {
+      console.log("multi_vcard")
+      console.log(message)
+    	
+      if(message.body !== null && message.body !== "") {
+        let newBody = JSON.parse(message.body)
+        return (
+          <>
+            {
+            newBody.map(v => (
+              <VcardPreview contact={v.name} numbers={v.number} />
+            ))
+            }
+          </>
+        )
+      } else return (<></>)
+    }*/
+    else if (message.mediaType === "image") {
+      return <ModalImageCors imageUrl={message.mediaUrl} />;
+    } else if (message.mediaType === "audio") {
+      return (
+        <audio controls>
+          <source src={message.mediaUrl} type="audio/ogg"></source>
+        </audio>
+      );
+    } else if (message.mediaType === "video") {
+      return (
+        <video
+          className={classes.messageMedia}
+          src={message.mediaUrl}
+          controls
+        />
+      );
+    } else {
+      return (
+        <>
+          <div className={classes.downloadMedia}>
+            <Button
+              startIcon={<GetApp />}
+              color="primary"
+              variant="outlined"
+              target="_blank"
+              href={message.mediaUrl}
+            >
+              {i18n.t("messagesList.header.buttons.download")}
+            </Button>
+          </div>
+          <div style={{marginBottom: message.body === "" ? 8 : 0}}>
+            <Divider />
+          </div>
+        </>
+      );
+    }
+  };
+
+  const renderMessageAck = (message) => {
+    if (message.ack === 1) {
+      return <AccessTime fontSize="small" className={classes.ackIcons} />;
+    }
+    if (message.ack === 2) {
+      return <Done fontSize="small" className={classes.ackIcons} />;
+    }
+    if (message.ack === 3) {
+      return <DoneAll fontSize="small" className={classes.ackIcons} />;
+    }
+    if (message.ack === 4 || message.ack === 5) {
+      return <DoneAll fontSize="small" className={classes.ackDoneAllIcon} />;
+    }
+  };
+
+  const renderDailyTimestamps = (message, index) => {
+    if (index === 0) {
+      return (
+        <span
+          className={classes.dailyTimestamp}
+          key={`timestamp-${message.id}`}
+        >
+          <div className={classes.dailyTimestampText}>
+            {format(parseISO(messagesList[index].createdAt), "dd/MM/yyyy")}
+          </div>
+        </span>
+      );
+    }
+    if (index < messagesList.length - 1) {
+      let messageDay = parseISO(messagesList[index].createdAt);
+      let previousMessageDay = parseISO(messagesList[index - 1].createdAt);
+
+      if (!isSameDay(messageDay, previousMessageDay)) {
+        return (
+          <span
+            className={classes.dailyTimestamp}
+            key={`timestamp-${message.id}`}
+          >
+            <div className={classes.dailyTimestampText}>
+              {format(parseISO(messagesList[index].createdAt), "dd/MM/yyyy")}
+            </div>
+          </span>
+        );
+      }
+    }
+    if (index === messagesList.length - 1) {
+      return (
+        <div
+          key={`ref-${message.createdAt}`}
+          ref={lastMessageRef}
+          style={{ float: "left", clear: "both" }}
+        />
+      );
+    }
+  };
+
+  const renderNumberTicket = (message, index) => {
+    if (index < messagesList.length && index > 0) {
+
+      let messageTicket = message.ticketId;
+      let connectionName = message.ticket?.whatsapp?.name;
+      let previousMessageTicket = messagesList[index - 1].ticketId;
+
+      if (messageTicket !== previousMessageTicket) {
+        return (
+          <center>
+            <div className={classes.ticketNunberClosed}>
+              Conversa encerrada: {format(parseISO(messagesList[index - 1].createdAt), "dd/MM/yyyy HH:mm:ss")}
+            </div>
+
+            <div className={classes.ticketNunberOpen}>
+              Conversa iniciada: {format(parseISO(message.createdAt), "dd/MM/yyyy HH:mm:ss")}
+            </div>
+          </center>
+        );
+      }
+    }
+  };
+
+  const renderMessageDivider = (message, index) => {
+    if (index < messagesList.length && index > 0) {
+      let messageUser = messagesList[index].fromMe;
+      let previousMessageUser = messagesList[index - 1].fromMe;
+
+      if (messageUser !== previousMessageUser) {
+        return (
+          <span style={{ marginTop: 16 }} key={`divider-${message.id}`}></span>
+        );
+      }
+    }
+  };
+
+  const renderQuotedMessage = (message) => {
+    return (
+      <div
+        className={clsx(classes.quotedContainerLeft, {
+          [classes.quotedContainerRight]: message.fromMe,
+        })}
+      >
+        <span
+          className={clsx(classes.quotedSideColorLeft, {
+            [classes.quotedSideColorRight]: message.quotedMsg?.fromMe,
+          })}
+        ></span>
+        <div className={classes.quotedMsg}>
+          {!message.quotedMsg?.fromMe && (
+            <span className={classes.messageContactName}>
+              {message.quotedMsg?.contact?.name}
+            </span>
+          )}
+
+          {message.quotedMsg.mediaType === "audio"
+            && (
+              <div className={classes.downloadMedia}>
+                <audio controls>
+                  <source src={message.quotedMsg.mediaUrl} type="audio/ogg"></source>
+                </audio>
+              </div>
+            )
+          }
+          {message.quotedMsg.mediaType === "video"
+            && (
+              <video
+                className={classes.messageMedia}
+                src={message.quotedMsg.mediaUrl}
+                controls
+              />
+            )
+          }
+          {message.quotedMsg.mediaType === "application"
+            && (
+              <div className={classes.downloadMedia}>
+                <Button
+                  startIcon={<GetApp />}
+                  color="primary"
+                  variant="outlined"
+                  target="_blank"
+                  href={message.quotedMsg.mediaUrl}
+                >
+                  {i18n.t("messagesList.header.buttons.download")}
+                </Button>
+              </div>
+            )
+          }
+
+          {message.quotedMsg.mediaType === "image"
+            && (<ModalImageCors imageUrl={message.quotedMsg.mediaUrl} />)}
+
+          {message.quotedMsg.mediaType === "contactMessage"
+            && (
+                <span>{message.quotedMsg.body}</span>
+              )
+          }
+        </div>
+      </div>
+    );
+  };
+
+  const renderMessages = () => {
+    if (messagesList.length > 0) {
+      const viewMessagesList = messagesList.map((message, index) => {
+
+        if (message.mediaType === "call_log") {
+          return (
+            <React.Fragment key={message.id}>
+              {renderDailyTimestamps(message, index)}
+              {renderNumberTicket(message, index)}
+              {renderMessageDivider(message, index)}
+              <div className={classes.messageCenter}>
+                <IconButton
+                  variant="contained"
+                  size="small"
+                  id="messageActionsButton"
+                  disabled={message.isDeleted}
+                  className={classes.messageActionsButton}
+                  onClick={(e) => handleOpenMessageOptionsMenu(e, message)}
+                >
+                  <ExpandMore />
+                </IconButton>
+                {isGroup && (
+                  <span className={classes.messageContactName}>
+                    {message.contact?.name}
+                  </span>
+                )}
+                <div>
+                  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 17" width="20" height="17">
+                    <path fill="#df3333" d="M18.2 12.1c-1.5-1.8-5-2.7-8.2-2.7s-6.7 1-8.2 2.7c-.7.8-.3 2.3.2 2.8.2.2.3.3.5.3 1.4 0 3.6-.7 3.6-.7.5-.2.8-.5.8-1v-1.3c.7-1.2 5.4-1.2 6.4-.1l.1.1v1.3c0 .2.1.4.2.6.1.2.3.3.5.4 0 0 2.2.7 3.6.7.2 0 1.4-2 .5-3.1zM5.4 3.2l4.7 4.6 5.8-5.7-.9-.8L10.1 6 6.4 2.3h2.5V1H4.1v4.8h1.3V3.2z"></path>
+                  </svg> <span>{i18n.t("messagesList.lostCall")} {format(parseISO(message.createdAt), "HH:mm")}</span>
+                </div>
+              </div>
+            </React.Fragment>
+          );
+        }
+
+        if (!message.fromMe) {
+          return (
+            <React.Fragment key={message.id}>
+              {renderDailyTimestamps(message, index)}
+              {renderNumberTicket(message, index)}
+              {renderMessageDivider(message, index)}
+              <div className={classes.messageLeft}>
+                <IconButton
+                  variant="contained"
+                  size="small"
+                  id="messageActionsButton"
+                  disabled={message.isDeleted}
+                  className={classes.messageActionsButton}
+                  onClick={(e) => handleOpenMessageOptionsMenu(e, message)}
+                >
+                  <ExpandMore />
+                </IconButton>
+                {isGroup && (
+                  <span className={classes.messageContactName}>
+                    {message.contact?.name}
+                  </span>
+                )}
+
+                {/* aviso de mensagem apagado pelo contato */}
+                {message.isDeleted && (
+                  <div>
+                    <span className={"message-deleted"}
+                    >{i18n.t("messagesList.deletedMessage")} &nbsp;
+                      <Block
+                        color="error"
+                        fontSize="small"
+                        className={classes.deletedIcon}
+                      />
+                    </span>
+                  </div>
+                )}
+
+                {(message.mediaUrl || message.mediaType === "locationMessage" || message.mediaType === "vcard"
+                  //|| message.mediaType === "multi_vcard" 
+                ) && checkMessageMedia(message)}
+                <div className={classes.textContentItem}>
+                  {message.quotedMsg && renderQuotedMessage(message)}
+                  <MarkdownWrapper>{message.mediaType === "locationMessage" ? null : message.body}</MarkdownWrapper>
+                  <span className={classes.timestamp}>
+				    {message.isEdited && <span>Editada </span>}
+                    {format(parseISO(message.createdAt), "HH:mm")}
+                  </span>
+                </div>
+              </div>
+            </React.Fragment>
+          );
+        } else {
+          return (
+            <React.Fragment key={message.id}>
+              {renderDailyTimestamps(message, index)}
+              {renderNumberTicket(message, index)}
+              {renderMessageDivider(message, index)}
+              <div className={classes.messageRight}>
+                <IconButton
+                  variant="contained"
+                  size="small"
+                  id="messageActionsButton"
+                  disabled={message.isDeleted}
+                  className={classes.messageActionsButton}
+                  onClick={(e) => handleOpenMessageOptionsMenu(e, message)}
+                >
+                  <ExpandMore />
+                </IconButton>
+                {(message.mediaUrl || message.mediaType === "locationMessage" || message.mediaType === "vcard"
+                  //|| message.mediaType === "multi_vcard" 
+                ) && checkMessageMedia(message)}
+                <div
+                  className={clsx(classes.textContentItem, {
+                    [classes.textContentItemDeleted]: message.isDeleted,
+					[classes.textContentItemEdited]: message.isEdited,
+                  })}
+                >
+                  {message.isDeleted && (
+                    <Block
+                      color="disabled"
+                      fontSize="small"
+                      className={classes.deletedIcon}
+                    />
+                  )}
+                  {message.quotedMsg && renderQuotedMessage(message)}
+                  <MarkdownWrapper>{message.mediaType === "locationMessage" ? null : message.body}</MarkdownWrapper>
+                  <span className={classes.timestamp}>
+				    {message.isEdited && <span>{i18n.t("messagesList.edited")}</span>}
+                    {format(parseISO(message.createdAt), "HH:mm")}
+                    {renderMessageAck(message)}
+                  </span>
+                </div>
+              </div>
+            </React.Fragment>
+          );
+        }
+      });
+      return viewMessagesList;
+    } else {
+      return <div>{i18n.t("messagesList.saudation")}</div>;
+    }
+  };
+
+  return (
+    <div className={classes.messagesListWrapper}>
+      <MessageOptionsMenu
+        message={selectedMessage}
+        anchorEl={anchorEl}
+        menuOpen={messageOptionsMenuOpen}
+        handleClose={handleCloseMessageOptionsMenu}
+      />
+      <div
+        id="messagesList"
+        className={classes.messagesList}
+        onScroll={handleScroll}
+      >
+        {messagesList.length > 0 ? renderMessages() : []}
+      </div>
+      {loading && (
+        <div>
+          <CircularProgress className={classes.circleLoading} />
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default MessagesList;

+ 50 - 0
frontend/src/components/ModalImageCors/index.js

@@ -0,0 +1,50 @@
+import React, { useState, useEffect } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+
+import ModalImage from "react-modal-image";
+import api from "../../services/api";
+
+const useStyles = makeStyles(theme => ({
+	messageMedia: {
+		objectFit: "cover",
+		width: 250,
+		height: 200,
+		borderTopLeftRadius: 8,
+		borderTopRightRadius: 8,
+		borderBottomLeftRadius: 8,
+		borderBottomRightRadius: 8,
+	},
+}));
+
+const ModalImageCors = ({ imageUrl }) => {
+	const classes = useStyles();
+	const [fetching, setFetching] = useState(true);
+	const [blobUrl, setBlobUrl] = useState("");
+
+	useEffect(() => {
+		if (!imageUrl) return;
+		const fetchImage = async () => {
+			const { data, headers } = await api.get(imageUrl, {
+				responseType: "blob",
+			});
+			const url = window.URL.createObjectURL(
+				new Blob([data], { type: headers["content-type"] })
+			);
+			setBlobUrl(url);
+			setFetching(false);
+		};
+		fetchImage();
+	}, [imageUrl]);
+
+	return (
+		<ModalImage
+			className={classes.messageMedia}
+			smallSrcSet={fetching ? imageUrl : blobUrl}
+			medium={fetching ? imageUrl : blobUrl}
+			large={fetching ? imageUrl : blobUrl}
+			alt="image"
+		/>
+	);
+};
+
+export default ModalImageCors;

+ 263 - 0
frontend/src/components/ModalUsers/index.js

@@ -0,0 +1,263 @@
+import React, { useState, useEffect, useContext } from "react";
+
+import * as Yup from "yup";
+import { Formik, Form, Field } from "formik";
+import { toast } from "react-toastify";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import CircularProgress from "@material-ui/core/CircularProgress";
+import Select from "@material-ui/core/Select";
+import InputLabel from "@material-ui/core/InputLabel";
+import MenuItem from "@material-ui/core/MenuItem";
+import FormControl from "@material-ui/core/FormControl";
+
+import { i18n } from "../../translate/i18n";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+import QueueSelectCustom from "../QueueSelectCustom";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { Can } from "../Can";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    display: "flex",
+    flexWrap: "wrap",
+  },
+  multFieldLine: {
+    display: "flex",
+    "& > *:not(:last-child)": {
+      marginRight: theme.spacing(1),
+    },
+  },
+
+  btnWrapper: {
+    position: "relative",
+  },
+
+  buttonProgress: {
+    color: green[500],
+    position: "absolute",
+    top: "50%",
+    left: "50%",
+    marginTop: -12,
+    marginLeft: -12,
+  },
+  formControl: {
+    margin: theme.spacing(1),
+    minWidth: 120,
+  },
+}));
+
+const UserSchema = Yup.object().shape({
+  name: Yup.string()
+    .min(2, "Too Short!")
+    .max(50, "Too Long!")
+    .required("Required"),
+  password: Yup.string().min(5, "Too Short!").max(50, "Too Long!"),
+  email: Yup.string().email("Invalid email").required("Required"),
+});
+
+const ModalUsers = ({ open, onClose, userId, companyId }) => {
+  const classes = useStyles();
+
+  const initialState = {
+    name: "",
+    email: "",
+    password: "",
+    profile: "user",
+  };
+
+  const { user: loggedInUser } = useContext(AuthContext);
+
+  const [user, setUser] = useState(initialState);
+  const [selectedQueueIds, setSelectedQueueIds] = useState([]);
+
+  useEffect(() => {
+    const fetchUser = async () => {
+      if (!userId) return;
+      if (open) {
+        try {
+          const { data } = await api.get(`/users/${userId}`);
+          setUser((prevState) => {
+            return { ...prevState, ...data };
+          });
+          const userQueueIds = data.queues?.map((queue) => queue.id);
+          setSelectedQueueIds(userQueueIds);
+        } catch (err) {
+          toastError(err);
+        }
+      }
+    };
+
+    fetchUser();
+  }, [userId, open]);
+
+  const handleClose = () => {
+    onClose();
+    setUser(initialState);
+  };
+
+  const handleSaveUser = async (values) => {
+    const userData = { ...values, companyId, queueIds: selectedQueueIds };
+    try {
+      if (userId) {
+        await api.put(`/users/${userId}`, userData);
+      } else {
+        await api.post("/users", userData);
+      }
+      toast.success(i18n.t("userModal.success"));
+    } catch (err) {
+      toastError(err);
+    }
+    handleClose();
+  };
+
+  return (
+    <div className={classes.root}>
+      <Dialog
+        open={open}
+        onClose={handleClose}
+        maxWidth="xs"
+        fullWidth
+        scroll="paper"
+      >
+        <DialogTitle id="form-dialog-title">
+          {userId
+            ? `${i18n.t("userModal.title.edit")}`
+            : `${i18n.t("userModal.title.add")}`}
+        </DialogTitle>
+        <Formik
+          initialValues={user}
+          enableReinitialize={true}
+          validationSchema={UserSchema}
+          onSubmit={(values, actions) => {
+            setTimeout(() => {
+              handleSaveUser(values);
+              actions.setSubmitting(false);
+            }, 400);
+          }}
+        >
+          {({ touched, errors, isSubmitting }) => (
+            <Form>
+              <DialogContent dividers>
+                <div className={classes.multFieldLine}>
+                  <Field
+                    as={TextField}
+                    label={i18n.t("userModal.form.name")}
+                    autoFocus
+                    name="name"
+                    error={touched.name && Boolean(errors.name)}
+                    helperText={touched.name && errors.name}
+                    variant="outlined"
+                    margin="dense"
+                    fullWidth
+                  />
+                  <Field
+                    as={TextField}
+                    label={i18n.t("userModal.form.password")}
+                    type="password"
+                    name="password"
+                    error={touched.password && Boolean(errors.password)}
+                    helperText={touched.password && errors.password}
+                    variant="outlined"
+                    margin="dense"
+                    fullWidth
+                  />
+                </div>
+                <div className={classes.multFieldLine}>
+                  <Field
+                    as={TextField}
+                    label={i18n.t("userModal.form.email")}
+                    name="email"
+                    error={touched.email && Boolean(errors.email)}
+                    helperText={touched.email && errors.email}
+                    variant="outlined"
+                    margin="dense"
+                    fullWidth
+                  />
+                  <FormControl
+                    variant="outlined"
+                    className={classes.formControl}
+                    margin="dense"
+                  >
+                    <Can
+                      role={loggedInUser.profile}
+                      perform="user-modal:editProfile"
+                      yes={() => (
+                        <>
+                          <InputLabel id="profile-selection-input-label">
+                            {i18n.t("userModal.form.profile")}
+                          </InputLabel>
+
+                          <Field
+                            as={Select}
+                            label={i18n.t("userModal.form.profile")}
+                            name="profile"
+                            labelId="profile-selection-label"
+                            id="profile-selection"
+                            required
+                          >
+                            <MenuItem value="admin">Admin</MenuItem>
+                            <MenuItem value="user">User</MenuItem>
+                          </Field>
+                        </>
+                      )}
+                    />
+                  </FormControl>
+                </div>
+                <Can
+                  role={loggedInUser.profile}
+                  perform="user-modal:editQueues"
+                  yes={() => (
+                    <QueueSelectCustom
+                      companyId={companyId}
+                      selectedQueueIds={selectedQueueIds}
+                      onChange={(values) => setSelectedQueueIds(values)}
+                    />
+                  )}
+                />
+              </DialogContent>
+              <DialogActions>
+                <Button
+                  onClick={handleClose}
+                  color="secondary"
+                  disabled={isSubmitting}
+                  variant="outlined"
+                >
+                  {i18n.t("userModal.buttons.cancel")}
+                </Button>
+                <Button
+                  type="submit"
+                  color="primary"
+                  disabled={isSubmitting}
+                  variant="contained"
+                  className={classes.btnWrapper}
+                >
+                  {userId
+                    ? `${i18n.t("userModal.buttons.okEdit")}`
+                    : `${i18n.t("userModal.buttons.okAdd")}`}
+                  {isSubmitting && (
+                    <CircularProgress
+                      size={24}
+                      className={classes.buttonProgress}
+                    />
+                  )}
+                </Button>
+              </DialogActions>
+            </Form>
+          )}
+        </Formik>
+      </Dialog>
+    </div>
+  );
+};
+
+export default ModalUsers;

+ 447 - 0
frontend/src/components/NewTicketModal/index.js

@@ -0,0 +1,447 @@
+import React, { useState, useEffect, useContext } from "react";
+
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import Dialog from "@material-ui/core/Dialog";
+
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import Autocomplete, {
+	createFilterOptions,
+} from "@material-ui/lab/Autocomplete";
+import CircularProgress from "@material-ui/core/CircularProgress";
+
+import { i18n } from "../../translate/i18n";
+import api from "../../services/api";
+import ButtonWithSpinner from "../ButtonWithSpinner";
+import ContactModal from "../ContactModal";
+import toastError from "../../errors/toastError";
+import { makeStyles } from "@material-ui/core/styles";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import {  WhatsApp } from "@material-ui/icons";
+import { Grid, ListItemText, MenuItem, Select } from "@material-ui/core";
+import Typography from "@material-ui/core/Typography";
+import { toast } from "react-toastify";
+//import ShowTicketOpen from "../ShowTicketOpenModal";
+
+const useStyles = makeStyles((theme) => ({
+  online: {
+    fontSize: 11,
+    color: "#25d366"
+  },
+  offline: {
+    fontSize: 11,
+    color: "#e1306c"
+  }
+}));
+
+const filter = createFilterOptions({
+  trim: true,
+});
+
+const NewTicketModal = ({ modalOpen, onClose, initialContact }) => {
+  const classes = useStyles();
+  const [options, setOptions] = useState([]);
+
+  const [loading, setLoading] = useState(false);
+  const [searchParam, setSearchParam] = useState("");
+  const [selectedContact, setSelectedContact] = useState(null);
+  const [selectedQueue, setSelectedQueue] = useState("");
+  const [selectedWhatsapp, setSelectedWhatsapp] = useState("");
+  const [newContact, setNewContact] = useState({});
+  const [whatsapps, setWhatsapps] = useState([]);
+  const [queues, setQueues] = useState([]);
+  const [contactModalOpen, setContactModalOpen] = useState(false);
+  const { user } = useContext(AuthContext);
+  const { companyId, whatsappId } = user;
+
+  const [ openAlert, setOpenAlert ] = useState(false);
+	const [ userTicketOpen, setUserTicketOpen] = useState("");
+	const [ queueTicketOpen, setQueueTicketOpen] = useState("");
+
+  useEffect(() => {
+    if (initialContact?.id !== undefined) {
+      setOptions([initialContact]);
+      setSelectedContact(initialContact);
+    }
+  }, [initialContact]);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      const fetchContacts = async () => {
+        api
+          .get(`/whatsapp`, { params: { companyId, session: 0 } })
+          .then(({ data }) => setWhatsapps(data));
+      };
+
+      if (whatsappId !== null && whatsappId!== undefined) {
+        setSelectedWhatsapp(whatsappId)
+      }
+
+      const fetchQueues = async (  ) => {
+
+        if( user.profile !== "admin" ){
+
+          setQueues( user.queues );
+          
+          if (user.queues.length === 1) {
+            setSelectedQueue(user.queues[0].id)
+          }
+
+          return;
+        }
+
+        try{
+
+          const {data: queues} = await api.get('/queue');
+          setQueues( queues )
+
+          if( queues.length === 1) {
+            setSelectedQueue(queues[0].id)
+          }
+
+        }catch(err){
+          toastError(i18n.t("newTicketModal.searchQueueError"));       
+        }        
+      }
+
+      fetchQueues(  );
+      fetchContacts(  );
+      setLoading(false);
+    }, 500);
+    return () => clearTimeout(delayDebounceFn);
+  }, [])
+
+  useEffect(() => {
+    if (!modalOpen || searchParam.length < 3) {
+      setLoading(false);
+      return;
+    }
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      const fetchContacts = async () => {
+        try {
+          const { data } = await api.get("contacts", {
+            params: { searchParam },
+          });
+          setOptions(data.contacts);
+          setLoading(false);
+        } catch (err) {
+          setLoading(false);
+          toastError(err);
+        }
+      };
+      fetchContacts();
+    }, 500);
+    return () => clearTimeout(delayDebounceFn);
+  }, [searchParam, modalOpen]);
+
+  // const IconChannel = (channel) => {
+  //   switch (channel) {
+  //     case "facebook":
+  //       return <Facebook style={{ color: "#3b5998", verticalAlign: "middle" }} />;
+  //     case "instagram":
+  //       return <Instagram style={{ color: "#e1306c", verticalAlign: "middle" }} />;
+  //     case "whatsapp":
+  //       return <WhatsApp style={{ color: "#25d366", verticalAlign: "middle" }} />
+  //     default:
+  //       return "error";
+  //   }
+  // };
+
+  const handleClose = () => {
+    onClose();
+    setSearchParam("");
+    setOpenAlert(false);
+    setUserTicketOpen("");
+    setQueueTicketOpen("");
+    setSelectedContact(null);
+  };
+
+  const handleCloseAlert = () => {
+    setOpenAlert(false);
+    setLoading(false);
+    setOpenAlert(false);
+    setUserTicketOpen("");
+    setQueueTicketOpen("");
+  };
+
+  const handleSaveTicket = async contactId => {
+    if (!contactId) return;
+    if (selectedQueue === "" && user.profile !== 'admin') {
+      toast.error(i18n.t("newTicketModal.selectQueue"));
+      return;
+    }
+    
+    setLoading(true);
+    try {
+      const queueId = selectedQueue !== "" ? selectedQueue : null;
+      const whatsappId = selectedWhatsapp !== "" ? selectedWhatsapp : null;
+      const { data: ticket } = await api.post("/tickets", {
+        contactId: contactId,
+        queueId,
+        whatsappId,
+        userId: user.id,
+        status: "open",
+      });      
+
+      onClose(ticket);
+    } catch (err) {
+      
+      console.log(err);
+      const ticket  = err.response.data.error;
+      console.log(ticket);
+
+      if( ticket === "ERR_OTHER_OPEN_TICKET" )
+        toastError(err);
+      
+      if ( ticket !== "ERR_OTHER_OPEN_TICKET" && ticket.userId !== user?.id) {
+        setOpenAlert(true);
+        setUserTicketOpen(ticket.user.name);
+        setQueueTicketOpen(ticket.queue.name);
+      } else {
+        setOpenAlert(false);
+        setUserTicketOpen("");
+        setQueueTicketOpen("");
+        setLoading(false);
+        onClose(ticket);
+      }
+    }  
+    setLoading(false);
+  };
+
+  const handleSelectOption = (e, newValue) => {
+    if (newValue?.number) {
+      setSelectedContact(newValue);
+    } else if (newValue?.name) {
+      setNewContact({ name: newValue.name });
+      setContactModalOpen(true);
+    }
+  };
+
+  const handleCloseContactModal = () => {
+    setContactModalOpen(false);    
+  };
+
+  const handleAddNewContactTicket = contact => {
+    handleSaveTicket(contact.id);
+  };
+
+  const createAddContactOption = (filterOptions, params) => {
+    const filtered = filter(filterOptions, params);
+    if (params.inputValue !== "" && !loading && searchParam.length >= 3) {
+      filtered.push({
+        name: `${params.inputValue}`,
+      });
+    }
+    return filtered;
+  };
+
+  const renderOption = option => {
+    if (option.number) {
+      return <>
+        {/* {IconChannel(option.channel)} */}
+        <Typography component="span" style={{ fontSize: 14, marginLeft: "10px", display: "inline-flex", alignItems: "center", lineHeight: "2" }}>
+          {option.name} - {option.number}
+        </Typography>
+      </>
+    } else {
+      return `${i18n.t("newTicketModal.add")} ${option.name}`;
+    }
+  };
+
+  const renderOptionLabel = option => {
+    if (option.number) {
+      return `${option.name} - ${option.number}`;
+    } else {
+      return `${option.name}`;
+    }
+  };
+
+  const renderContactAutocomplete = () => {
+    if (initialContact === undefined || initialContact.id === undefined) {
+      return (
+        <Grid xs={12} item>
+          <Autocomplete
+            fullWidth
+            options={options}
+            loading={loading}
+            clearOnBlur
+            autoHighlight
+            freeSolo
+            clearOnEscape
+            getOptionLabel={renderOptionLabel}
+            renderOption={renderOption}
+            filterOptions={createAddContactOption}
+            onChange={(e, newValue) => handleSelectOption(e, newValue)}
+            renderInput={params => (
+              <TextField
+                {...params}
+                label={i18n.t("newTicketModal.fieldLabel")}
+                variant="outlined"
+                autoFocus
+                onChange={e => setSearchParam(e.target.value)}
+                onKeyPress={e => {
+                  if (loading || !selectedContact) return;
+                  else if (e.key === "Enter") {
+                    handleSaveTicket(selectedContact.id);
+                  }
+                }}
+                InputProps={{
+                  ...params.InputProps,
+                  endAdornment: (
+                    <React.Fragment>
+                      {loading ? (
+                        <CircularProgress color="inherit" size={20} />
+                      ) : null}
+                      {params.InputProps.endAdornment}
+                    </React.Fragment>
+                  ),
+                }}
+              />
+            )}
+          />
+        </Grid>
+      )
+    }
+    return null;
+  }
+
+  return (
+    <>
+      <ContactModal
+        open={contactModalOpen}
+        initialValues={newContact}
+        onClose={handleCloseContactModal}
+        onSave={handleAddNewContactTicket}
+      ></ContactModal>
+      <Dialog open={modalOpen} onClose={handleClose}>
+        <DialogTitle id="form-dialog-title">
+          {i18n.t("newTicketModal.title")}
+        </DialogTitle>
+        <DialogContent dividers>
+          <Grid style={{ width: 300 }} container spacing={2}>
+            {/* CONTATO */}
+            {renderContactAutocomplete()}
+            {/* FILA */}
+            <Grid xs={12} item>
+              <Select
+                required
+                fullWidth
+                displayEmpty
+                variant="outlined"
+                value={selectedQueue}
+                onChange={(e) => {
+                  setSelectedQueue(e.target.value)
+                }}
+                MenuProps={{
+                  anchorOrigin: {
+                    vertical: "bottom",
+                    horizontal: "left",
+                  },
+                  transformOrigin: {
+                    vertical: "top",
+                    horizontal: "left",
+                  },
+                  getContentAnchorEl: null,
+                }}
+                renderValue={() => {
+                  if (selectedQueue === "") {
+                    return i18n.t("newTicketModal.selectQueue")
+                  }
+
+                  const queue = queues.find(q => q.id === selectedQueue)
+                  return queue.name
+                }}
+              >
+                {queues?.length > 0 &&
+                  queues.map((queue, key) => (
+                    <MenuItem dense key={key} value={queue.id}>
+                      <ListItemText primary={queue.name} />
+                    </MenuItem>
+                  ))
+                }
+              </Select>
+            </Grid>
+            {/* CONEXAO */}
+            <Grid xs={12} item>
+              <Select
+                required
+                fullWidth
+                displayEmpty
+                variant="outlined"
+                value={selectedWhatsapp}
+                onChange={(e) => {
+                  setSelectedWhatsapp(e.target.value)
+                }}
+                MenuProps={{
+                  anchorOrigin: {
+                    vertical: "bottom",
+                    horizontal: "left",
+                  },
+                  transformOrigin: {
+                    vertical: "top",
+                    horizontal: "left",
+                  },
+                  getContentAnchorEl: null,
+                }}
+                renderValue={() => {
+                  if (selectedWhatsapp === "") {
+                    return i18n.t("newTicketModal.selectConection")
+                  }
+                  const whatsapp = whatsapps.find(w => w.id === selectedWhatsapp)
+                  return whatsapp.name
+                }}
+              >
+                {whatsapps?.length > 0 &&
+                  whatsapps.map((whatsapp, key) => (
+                    <MenuItem dense key={key} value={whatsapp.id}>
+                      <ListItemText
+                        primary={
+                          <>
+                            {/* {IconChannel(whatsapp.channel)} */}
+                            <Typography component="span" style={{ fontSize: 14, marginLeft: "10px", display: "inline-flex", alignItems: "center", lineHeight: "2" }}>
+                              {whatsapp.name} &nbsp; <p className={(whatsapp.status) === 'CONNECTED' ? classes.online : classes.offline} >({whatsapp.status})</p>
+                            </Typography>
+                          </>
+                        }
+                      />
+                    </MenuItem>
+                  ))}
+              </Select>
+            </Grid>
+          </Grid>
+        </DialogContent>
+        <DialogActions>
+          <Button
+            onClick={handleClose}
+            color="secondary"
+            disabled={loading}
+            variant="outlined"
+          >
+            {i18n.t("newTicketModal.buttons.cancel")}
+          </Button>
+          <ButtonWithSpinner
+            variant="contained"
+            type="button"
+            disabled={!selectedContact}
+            onClick={() => handleSaveTicket(selectedContact.id)}
+            color="primary"
+            loading={loading}
+          >
+            {i18n.t("newTicketModal.buttons.ok")}
+          </ButtonWithSpinner>
+        </DialogActions>
+        {/* <ShowTicketOpen
+          isOpen={openAlert}
+          handleClose={handleCloseAlert}
+          user={userTicketOpen}
+          queue={queueTicketOpen}
+			  /> */}
+      </Dialog >
+    </>
+  );
+};
+export default NewTicketModal;

+ 270 - 0
frontend/src/components/NotificationsPopOver/index.js

@@ -0,0 +1,270 @@
+import React, { useState, useRef, useEffect, useContext } from "react";
+
+import { useHistory } from "react-router-dom";
+import { format } from "date-fns";
+import { SocketContext } from "../../context/Socket/SocketContext";
+
+import useSound from "use-sound";
+
+import Popover from "@material-ui/core/Popover";
+import IconButton from "@material-ui/core/IconButton";
+import List from "@material-ui/core/List";
+import ListItem from "@material-ui/core/ListItem";
+import ListItemText from "@material-ui/core/ListItemText";
+import { makeStyles } from "@material-ui/core/styles";
+import Badge from "@material-ui/core/Badge";
+import ChatIcon from "@material-ui/icons/Chat";
+
+import TicketListItem from "../TicketListItemCustom";
+import useTickets from "../../hooks/useTickets";
+import alertSound from "../../assets/sound.mp3";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { i18n } from "../../translate/i18n";
+import toastError from "../../errors/toastError";
+
+const useStyles = makeStyles(theme => ({
+	tabContainer: {
+		overflowY: "auto",
+		maxHeight: 350,
+		...theme.scrollbarStyles,
+	},
+	popoverPaper: {
+		width: "100%",
+		maxWidth: 350,
+		marginLeft: theme.spacing(2),
+		marginRight: theme.spacing(1),
+		[theme.breakpoints.down("sm")]: {
+			maxWidth: 270,
+		},
+	},
+	noShadow: {
+		boxShadow: "none !important",
+	},
+}));
+
+const NotificationsPopOver = (volume) => {
+	const classes = useStyles();
+
+	const history = useHistory();
+	const { user } = useContext(AuthContext);
+	const ticketIdUrl = +history.location.pathname.split("/")[2];
+	const ticketIdRef = useRef(ticketIdUrl);
+	const anchorEl = useRef();
+	const [isOpen, setIsOpen] = useState(false);
+	const [notifications, setNotifications] = useState([]);
+
+	const [showPendingTickets, setShowPendingTickets] = useState(false);
+
+	const [, setDesktopNotifications] = useState([]);
+
+	const { tickets } = useTickets({ withUnreadMessages: "true" });
+
+	const [play] = useSound(alertSound, volume);
+	const soundAlertRef = useRef();
+
+	const historyRef = useRef(history);
+
+  const socketManager = useContext(SocketContext);
+
+	useEffect(() => {
+		const fetchSettings = async () => {
+			try {
+
+				if (user.allTicket === "enable") {
+					setShowPendingTickets(true);
+				}
+			} catch (err) {
+			  	toastError(err);
+			}
+		}
+	  
+		fetchSettings();
+	}, []);
+
+	useEffect(() => {
+		soundAlertRef.current = play;
+
+		if (!("Notification" in window)) {
+			console.log("This browser doesn't support notifications");
+		} else {
+			Notification.requestPermission();
+		}
+	}, [play]);
+
+	useEffect(() => {
+		const processNotifications = () => {
+			if (showPendingTickets) {
+				setNotifications(tickets);
+			} else {
+				const newNotifications = tickets.filter(ticket => ticket.status !== "pending");
+
+				setNotifications(newNotifications);
+			}
+		}
+
+		processNotifications();
+	}, [tickets]);
+
+	useEffect(() => {
+		ticketIdRef.current = ticketIdUrl;
+	}, [ticketIdUrl]);
+
+	useEffect(() => {
+    const socket = socketManager.getSocket(user.companyId);
+
+		socket.on("ready", () => socket.emit("joinNotification"));
+
+		socket.on(`company-${user.companyId}-ticket`, data => {
+			if (data.action === "updateUnread" || data.action === "delete") {
+				setNotifications(prevState => {
+					const ticketIndex = prevState.findIndex(t => t.id === data.ticketId);
+					if (ticketIndex !== -1) {
+						prevState.splice(ticketIndex, 1);
+						return [...prevState];
+					}
+					return prevState;
+				});
+
+				setDesktopNotifications(prevState => {
+					const notfiticationIndex = prevState.findIndex(
+						n => n.tag === String(data.ticketId)
+					);
+					if (notfiticationIndex !== -1) {
+						prevState[notfiticationIndex].close();
+						prevState.splice(notfiticationIndex, 1);
+						return [...prevState];
+					}
+					return prevState;
+				});
+			}
+		});
+
+		socket.on(`company-${user.companyId}-appMessage`, data => {
+			if (
+				data.action === "create" && !data.message.fromMe && 
+				(data.ticket.status !== "pending" ) &&
+				(!data.message.read || data.ticket.status === "pending") &&
+				(data.ticket.userId === user?.id || !data.ticket.userId) &&
+				(user?.queues?.some(queue => (queue.id === data.ticket.queueId)) || !data.ticket.queueId)
+			) {
+				setNotifications(prevState => {
+					const ticketIndex = prevState.findIndex(t => t.id === data.ticket.id);
+					if (ticketIndex !== -1) {
+						prevState[ticketIndex] = data.ticket;
+						return [...prevState];
+					}
+					return [data.ticket, ...prevState];
+				});
+
+				const shouldNotNotificate =
+					(data.message.ticketId === ticketIdRef.current &&
+						document.visibilityState === "visible") ||
+					(data.ticket.userId && data.ticket.userId !== user?.id) ||
+					data.ticket.isGroup;
+
+				if (shouldNotNotificate) return;
+
+				handleNotifications(data);
+			}
+		});
+
+		return () => {
+			socket.disconnect();
+		};
+	}, [user, showPendingTickets, socketManager]);
+
+	const handleNotifications = data => {
+		const { message, contact, ticket } = data;
+
+		const options = {
+			body: `${message.body} - ${format(new Date(), "HH:mm")}`,
+			icon: contact.urlPicture,
+			tag: ticket.id,
+			renotify: true,
+		};
+
+		const notification = new Notification(
+			`${i18n.t("tickets.notification.message")} ${contact.name}`,
+			options
+		);
+
+		notification.onclick = e => {
+			e.preventDefault();
+			window.focus();
+			historyRef.current.push(`/tickets/${ticket.uuid}`);
+			// handleChangeTab(null, ticket.isGroup? "group" : "open");
+		};
+
+		setDesktopNotifications(prevState => {
+			const notfiticationIndex = prevState.findIndex(
+				n => n.tag === notification.tag
+			);
+			if (notfiticationIndex !== -1) {
+				prevState[notfiticationIndex] = notification;
+				return [...prevState];
+			}
+			return [notification, ...prevState];
+		});
+
+		soundAlertRef.current();
+	};
+
+	const handleClick = () => {
+		setIsOpen(prevState => !prevState);
+	};
+
+	const handleClickAway = () => {
+		setIsOpen(false);
+	};
+
+	const NotificationTicket = ({ children }) => {
+		return <div onClick={handleClickAway}>{children}</div>;
+	};
+
+	return (
+		<>
+			<IconButton
+				onClick={handleClick}
+				ref={anchorEl}
+				aria-label="Open Notifications"
+				color="inherit"
+				style={{color:"white"}}
+			>
+				<Badge overlap="rectangular" badgeContent={notifications.length} color="secondary">
+					<ChatIcon />
+				</Badge>
+			</IconButton>
+			<Popover
+				disableScrollLock
+				open={isOpen}
+				anchorEl={anchorEl.current}
+				anchorOrigin={{
+					vertical: "bottom",
+					horizontal: "right",
+				}}
+				transformOrigin={{
+					vertical: "top",
+					horizontal: "right",
+				}}
+				classes={{ paper: classes.popoverPaper }}
+				onClose={handleClickAway}
+			>
+				<List dense className={classes.tabContainer}>
+					{notifications.length === 0 ? (
+						<ListItem>
+							<ListItemText>{i18n.t("notifications.noTickets")}</ListItemText>
+						</ListItem>
+					) : (
+						notifications.map(ticket => (
+							<NotificationTicket key={ticket.id}>
+								<TicketListItem ticket={ticket} />
+							</NotificationTicket>
+						))
+					)}
+				</List>
+			</Popover>
+		</>
+	);
+};
+
+export default NotificationsPopOver;

+ 275 - 0
frontend/src/components/NotificationsPopOver/index_Antigo.js

@@ -0,0 +1,275 @@
+import React, { useState, useRef, useEffect, useContext } from "react";
+
+import { useHistory } from "react-router-dom";
+import { format } from "date-fns";
+import useSound from "use-sound";
+
+import Popover from "@material-ui/core/Popover";
+import IconButton from "@material-ui/core/IconButton";
+import List from "@material-ui/core/List";
+import ListItem from "@material-ui/core/ListItem";
+import ListItemText from "@material-ui/core/ListItemText";
+import { makeStyles } from "@material-ui/core/styles";
+import Badge from "@material-ui/core/Badge";
+import ChatIcon from "@material-ui/icons/Chat";
+
+import TicketListItem from "../TicketListItem";
+import { i18n } from "../../translate/i18n";
+import useTickets from "../../hooks/useTickets";
+import alertSound from "../../assets/sound.mp3";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { socketConnection } from "../../services/socket";
+
+const useStyles = makeStyles((theme) => ({
+  tabContainer: {
+    overflowY: "auto",
+    maxHeight: 350,
+    ...theme.scrollbarStyles,
+  },
+  popoverPaper: {
+    width: "100%",
+    maxWidth: 350,
+    marginLeft: theme.spacing(2),
+    marginRight: theme.spacing(1),
+    [theme.breakpoints.down("sm")]: {
+      maxWidth: 270,
+    },
+  },
+  noShadow: {
+    boxShadow: "none !important",
+  },
+  icons: {
+    color: "#fff",
+  },
+  customBadge: {
+    backgroundColor: "#f44336",
+    color: "#fff",
+  },
+}));
+
+const NotificationsPopOver = ({ volume }) => {
+  const classes = useStyles();
+
+  const history = useHistory();
+  const { user } = useContext(AuthContext);
+  const ticketIdUrl = +history.location.pathname.split("/")[2];
+  const ticketIdRef = useRef(ticketIdUrl);
+  const anchorEl = useRef();
+  const [isOpen, setIsOpen] = useState(false);
+  const [notifications, setNotifications] = useState([]);
+  const { profile, queues } = user;
+
+  const [, setDesktopNotifications] = useState([]);
+
+  const { tickets } = useTickets({ withUnreadMessages: "true" });
+  const [play] = useSound(alertSound, { volume, });
+  const soundAlertRef = useRef();
+
+  const historyRef = useRef(history);
+
+  useEffect(() => {
+    soundAlertRef.current = play;
+
+    if (!("Notification" in window)) {
+      console.log("This browser doesn't support notifications");
+    } else {
+      Notification.requestPermission();
+    }
+  }, [play]);
+
+  useEffect(() => {
+    const queueIds = queues.map((q) => q.id);
+    const filteredTickets = tickets.filter(
+      (t) => queueIds.indexOf(t.queueId) > -1
+    );
+
+    if (profile === "user") {
+      setNotifications(filteredTickets);
+    } else {
+      setNotifications(tickets);
+    }
+  }, [tickets, queues, profile]);
+
+  useEffect(() => {
+    ticketIdRef.current = ticketIdUrl;
+  }, [ticketIdUrl]);
+
+  useEffect(() => {
+    const companyId = localStorage.getItem("companyId");
+    const socket = socketConnection({ companyId });
+    if (!socket) {
+      return () => {}; 
+    }
+
+    const queueIds = queues.map((q) => q.id);
+
+    socket.on("connect", () => socket.emit("joinNotification"));
+
+    socket.on(`company-${companyId}-ticket`, (data) => {
+      if (data.action === "updateUnread" || data.action === "delete") {
+        setNotifications((prevState) => {
+          const ticketIndex = prevState.findIndex(
+            (t) => t.id === data.ticketId
+          );
+          if (ticketIndex !== -1) {
+            prevState.splice(ticketIndex, 1);
+            return [...prevState];
+          }
+          return prevState;
+        });
+
+        setDesktopNotifications((prevState) => {
+          const notfiticationIndex = prevState.findIndex(
+            (n) => n.tag === String(data.ticketId)
+          );
+          if (notfiticationIndex !== -1) {
+            prevState[notfiticationIndex].close();
+            prevState.splice(notfiticationIndex, 1);
+            return [...prevState];
+          }
+          return prevState;
+        });
+      }
+    });
+
+    socket.on(`company-${companyId}-appMessage`, (data) => {
+      if (
+        data.action === "create" &&
+        !data.message.read &&
+        (data.ticket.userId === user?.id || !data.ticket.userId)
+      ) {
+        if (
+          profile === "user" &&
+          (queueIds.indexOf(data.ticket.queue?.id) === -1 ||
+            data.ticket.queue === null)
+        ) {
+          return;
+        }
+
+        setNotifications((prevState) => {
+          const ticketIndex = prevState.findIndex(
+            (t) => t.id === data.ticket.id
+          );
+          if (ticketIndex !== -1) {
+            prevState[ticketIndex] = data.ticket;
+            return [...prevState];
+          }
+          return [data.ticket, ...prevState];
+        });
+
+        const shouldNotNotificate =
+          (data.message.ticketId === ticketIdRef.current &&
+            document.visibilityState === "visible") ||
+          (data.ticket.userId && data.ticket.userId !== user?.id) ||
+          data.ticket.isGroup ||
+          data.ticket.isBot;
+
+        if (shouldNotNotificate) return;
+
+        handleNotifications(data);
+      }
+    });
+
+    return () => {
+      socket.disconnect();
+    };
+  }, [user, profile, queues]);
+
+  const handleNotifications = (data) => {
+    const { message, contact, ticket } = data;
+
+    const options = {
+      body: `${message.body} - ${format(new Date(), "HH:mm")}`,
+      icon: contact.profilePicUrl,
+      tag: ticket.id,
+      renotify: true,
+    };
+
+    const notification = new Notification(
+      `${i18n.t("tickets.notification.message")} ${contact.name}`,
+      options
+    );
+
+    notification.onclick = (e) => {
+      e.preventDefault();
+      window.focus();
+      historyRef.current.push(`/tickets/${ticket.uuid}`);
+    };
+
+    setDesktopNotifications((prevState) => {
+      const notfiticationIndex = prevState.findIndex(
+        (n) => n.tag === notification.tag
+      );
+      if (notfiticationIndex !== -1) {
+        prevState[notfiticationIndex] = notification;
+        return [...prevState];
+      }
+      return [notification, ...prevState];
+    });
+
+    soundAlertRef.current();
+  };
+
+  const handleClick = () => {
+    setIsOpen((prevState) => !prevState);
+  };
+
+  const handleClickAway = () => {
+    setIsOpen(false);
+  };
+
+  const NotificationTicket = ({ children }) => {
+    return <div onClick={handleClickAway}>{children}</div>;
+  };
+
+  return (
+    <>
+      <IconButton
+        className={classes.icons}
+        onClick={handleClick}
+        ref={anchorEl}
+        aria-label="Open Notifications"
+        variant="contained"
+      >
+        <Badge
+          overlap="rectangular"
+          badgeContent={notifications.length}
+          classes={{ badge: classes.customBadge }}
+        >
+          <ChatIcon />
+        </Badge>
+      </IconButton>
+      <Popover
+        disableScrollLock
+        open={isOpen}
+        anchorEl={anchorEl.current}
+        anchorOrigin={{
+          vertical: "bottom",
+          horizontal: "right",
+        }}
+        transformOrigin={{
+          vertical: "top",
+          horizontal: "right",
+        }}
+        classes={{ paper: classes.popoverPaper }}
+        onClose={handleClickAway}
+      >
+        <List dense className={classes.tabContainer}>
+          {notifications.length === 0 ? (
+            <ListItem>
+              <ListItemText>{i18n.t("notifications.noTickets")}</ListItemText>
+            </ListItem>
+          ) : (
+            notifications.map((ticket) => (
+              <NotificationTicket key={ticket.id}>
+                <TicketListItem ticket={ticket} />
+              </NotificationTicket>
+            ))
+          )}
+        </List>
+      </Popover>
+    </>
+  );
+};
+
+export default NotificationsPopOver;

+ 110 - 0
frontend/src/components/NotificationsVolume/index.js

@@ -0,0 +1,110 @@
+import React, { useState, useRef } from "react";
+
+import Popover from "@material-ui/core/Popover";
+import IconButton from "@material-ui/core/IconButton";
+import List from "@material-ui/core/List";
+import { makeStyles } from "@material-ui/core/styles";
+import VolumeUpIcon from "@material-ui/icons/VolumeUp";
+import VolumeDownIcon from "@material-ui/icons/VolumeDown";
+
+import { Grid, Slider } from "@material-ui/core";
+
+const useStyles = makeStyles((theme) => ({
+    tabContainer: {
+        padding: theme.spacing(2),
+    },
+    popoverPaper: {
+        width: "100%",
+        maxWidth: 350,
+        marginLeft: theme.spacing(2),
+        marginRight: theme.spacing(1),
+        [theme.breakpoints.down("sm")]: {
+            maxWidth: 270,
+        },
+    },
+    noShadow: {
+        boxShadow: "none !important",
+    },
+    icons: {
+        color: "#fff",
+    },
+    customBadge: {
+        backgroundColor: "#f44336",
+        color: "#fff",
+    },
+}));
+
+const NotificationsVolume = ({ volume, setVolume }) => {
+    const classes = useStyles();
+
+    const anchorEl = useRef();
+    const [isOpen, setIsOpen] = useState(false);
+
+    const handleClick = () => {
+        setIsOpen((prevState) => !prevState);
+    };
+
+    const handleClickAway = () => {
+        setIsOpen(false);
+    };
+
+    const handleVolumeChange = (value) => {
+        setVolume(value);
+        localStorage.setItem("volume", value);
+    };
+
+    return (
+        <>
+            <IconButton
+                className={classes.icons}
+                onClick={handleClick}
+                ref={anchorEl}
+                aria-label="Open Notifications"
+                // color="inherit"
+                // color="secondary"
+            >
+                <VolumeUpIcon color="inherit" />
+            </IconButton>
+            <Popover
+                disableScrollLock
+                open={isOpen}
+                anchorEl={anchorEl.current}
+                anchorOrigin={{
+                    vertical: "bottom",
+                    horizontal: "right",
+                }}
+                transformOrigin={{
+                    vertical: "top",
+                    horizontal: "right",
+                }}
+                classes={{ paper: classes.popoverPaper }}
+                onClose={handleClickAway}
+            >
+                <List dense className={classes.tabContainer}>
+                    <Grid container spacing={2}>
+                        <Grid item>
+                            <VolumeDownIcon />
+                        </Grid>
+                        <Grid item xs>
+                            <Slider
+                                value={volume}
+                                aria-labelledby="continuous-slider"
+                                step={0.1}
+                                min={0}
+                                max={1}
+                                onChange={(e, value) =>
+                                    handleVolumeChange(value)
+                                }
+                            />
+                        </Grid>
+                        <Grid item>
+                            <VolumeUpIcon />
+                        </Grid>
+                    </Grid>
+                </List>
+            </Popover>
+        </>
+    );
+};
+
+export default NotificationsVolume;

+ 9 - 0
frontend/src/components/OnlyForSuperUser/index.js

@@ -0,0 +1,9 @@
+const OnlyForSuperUser = ({ user, yes, no }) => user.super ? yes() : no();
+
+OnlyForSuperUser.defaultProps = {
+    user: {},
+	yes: () => null,
+	no: () => null,
+};
+
+export default OnlyForSuperUser;

+ 30 - 0
frontend/src/components/OutlinedDiv/index.js

@@ -0,0 +1,30 @@
+import React from "react";
+
+import TextField from "@material-ui/core/TextField";
+
+const InputComponent = ({ inputRef, ...other }) => <div {...other} />;
+
+const OutlinedDiv = ({
+  InputProps,
+  children,
+  InputLabelProps,
+  label,
+  ...other
+}) => {
+  return (
+    <TextField
+      {...other}
+      variant="outlined"
+      label={label}
+      multiline
+      InputLabelProps={{ shrink: true, ...InputLabelProps }}
+      InputProps={{
+        inputComponent: InputComponent,
+        ...InputProps
+      }}
+      inputProps={{ children: children }}
+    />
+  );
+};
+
+export default OutlinedDiv;

+ 561 - 0
frontend/src/components/PlansManager/index.js

@@ -0,0 +1,561 @@
+import React, { useState, useEffect } from "react";
+import {
+    makeStyles,
+    Paper,
+    Grid,
+    TextField,
+    Table,
+    TableHead,
+    TableBody,
+    TableCell,
+    TableRow,
+    IconButton,
+    FormControl,
+    InputLabel,
+    MenuItem,
+    Select
+} from "@material-ui/core";
+import { Formik, Form, Field } from 'formik';
+import ButtonWithSpinner from "../ButtonWithSpinner";
+import ConfirmationModal from "../ConfirmationModal";
+
+import { Edit as EditIcon } from "@material-ui/icons";
+
+import { toast } from "react-toastify";
+import usePlans from "../../hooks/usePlans";
+import { i18n } from "../../translate/i18n";
+
+
+const useStyles = makeStyles(theme => ({
+    root: {
+        width: '100%'
+    },
+    mainPaper: {
+        width: '100%',
+        flex: 1,
+        padding: theme.spacing(2)
+    },
+    fullWidth: {
+        width: '100%'
+    },
+    tableContainer: {
+        width: '100%',
+        overflowX: "scroll",
+        ...theme.scrollbarStyles
+    },
+    textfield: {
+        width: '100%'
+    },
+    textRight: {
+        textAlign: 'right'
+    },
+    row: {
+        paddingTop: theme.spacing(2),
+        paddingBottom: theme.spacing(2)
+    },
+    control: {
+        paddingRight: theme.spacing(1),
+        paddingLeft: theme.spacing(1)
+    },
+    buttonContainer: {
+        textAlign: 'right',
+        padding: theme.spacing(1)
+    }
+}));
+
+export function PlanManagerForm(props) {
+    const { onSubmit, onDelete, onCancel, initialValue, loading } = props;
+    const classes = useStyles()
+
+    const [record, setRecord] = useState({
+        name: '',
+        users: 0,
+        connections: 0,
+        queues: 0,
+        value: 0,
+        useCampaigns: true,
+        useSchedules: true,
+        useInternalChat: true,
+        useExternalApi: true,
+        useKanban: true,
+        useOpenAi: true,
+        useIntegrations: true,
+    });
+
+    useEffect(() => {
+        setRecord(initialValue)
+    }, [initialValue])
+
+    const handleSubmit = async (data) => {
+        onSubmit(data)
+    }
+
+    return (
+        <Formik
+            enableReinitialize
+            className={classes.fullWidth}
+            initialValues={record}
+            onSubmit={(values, { resetForm }) =>
+                setTimeout(() => {
+                    handleSubmit(values)
+                    resetForm()
+                }, 500)
+            }
+        >
+            {(values) => (
+                <Form className={classes.fullWidth}>
+                    <Grid spacing={1} justifyContent="flex-start" container>
+                        {/* NOME */}
+                        <Grid xs={12} sm={6} md={2} item>
+                            <Field
+                                as={TextField}
+                                label={i18n.t("plans.form.name")}
+                                name="name"
+                                variant="outlined"
+                                className={classes.fullWidth}
+                                margin="dense"
+                            />
+                        </Grid>
+
+                        {/* USUARIOS */}
+                        <Grid xs={12} sm={6} md={1} item>
+                            <Field
+                                as={TextField}
+                                label={i18n.t("plans.form.users")}
+                                name="users"
+                                variant="outlined"
+                                className={classes.fullWidth}
+                                margin="dense"
+                                type="number"
+                            />
+                        </Grid>
+
+                        {/* CONEXOES */}
+                        <Grid xs={12} sm={6} md={1} item>
+                            <Field
+                                as={TextField}
+                                label={i18n.t("plans.form.connections")}
+                                name="connections"
+                                variant="outlined"
+                                className={classes.fullWidth}
+                                margin="dense"
+                                type="number"
+                            />
+                        </Grid>
+
+                        {/* FILAS */}
+                        <Grid xs={12} sm={6} md={1} item>
+                            <Field
+                                as={TextField}
+                                label={i18n.t("plans.form.queues")}
+                                name="queues"
+                                variant="outlined"
+                                className={classes.fullWidth}
+                                margin="dense"
+                                type="number"
+                            />
+                        </Grid>
+
+                        {/* VALOR */}
+                        <Grid xs={12} sm={6} md={1} item>
+                            <Field
+                                as={TextField}
+                                label={i18n.t("plans.form.value")}
+                                name="value"
+                                variant="outlined"
+                                className={classes.fullWidth}
+                                margin="dense"
+                                type="text"
+                            />
+                        </Grid>
+
+                        {/* CAMPANHAS */}
+                        <Grid xs={12} sm={6} md={2} item>
+                            <FormControl margin="dense" variant="outlined" fullWidth>
+                                <InputLabel htmlFor="useCampaigns-selection">{i18n.t("plans.form.campaigns")}</InputLabel>
+                                <Field
+                                    as={Select}
+                                    id="useCampaigns-selection"
+                                    label={i18n.t("plans.form.campaigns")}
+                                    labelId="useCampaigns-selection-label"
+                                    name="useCampaigns"
+                                    margin="dense"
+                                >
+                                    <MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
+                                    <MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
+                                </Field>
+                            </FormControl>
+                        </Grid>
+
+                        {/* AGENDAMENTOS */}
+                        <Grid xs={12} sm={8} md={2} item>
+                            <FormControl margin="dense" variant="outlined" fullWidth>
+                                <InputLabel htmlFor="useSchedules-selection">{i18n.t("plans.form.schedules")}</InputLabel>
+                                <Field
+                                    as={Select}
+                                    id="useSchedules-selection"
+                                    label={i18n.t("plans.form.schedules")}
+                                    labelId="useSchedules-selection-label"
+                                    name="useSchedules"
+                                    margin="dense"
+                                >
+                                    <MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
+                                    <MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
+                                </Field>
+                            </FormControl>
+                        </Grid>
+
+                        {/* CHAT INTERNO */}
+                        <Grid xs={12} sm={8} md={2} item>
+                            <FormControl margin="dense" variant="outlined" fullWidth>
+                                <InputLabel htmlFor="useInternalChat-selection">{i18n.t("plans.form.internalChat")}</InputLabel>
+                                <Field
+                                    as={Select}
+                                    id="useInternalChat-selection"
+                                    label={i18n.t("plans.form.internalChat")}
+                                    labelId="useInternalChat-selection-label"
+                                    name="useInternalChat"
+                                    margin="dense"
+                                >
+                                    <MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
+                                    <MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
+                                </Field>
+                            </FormControl>
+                        </Grid>
+
+                        {/* API Externa */}
+                        <Grid xs={12} sm={8} md={4} item>
+                            <FormControl margin="dense" variant="outlined" fullWidth>
+                                <InputLabel htmlFor="useExternalApi-selection">{i18n.t("plans.form.externalApi")}</InputLabel>
+                                <Field
+                                    as={Select}
+                                    id="useExternalApi-selection"
+                                    label={i18n.t("plans.form.externalApi")}
+                                    labelId="useExternalApi-selection-label"
+                                    name="useExternalApi"
+                                    margin="dense"
+                                >
+                                    <MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
+                                    <MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
+                                </Field>
+                            </FormControl>
+                        </Grid>
+
+                        {/* KANBAN */}
+                        <Grid xs={12} sm={8} md={2} item>
+                            <FormControl margin="dense" variant="outlined" fullWidth>
+                                <InputLabel htmlFor="useKanban-selection">{i18n.t("plans.form.kanban")}</InputLabel>
+                                <Field
+                                    as={Select}
+                                    id="useKanban-selection"
+                                    label={i18n.t("plans.form.kanban")}
+                                    labelId="useKanban-selection-label"
+                                    name="useKanban"
+                                    margin="dense"
+                                >
+                                    <MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
+                                    <MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
+                                </Field>
+                            </FormControl>
+                        </Grid>
+
+                        {/* OPENAI */}
+                        <Grid xs={12} sm={8} md={2} item>
+                            <FormControl margin="dense" variant="outlined" fullWidth>
+                                <InputLabel htmlFor="useOpenAi-selection">Open.Ai</InputLabel>
+                                <Field
+                                    as={Select}
+                                    id="useOpenAi-selection"
+                                    label="Talk.Ai"
+                                    labelId="useOpenAi-selection-label"
+                                    name="useOpenAi"
+                                    margin="dense"
+                                >
+                                    <MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
+                                    <MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
+                                </Field>
+                            </FormControl>
+                        </Grid>
+
+                        {/* INTEGRACOES */}
+                        <Grid xs={12} sm={8} md={2} item>
+                            <FormControl margin="dense" variant="outlined" fullWidth>
+                                <InputLabel htmlFor="useIntegrations-selection">
+                                    {i18n.t("plans.form.integrations")}
+                                </InputLabel>
+                                <Field
+                                    as={Select}
+                                    id="useIntegrations-selection"
+                                    label={i18n.t("plans.form.integrations")}
+                                    labelId="useIntegrations-selection-label"
+                                    name="useIntegrations"
+                                    margin="dense"
+                                >
+                                    <MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
+                                    <MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
+                                </Field>
+                            </FormControl>
+                        </Grid>
+                    </Grid>
+                    <Grid spacing={2} justifyContent="flex-end" container>
+
+                        <Grid sm={3} md={2} item>
+                            <ButtonWithSpinner className={classes.fullWidth} loading={loading} onClick={() => onCancel()} variant="contained">
+                                {i18n.t("plans.form.clear")}
+                            </ButtonWithSpinner>
+                        </Grid>
+                        {record.id !== undefined ? (
+                            <Grid sm={3} md={2} item>
+                                <ButtonWithSpinner className={classes.fullWidth} loading={loading} onClick={() => onDelete(record)} variant="contained" color="secondary">
+                                    {i18n.t("plans.form.delete")}
+                                </ButtonWithSpinner>
+                            </Grid>
+                        ) : null}
+                        <Grid sm={3} md={2} item>
+                            <ButtonWithSpinner className={classes.fullWidth} loading={loading} type="submit" variant="contained" color="primary">
+                                {i18n.t("plans.form.save")}
+                            </ButtonWithSpinner>
+                        </Grid>
+                    </Grid>
+                </Form>
+            )}
+        </Formik>
+    )
+}
+
+export function PlansManagerGrid(props) {
+    const { records, onSelect } = props
+    const classes = useStyles()
+    
+    const renderCampaigns = (row) => {
+        return row.useCampaigns === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
+    };
+
+    const renderSchedules = (row) => {
+        return row.useSchedules === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
+    };
+
+    const renderInternalChat = (row) => {
+        return row.useInternalChat === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
+    };
+
+    const renderExternalApi = (row) => {
+        return row.useExternalApi === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
+    };
+
+    const renderKanban = (row) => {
+        return row.useKanban === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
+    };
+
+    const renderOpenAi = (row) => {
+        return row.useOpenAi === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
+    };
+
+    const renderIntegrations = (row) => {
+        return row.useIntegrations === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
+    };
+
+    return (
+        <Paper className={classes.tableContainer}>
+            <Table
+                className={classes.fullWidth}
+                // size="small"
+                padding="none"
+                aria-label="a dense table"
+            >
+                <TableHead>
+                    <TableRow>
+                        <TableCell align="center" style={{ width: '1%' }}>#</TableCell>
+                        <TableCell align="left">{i18n.t("plans.form.name")}</TableCell>
+                        <TableCell align="center">{i18n.t("plans.form.users")}</TableCell>
+                        <TableCell align="center">{i18n.t("plans.form.connections")}</TableCell>
+                        <TableCell align="center">{i18n.t("plans.form.queues")}</TableCell>
+                        <TableCell align="center">{i18n.t("plans.form.value")}</TableCell>
+                        <TableCell align="center">{i18n.t("plans.form.campaigns")}</TableCell>
+                        <TableCell align="center">{i18n.t("plans.form.schedules")}</TableCell>
+                        <TableCell align="center">{i18n.t("plans.form.internalChat")}</TableCell>
+                        <TableCell align="center">{i18n.t("plans.form.externalApi")}</TableCell>
+                        <TableCell align="center">{i18n.t("plans.form.kanban")}</TableCell>
+                        <TableCell align="center">Open.Ai</TableCell>
+                        <TableCell align="center">{i18n.t("plans.form.integrations")}</TableCell>
+                    </TableRow>
+                </TableHead>
+                <TableBody>
+                    {records.map((row) => (
+                        <TableRow key={row.id}>
+                            <TableCell align="center" style={{ width: '1%' }}>
+                                <IconButton onClick={() => onSelect(row)} aria-label="delete">
+                                    <EditIcon />
+                                </IconButton>
+                            </TableCell>
+                            <TableCell align="left">{row.name || '-'}</TableCell>
+                            <TableCell align="center">{row.users || '-'}</TableCell>
+                            <TableCell align="center">{row.connections || '-'}</TableCell>
+                            <TableCell align="center">{row.queues || '-'}</TableCell>
+                            <TableCell align="center">{i18n.t("plans.form.money")} {row.value ? row.value.toLocaleString('pt-br', { minimumFractionDigits: 2 }) : '00.00'}</TableCell>
+                            <TableCell align="center">{renderCampaigns(row)}</TableCell>
+                            <TableCell align="center">{renderSchedules(row)}</TableCell>
+                            <TableCell align="center">{renderInternalChat(row)}</TableCell>
+                            <TableCell align="center">{renderExternalApi(row)}</TableCell>
+                            <TableCell align="center">{renderKanban(row)}</TableCell>
+                            <TableCell align="center">{renderOpenAi(row)}</TableCell>
+                            <TableCell align="center">{renderIntegrations(row)}</TableCell>
+                        </TableRow>
+                    ))}
+                </TableBody>
+            </Table>
+        </Paper>
+    )
+}
+
+export default function PlansManager() {
+    const classes = useStyles()
+    const { list, save, update, remove } = usePlans()
+
+    const [showConfirmDialog, setShowConfirmDialog] = useState(false)
+    const [loading, setLoading] = useState(false)
+    const [records, setRecords] = useState([])
+    const [record, setRecord] = useState({
+        name: '',
+        users: 0,
+        connections: 0,
+        queues: 0,
+        value: 0,
+        useCampaigns: true,
+        useSchedules: true,
+        useInternalChat: true,
+        useExternalApi: true,
+        useKanban: true,
+        useOpenAi: true,
+        useIntegrations: true,
+    })
+
+    useEffect(() => {
+        async function fetchData() {
+            await loadPlans()
+        }
+        fetchData()
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [record])
+
+    const loadPlans = async () => {
+        setLoading(true)
+        try {
+            const planList = await list()
+            setRecords(planList)
+        } catch (e) {
+            toast.error(i18n.t("plans.toasts.errorList"))
+        }
+        setLoading(false)
+    }
+
+    const handleSubmit = async (data) => {
+        setLoading(true)
+        console.log(data)
+        try {
+            if (data.id !== undefined) {
+                await update(data)
+            } else {
+                await save(data)
+            }
+            await loadPlans()
+            handleCancel()
+            toast.success(i18n.t("plans.toasts.success"))
+        } catch (e) {
+            toast.error(i18n.t("plans.toasts.error"))
+        }
+        setLoading(false)
+    }
+
+    const handleDelete = async () => {
+        setLoading(true)
+        try {
+            await remove(record.id)
+            await loadPlans()
+            handleCancel()
+            toast.success(i18n.t("plans.toasts.success"))
+        } catch (e) {
+            toast.error(i18n.t("plans.toasts.errorOperation"))
+        }
+        setLoading(false)
+    }
+
+    const handleOpenDeleteDialog = () => {
+        setShowConfirmDialog(true)
+    }
+
+    const handleCancel = () => {
+        setRecord({
+            id: undefined,
+            name: '',
+            users: 0,
+            connections: 0,
+            queues: 0,
+            value: 0,
+            useCampaigns: true,
+            useSchedules: true,
+            useInternalChat: true,
+            useExternalApi: true,
+            useKanban: true,
+            useOpenAi: true,
+            useIntegrations: true
+        })
+    }
+
+    const handleSelect = (data) => {
+
+        let useCampaigns = data.useCampaigns === false ? false : true
+        let useSchedules = data.useSchedules === false ? false : true
+        let useInternalChat = data.useInternalChat === false ? false : true
+        let useExternalApi = data.useExternalApi === false ? false : true
+        let useKanban = data.useKanban === false ? false : true
+        let useOpenAi = data.useOpenAi === false ? false : true
+        let useIntegrations = data.useIntegrations === false ? false : true
+
+        setRecord({
+            id: data.id,
+            name: data.name || '',
+            users: data.users || 0,
+            connections: data.connections || 0,
+            queues: data.queues || 0,
+            value: data.value?.toLocaleString('pt-br', { minimumFractionDigits: 0 }) || 0,
+            useCampaigns,
+            useSchedules,
+            useInternalChat,
+            useExternalApi,
+            useKanban,
+            useOpenAi,
+            useIntegrations
+        })
+    }
+
+    return (
+        <Paper className={classes.mainPaper} elevation={0}>
+            <Grid spacing={2} container>
+                <Grid xs={12} item>
+                    <PlanManagerForm
+                        initialValue={record}
+                        onDelete={handleOpenDeleteDialog}
+                        onSubmit={handleSubmit}
+                        onCancel={handleCancel}
+                        loading={loading}
+                    />
+                </Grid>
+                <Grid xs={12} item>
+                    <PlansManagerGrid
+                        records={records}
+                        onSelect={handleSelect}
+                    />
+                </Grid>
+            </Grid>
+            <ConfirmationModal
+                title={i18n.t("plans.confirm.title")}
+                open={showConfirmDialog}
+                onClose={() => setShowConfirmDialog(false)}
+                onConfirm={() => handleDelete()}
+            >
+                {i18n.t("plans.confirm.message")}
+            </ConfirmationModal>
+        </Paper>
+    )
+}

+ 312 - 0
frontend/src/components/PromptModal/index.js

@@ -0,0 +1,312 @@
+import React, { useState, useEffect } from "react";
+
+import * as Yup from "yup";
+import { Formik, Form, Field } from "formik";
+import { toast } from "react-toastify";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import CircularProgress from "@material-ui/core/CircularProgress";
+import { i18n } from "../../translate/i18n";
+import { MenuItem, FormControl, InputLabel, Select, Menu, Grid } from "@material-ui/core";
+import { Visibility, VisibilityOff } from "@material-ui/icons";
+import { InputAdornment, IconButton } from "@material-ui/core";
+import QueueSelectSingle from "../../components/QueueSelectSingle";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+
+const useStyles = makeStyles(theme => ({
+    root: {
+        display: "flex",
+        flexWrap: "wrap",
+    },
+    multFieldLine: {
+        display: "flex",
+        "& > *:not(:last-child)": {
+            marginRight: theme.spacing(1),
+        },
+    },
+
+    btnWrapper: {
+        position: "relative",
+    },
+
+    buttonProgress: {
+        color: green[500],
+        position: "absolute",
+        top: "50%",
+        left: "50%",
+        marginTop: -12,
+        marginLeft: -12,
+    },
+    formControl: {
+        margin: theme.spacing(1),
+        minWidth: 120,
+    },
+    colorAdorment: {
+        width: 20,
+        height: 20,
+    },
+}));
+
+const PromptSchema = Yup.object().shape({
+    name: Yup.string().min(5, i18n.t("promptModal.formErrors.name.short")).max(100, i18n.t("promptModal.formErrors.name.long")).required(i18n.t("promptModal.formErrors.name.required")),
+    prompt: Yup.string().min(50, i18n.t("promptModal.formErrors.prompt.short")).required(i18n.t("promptModal.formErrors.prompt.required")),
+    model: Yup.string().required(i18n.t("promptModal.formErrors.modal.required")),
+    maxTokens: Yup.number().required(i18n.t("promptModal.formErrors.maxTokens.required")),
+    temperature: Yup.number().required(i18n.t("promptModal.formErrors.temperature.required")),
+    apiKey: Yup.string().required(i18n.t("promptModal.formErrors.apikey.required")),
+    queueId: Yup.number().required(i18n.t("promptModal.formErrors.queueId.required")),
+    maxMessages: Yup.number().required(i18n.t("promptModal.formErrors.maxMessages.required"))
+});
+
+const PromptModal = ({ open, onClose, promptId, refreshPrompts }) => {
+    const classes = useStyles();
+    const [selectedModel, setSelectedModel] = useState("gpt-3.5-turbo-1106");
+    const [showApiKey, setShowApiKey] = useState(false);
+
+    const handleToggleApiKey = () => {
+        setShowApiKey(!showApiKey);
+    };
+
+    const initialState = {
+        name: "",
+        prompt: "",
+        model: "gpt-3.5-turbo-1106",
+        maxTokens: 100,
+        temperature: 1,
+        apiKey: "",
+        queueId: '',
+        maxMessages: 10
+    };
+
+    const [prompt, setPrompt] = useState(initialState);
+
+    useEffect(() => {
+        const fetchPrompt = async () => {
+            if (!promptId) {
+                setPrompt(initialState);
+                return;
+            }
+            try {
+                const { data } = await api.get(`/prompt/${promptId}`);
+                setPrompt(prevState => {
+                    return { ...prevState, ...data };
+                });
+                
+                setSelectedModel(data.model);
+            } catch (err) { 
+                toastError(err);
+            }
+        };
+
+        fetchPrompt();
+    }, [promptId, open]);
+
+    const handleClose = () => {
+        setPrompt(initialState);
+        setSelectedModel("gpt-3.5-turbo-1106");
+        onClose();
+    };
+
+    const handleChangeModel = (e) => {
+        setSelectedModel(e.target.value);
+    };
+
+    const handleSavePrompt = async values => {
+        const promptData = { ...values, model: selectedModel };
+        console.log(promptData);
+        if (!values.queueId) {
+            toastError(i18n.t("promptModal.setor"));
+            return;
+        }
+        try {
+            if (promptId) {
+                await api.put(`/prompt/${promptId}`, promptData);
+            } else {
+                await api.post("/prompt", promptData);
+            }
+            toast.success(i18n.t("promptModal.success"));
+            refreshPrompts(  )
+        } catch (err) {
+            toastError(err);
+        }
+        handleClose();
+    };
+
+    return (
+        <div className={classes.root}>
+            <Dialog
+                open={open}
+                onClose={handleClose}
+                maxWidth="md"
+                scroll="paper"
+                fullWidth
+            >
+                <DialogTitle id="form-dialog-title">
+                    {promptId
+                        ? `${i18n.t("promptModal.title.edit")}`
+                        : `${i18n.t("promptModal.title.add")}`}
+                </DialogTitle>
+                <Formik
+                    initialValues={prompt}
+                    enableReinitialize={true}
+                    validationSchema={PromptSchema}
+                    onSubmit={(values, actions) => {
+                        setTimeout(() => {
+                            handleSavePrompt(values);
+                            actions.setSubmitting(false);
+                        }, 400);
+                    }}
+                >
+                    {({ touched, errors, isSubmitting, values }) => (
+                        <Form style={{ width: "100%" }}>
+                            <DialogContent dividers>
+                                <Field
+                                    as={TextField}
+                                    label={i18n.t("promptModal.form.name")}
+                                    name="name"
+                                    error={touched.name && Boolean(errors.name)}
+                                    helperText={touched.name && errors.name}
+                                    variant="outlined"
+                                    margin="dense"
+                                    fullWidth
+                                />
+                                <FormControl fullWidth margin="dense" variant="outlined">
+                                    <Field
+                                        as={TextField}
+                                        label={i18n.t("promptModal.form.apikey")}
+                                        name="apiKey"
+                                        type={showApiKey ? 'text' : 'password'}
+                                        error={touched.apiKey && Boolean(errors.apiKey)}
+                                        helperText={touched.apiKey && errors.apiKey}
+                                        variant="outlined"
+                                        margin="dense"
+                                        fullWidth
+                                        InputProps={{
+                                            endAdornment: (
+                                                <InputAdornment position="end">
+                                                    <IconButton onClick={handleToggleApiKey}>
+                                                        {showApiKey ? <VisibilityOff /> : <Visibility />}
+                                                    </IconButton>
+                                                </InputAdornment>
+                                            ),
+                                        }}
+                                    />
+                                </FormControl>
+                                <Field
+                                    as={TextField}
+                                    label={i18n.t("promptModal.form.prompt")}
+                                    name="prompt"
+                                    error={touched.prompt && Boolean(errors.prompt)}
+                                    helperText={touched.prompt && errors.prompt}
+                                    variant="outlined"
+                                    margin="dense"
+                                    fullWidth
+                                    rows={10}
+                                    multiline={true}
+                                />
+                                <QueueSelectSingle touched={touched} errors={errors}/>
+                                <div className={classes.multFieldLine}>
+                                    <FormControl fullWidth margin="dense" variant="outlined">
+                                    <InputLabel>{i18n.t("promptModal.form.model")}</InputLabel>
+                                        <Select
+                                            id="type-select"
+                                            labelWidth={60}
+                                            name="model"
+                                            value={selectedModel}
+                                            onChange={handleChangeModel}
+                                            multiple={false}
+                                        >
+                                            <MenuItem key={"gpt-3.5"} value={"gpt-3.5-turbo-1106"}>
+                                                GPT 3.5 turbo
+                                            </MenuItem>
+                                            <MenuItem key={"gpt-4"} value={"gpt-4o-mini"}>
+                                                GPT 4.0
+                                            </MenuItem>
+                                        </Select>
+                                    </FormControl>
+                                    <Field
+                                        as={TextField}
+                                        label={i18n.t("promptModal.form.temperature")}
+                                        name="temperature"
+                                        error={touched.temperature && Boolean(errors.temperature)}
+                                        helperText={touched.temperature && errors.temperature}
+                                        variant="outlined"
+                                        margin="dense"
+                                        fullWidth
+                                        type="number"
+                                        inputProps={{
+                                            step: "0.1",
+                                            min: "0",
+                                            max: "1"
+                                        }}
+                                    />
+                                </div>
+                                
+                                <div className={classes.multFieldLine}>
+                                    <Field
+                                        as={TextField}
+                                        label={i18n.t("promptModal.form.max_tokens")}
+                                        name="maxTokens"
+                                        error={touched.maxTokens && Boolean(errors.maxTokens)}
+                                        helperText={touched.maxTokens && errors.maxTokens}
+                                        variant="outlined"
+                                        margin="dense"
+                                        fullWidth
+                                    />
+                                    <Field
+                                        as={TextField}
+                                        label={i18n.t("promptModal.form.max_messages")}
+                                        name="maxMessages"
+                                        error={touched.maxMessages && Boolean(errors.maxMessages)}
+                                        helperText={touched.maxMessages && errors.maxMessages}
+                                        variant="outlined"
+                                        margin="dense"
+                                        fullWidth
+                                    />
+                                </div>
+                            </DialogContent>
+                            <DialogActions>
+                                <Button
+                                    onClick={handleClose}
+                                    color="secondary"
+                                    disabled={isSubmitting}
+                                    variant="outlined"
+                                >
+                                    {i18n.t("promptModal.buttons.cancel")}
+                                </Button>
+                                <Button
+                                    type="submit"
+                                    color="primary"
+                                    disabled={isSubmitting}
+                                    variant="contained"
+                                    className={classes.btnWrapper}
+                                >
+                                    {promptId
+                                        ? `${i18n.t("promptModal.buttons.okEdit")}`
+                                        : `${i18n.t("promptModal.buttons.okAdd")}`}
+                                    {isSubmitting && (
+                                        <CircularProgress
+                                            size={24}
+                                            className={classes.buttonProgress}
+                                        />
+                                    )}
+                                </Button>
+                            </DialogActions>
+                        </Form>
+                    )}
+                </Formik>
+            </Dialog>
+        </div>
+    );
+};
+
+export default PromptModal;

+ 84 - 0
frontend/src/components/QrcodeModal/index.js

@@ -0,0 +1,84 @@
+import React, { useEffect, useState, useContext } from "react";
+import QRCode from "qrcode.react";
+import toastError from "../../errors/toastError";
+
+import { Dialog, DialogContent, Paper, Typography, useTheme } from "@material-ui/core";
+import { i18n } from "../../translate/i18n";
+import api from "../../services/api";
+import { SocketContext } from "../../context/Socket/SocketContext";
+
+const QrcodeModal = ({ open, onClose, whatsAppId }) => {
+  const [qrCode, setQrCode] = useState("");
+  const theme = useTheme();
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    const fetchSession = async () => {
+      if (!whatsAppId) return;
+
+      try {
+        const { data } = await api.get(`/whatsapp/${whatsAppId}`);
+        setQrCode(data.qrcode);
+      } catch (err) {
+        toastError(err);
+      }
+    };
+    fetchSession();
+  }, [whatsAppId]);
+
+  useEffect(() => {
+    if (!whatsAppId) return;
+    const companyId = localStorage.getItem("companyId");
+    const socket = socketManager.getSocket(companyId);
+
+    socket.on(`company-${companyId}-whatsappSession`, (data) => {
+      if (data.action === "update" && data.session.id === whatsAppId) {
+        setQrCode(data.session.qrcode);
+      }
+
+      if (data.action === "update" && data.session.qrcode === "") {
+        onClose();
+      }
+    });
+
+    return () => {
+      socket.disconnect();
+    };
+  }, [whatsAppId, onClose, socketManager]);
+
+  return (
+    <Dialog open={open} onClose={onClose} maxWidth="lg" scroll="paper">
+      <DialogContent>
+        <Paper elevation={0} style={{ display: "flex", alignItems: "center" }}>
+          <div style={{ marginRight: "20px" }}>
+            <Typography variant="h2" component="h2" color="textPrimary" gutterBottom style={{ fontFamily: "Montserrat", fontWeight: "bold", fontSize:"20px",}}>
+              {i18n.t("qrCodeModal.title")}
+            </Typography>
+            <Typography variant="body1" color="textPrimary" gutterBottom>
+              {i18n.t("qrCodeModal.steps.one")}
+            </Typography>
+            <Typography variant="body1" color="textPrimary" gutterBottom>
+              {i18n.t("qrCodeModal.steps.two.partOne")} <svg class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path></svg> {i18n.t("qrCodeModal.steps.two.partTwo")} <svg class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"></path></svg> {i18n.t("qrCodeModal.steps.two.partThree")}
+            </Typography>
+            <Typography variant="body1" color="textPrimary" gutterBottom>
+              {i18n.t("qrCodeModal.steps.three")}
+            </Typography>
+            <Typography variant="body1" color="textPrimary" gutterBottom>
+              {i18n.t("qrCodeModal.steps.four")}
+            </Typography>
+          </div>
+          <div>
+            {qrCode ? (
+              <QRCode value={qrCode} size={256} />
+            ) : (
+              <span>{i18n.t("qrCodeModal.waiting")}</span>
+            )}
+          </div>
+        </Paper>
+      </DialogContent>
+    </Dialog>
+  );
+};
+
+export default React.memo(QrcodeModal);

+ 505 - 0
frontend/src/components/QueueIntegrationModal/index.js

@@ -0,0 +1,505 @@
+import React, { useState, useEffect } from "react";
+
+import * as Yup from "yup";
+import { Formik, Form, Field } from "formik";
+import { toast } from "react-toastify";
+
+import {
+  Button,
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+  CircularProgress,
+  Select,
+  InputLabel,
+  MenuItem,
+  FormControl,
+  TextField,
+  Grid,
+  Paper,
+} from "@material-ui/core";
+
+import { makeStyles } from "@material-ui/core/styles";
+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: {
+    display: "flex",
+    flexWrap: "wrap",
+    gap: 4
+  },
+  textField: {
+    marginRight: theme.spacing(1),
+    flex: 1,
+  },
+
+  btnWrapper: {
+    position: "relative",
+  },
+
+  buttonProgress: {
+    color: green[500],
+    position: "absolute",
+    top: "50%",
+    left: "50%",
+    marginTop: -12,
+    marginLeft: -12,
+  },
+  btnLeft: {
+    display: "flex",
+    marginRight: "auto",
+    marginLeft: 12,
+  },
+  colorAdorment: {
+    width: 20,
+    height: 20,
+  },
+}));
+
+const DialogflowSchema = Yup.object().shape({
+  name: Yup.string()
+    .min(2, "Too Short!")
+    .max(50, "Too Long!")
+    .required("Required"),
+  // projectName: Yup.string()
+  //   .min(3, "Too Short!")
+  //   .max(100, "Too Long!")
+  //   .required(),
+  // jsonContent: Yup.string().min(3, "Too Short!").required(),
+  // language: Yup.string().min(2, "Too Short!").max(50, "Too Long!").required(),
+});
+
+
+
+const QueueIntegration = ({ open, onClose, integrationId }) => {
+  const classes = useStyles();
+
+  const initialState = {
+    type: "typebot",
+    name: "",
+    projectName: "",
+    jsonContent: "",
+    language: "",
+    urlN8N: "",
+    typebotDelayMessage: 1000,
+    typebotExpires: 1,
+    typebotKeywordFinish: "",
+    typebotKeywordRestart: "",
+    typebotRestartMessage: "",
+    typebotSlug: "",
+    typebotUnknownMessage: "",
+
+  };
+
+  const [integration, setIntegration] = useState(initialState);
+
+  useEffect(() => {
+    (async () => {
+      if (!integrationId) return;
+      try {
+        const { data } = await api.get(`/queueIntegration/${integrationId}`);
+        setIntegration((prevState) => {
+          return { ...prevState, ...data };
+        });
+      } catch (err) {
+        toastError(err);
+      }
+    })();
+
+    return () => {
+      setIntegration({
+        type: "dialogflow",
+        name: "",
+        projectName: "",
+        jsonContent: "",
+        language: "",
+        urlN8N: "",
+        typebotDelayMessage: 1000
+      });
+    };
+
+  }, [integrationId, open]);
+
+  const handleClose = () => {
+    onClose();
+    setIntegration(initialState);
+  };
+
+  const handleTestSession = async (event, values) => {
+    try {
+      const { projectName, jsonContent, language } = values;
+
+      await api.post(`/queueIntegration/testSession`, {
+        projectName,
+        jsonContent,
+        language,
+      });
+
+      toast.success(i18n.t("queueIntegrationModal.messages.testSuccess"));
+    } catch (err) {
+      toastError(err);
+    }
+  };
+
+  const handleSaveDialogflow = async (values) => {
+    try {
+      if (values.type === 'n8n' || values.type === 'webhook' || values.type === 'typebot') values.projectName = values.name
+      if (integrationId) {
+        await api.put(`/queueIntegration/${integrationId}`, values);
+        toast.success(i18n.t("queueIntegrationModal.messages.editSuccess"));
+      } else {
+        await api.post("/queueIntegration", values);
+        toast.success(i18n.t("queueIntegrationModal.messages.addSuccess"));
+      }
+      handleClose();
+    } catch (err) {
+      toastError(err);
+    }
+  };
+
+  return (
+    <div className={classes.root}>
+      <Dialog open={open} onClose={handleClose} fullWidth maxWidth="md" scroll="paper">
+        <DialogTitle>
+          {integrationId
+            ? `${i18n.t("queueIntegrationModal.title.edit")}`
+            : `${i18n.t("queueIntegrationModal.title.add")}`}
+        </DialogTitle>
+        <Formik
+          initialValues={integration}
+          enableReinitialize={true}
+          validationSchema={DialogflowSchema}
+          onSubmit={(values, actions, event) => {
+            setTimeout(() => {
+              handleSaveDialogflow(values);
+              actions.setSubmitting(false);
+            }, 400);
+          }}
+        >
+          {({ touched, errors, isSubmitting, values }) => (
+            <Form>
+              <Paper square className={classes.mainPaper} elevation={1}>
+                <DialogContent dividers>
+                  <Grid container spacing={1}>
+                    <Grid item xs={12} md={6} xl={6}>
+                      <FormControl
+                        variant="outlined"
+                        className={classes.formControl}
+                        margin="dense"
+                        fullWidth
+                      >
+                        <InputLabel id="type-selection-input-label">
+                          {i18n.t("queueIntegrationModal.form.type")}
+                        </InputLabel>
+
+                        <Field
+                          as={Select}
+                          label={i18n.t("queueIntegrationModal.form.type")}
+                          name="type"
+                          labelId="profile-selection-label"
+                          error={touched.type && Boolean(errors.type)}
+                          helpertext={touched.type && errors.type}
+                          id="type"
+                          required
+                        >
+                          <MenuItem value="dialogflow">DialogFlow</MenuItem>
+                          <MenuItem value="n8n">N8N</MenuItem>
+                          <MenuItem value="webhook">WebHooks</MenuItem>
+                          <MenuItem value="typebot">Typebot</MenuItem>
+                        </Field>
+                      </FormControl>
+                    </Grid>
+                    {values.type === "dialogflow" && (
+                      <>
+                        <Grid item xs={12} md={6} xl={6} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.name")}
+                            autoFocus
+                            name="name"
+                            fullWidth
+                            error={touched.name && Boolean(errors.name)}
+                            helpertext={touched.name && errors.name}
+                            variant="outlined"
+                            margin="dense"
+                            className={classes.textField}
+                          />
+                        </Grid>
+                        <Grid item xs={12} md={6} xl={6} >
+                          <FormControl
+                            variant="outlined"
+                            className={classes.formControl}
+                            margin="dense"
+                            fullWidth
+                          >
+                            <InputLabel id="language-selection-input-label">
+                              {i18n.t("queueIntegrationModal.form.language")}
+                            </InputLabel>
+
+                            <Field
+                              as={Select}
+                              label={i18n.t("queueIntegrationModal.form.language")}
+                              name="language"
+                              labelId="profile-selection-label"
+                              fullWidth
+                              error={touched.language && Boolean(errors.language)}
+                              helpertext={touched.language && errors.language}
+                              id="language-selection"
+                              required
+                            >
+                              <MenuItem value="pt-BR">Portugues</MenuItem>
+                              <MenuItem value="en">Inglês</MenuItem>
+                              <MenuItem value="es">Español</MenuItem>
+                            </Field>
+                          </FormControl>
+                        </Grid>
+                        <Grid item xs={12} md={6} xl={6} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.projectName")}
+                            name="projectName"
+                            error={touched.projectName && Boolean(errors.projectName)}
+                            helpertext={touched.projectName && errors.projectName}
+                            fullWidth
+                            variant="outlined"
+                            margin="dense"
+                          />
+                        </Grid>
+                        <Grid item xs={12} md={12} xl={12} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.jsonContent")}
+                            type="jsonContent"
+                            multiline
+                            //inputRef={greetingRef}
+                            maxRows={5}
+                            minRows={5}
+                            fullWidth
+                            name="jsonContent"
+                            error={touched.jsonContent && Boolean(errors.jsonContent)}
+                            helpertext={touched.jsonContent && errors.jsonContent}
+                            variant="outlined"
+                            margin="dense"
+                          />
+                        </Grid>
+                      </>
+                    )}
+
+                    {(values.type === "n8n" || values.type === "webhook") && (
+                      <>
+                        <Grid item xs={12} md={6} xl={6} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.name")}
+                            autoFocus
+                            required
+                            name="name"
+                            error={touched.name && Boolean(errors.name)}
+                            helpertext={touched.name && errors.name}
+                            variant="outlined"
+                            margin="dense"
+                            fullWidth
+                            className={classes.textField}
+                          />
+                        </Grid>
+                        <Grid item xs={12} md={12} xl={12} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.urlN8N")}
+                            name="urlN8N"
+                            error={touched.urlN8N && Boolean(errors.urlN8N)}
+                            helpertext={touched.urlN8N && errors.urlN8N}
+                            variant="outlined"
+                            margin="dense"
+                            required
+                            fullWidth
+                            className={classes.textField}
+                          />
+                        </Grid>
+                      </>
+                    )}
+                    {(values.type === "typebot") && (
+                      <>
+                        <Grid item xs={12} md={6} xl={6} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.name")}
+                            autoFocus
+                            name="name"
+                            error={touched.name && Boolean(errors.name)}
+                            helpertext={touched.name && errors.name}
+                            variant="outlined"
+                            margin="dense"
+                            required
+                            fullWidth
+                            className={classes.textField}
+                          />
+                        </Grid>
+                        <Grid item xs={12} md={12} xl={12} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.urlN8N")}
+                            name="urlN8N"
+                            error={touched.urlN8N && Boolean(errors.urlN8N)}
+                            helpertext={touched.urlN8N && errors.urlN8N}
+                            variant="outlined"
+                            margin="dense"
+                            required
+                            fullWidth
+                            className={classes.textField}
+                          />
+                        </Grid>
+                        <Grid item xs={12} md={6} xl={6} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.typebotSlug")}
+                            name="typebotSlug"
+                            error={touched.typebotSlug && Boolean(errors.typebotSlug)}
+                            helpertext={touched.typebotSlug && errors.typebotSlug}
+                            required
+                            variant="outlined"
+                            margin="dense"
+                            fullWidth
+                            className={classes.textField}
+                          />
+                        </Grid>
+                        <Grid item xs={12} md={6} xl={6} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.typebotExpires")}
+                            name="typebotExpires"
+                            error={touched.typebotExpires && Boolean(errors.typebotExpires)}
+                            helpertext={touched.typebotExpires && errors.typebotExpires}
+                            variant="outlined"
+                            margin="dense"
+                            fullWidth
+                            className={classes.textField}
+                          />
+                        </Grid>
+                        <Grid item xs={12} md={6} xl={6} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.typebotDelayMessage")}
+                            name="typebotDelayMessage"
+                            error={touched.typebotDelayMessage && Boolean(errors.typebotDelayMessage)}
+                            helpertext={touched.typebotDelayMessage && errors.typebotDelayMessage}
+                            variant="outlined"
+                            margin="dense"
+                            fullWidth
+                            className={classes.textField}
+                          />
+                        </Grid>
+                        <Grid item xs={12} md={6} xl={6} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.typebotKeywordFinish")}
+                            name="typebotKeywordFinish"
+                            error={touched.typebotKeywordFinish && Boolean(errors.typebotKeywordFinish)}
+                            helpertext={touched.typebotKeywordFinish && errors.typebotKeywordFinish}
+                            variant="outlined"
+                            margin="dense"
+                            fullWidth
+                            className={classes.textField}
+                          />
+                        </Grid>
+                        <Grid item xs={12} md={6} xl={6} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.typebotKeywordRestart")}
+                            name="typebotKeywordRestart"
+                            error={touched.typebotKeywordRestart && Boolean(errors.typebotKeywordRestart)}
+                            helpertext={touched.typebotKeywordRestart && errors.typebotKeywordRestart}
+                            variant="outlined"
+                            margin="dense"
+                            fullWidth
+                            className={classes.textField}
+                          />
+                        </Grid>
+                        <Grid item xs={12} md={6} xl={6} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.typebotUnknownMessage")}
+                            name="typebotUnknownMessage"
+                            error={touched.typebotUnknownMessage && Boolean(errors.typebotUnknownMessage)}
+                            helpertext={touched.typebotUnknownMessage && errors.typebotUnknownMessage}
+                            variant="outlined"
+                            margin="dense"
+                            fullWidth
+                            className={classes.textField}
+                          />
+                        </Grid>
+                        <Grid item xs={12} md={12} xl={12} >
+                          <Field
+                            as={TextField}
+                            label={i18n.t("queueIntegrationModal.form.typebotRestartMessage")}
+                            name="typebotRestartMessage"
+                            error={touched.typebotRestartMessage && Boolean(errors.typebotRestartMessage)}
+                            helpertext={touched.typebotRestartMessage && errors.typebotRestartMessage}
+                            variant="outlined"
+                            margin="dense"
+                            fullWidth
+                            className={classes.textField}
+                          />
+                        </Grid>
+                        
+                      </>
+                    )}
+                  </Grid>
+                </DialogContent>
+              </Paper>
+
+              <DialogActions>
+                {values.type === "dialogflow" && (
+                  <Button
+                    //type="submit"
+                    onClick={(e) => handleTestSession(e, values)}
+                    color="inherit"
+                    disabled={isSubmitting}
+                    name="testSession"
+                    variant="outlined"
+                    className={classes.btnLeft}
+                  >
+                    {i18n.t("queueIntegrationModal.buttons.test")}
+                  </Button>
+                )}
+                <Button
+                  onClick={handleClose}
+                  color="secondary"
+                  disabled={isSubmitting}
+                  variant="outlined"
+                >
+                  {i18n.t("queueIntegrationModal.buttons.cancel")}
+                </Button>
+                <Button
+                  type="submit"
+                  color="primary"
+                  disabled={isSubmitting}
+                  variant="contained"
+                  className={classes.btnWrapper}
+                >
+                  {integrationId
+                    ? `${i18n.t("queueIntegrationModal.buttons.okEdit")}`
+                    : `${i18n.t("queueIntegrationModal.buttons.okAdd")}`}
+                  {isSubmitting && (
+                    <CircularProgress
+                      size={24}
+                      className={classes.buttonProgress}
+                    />
+                  )}
+                </Button>
+              </DialogActions>
+            </Form>
+          )}
+        </Formik>
+      </Dialog>
+    </div >
+  );
+};
+
+export default QueueIntegration;

+ 510 - 0
frontend/src/components/QueueModal/index.js

@@ -0,0 +1,510 @@
+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 } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import CircularProgress from "@material-ui/core/CircularProgress";
+
+import { i18n } from "../../translate/i18n";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+import ColorPicker from "../ColorPicker";
+import {
+  FormControl,
+  Grid,
+  IconButton,
+  InputAdornment,
+  InputLabel,
+  MenuItem,
+  Paper,
+  Select,
+  Tab,
+  Tabs,
+} from "@material-ui/core";
+import { Colorize } from "@material-ui/icons";
+import { QueueOptions } from "../QueueOptions";
+import SchedulesForm from "../SchedulesForm";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    display: "flex",
+    flexWrap: "wrap",
+  },
+  textField: {
+    marginRight: theme.spacing(1),
+    flex: 1,
+  },
+
+  btnWrapper: {
+    position: "relative",
+  },
+
+  buttonProgress: {
+    color: green[500],
+    position: "absolute",
+    top: "50%",
+    left: "50%",
+    marginTop: -12,
+    marginLeft: -12,
+  },
+  formControl: {
+    margin: theme.spacing(1),
+    minWidth: 120,
+  },
+  colorAdorment: {
+    width: 20,
+    height: 20,
+  },
+}));
+
+const QueueSchema = Yup.object().shape({
+  name: Yup.string()
+    .min(2, i18n.t("queueModal.form.nameShort"))
+    .max(50, i18n.t("queueModal.form.nameLong"))
+    .required(i18n.t("queueModal.form.nameRequired")),
+  color: Yup.string()
+    .min(3, i18n.t("queueModal.form.colorShort"))
+    .max(9, i18n.t("queueModal.form.colorLong"))
+    .required(),
+  greetingMessage: Yup.string(),
+});
+
+const QueueModal = ({ open, onClose, queueId }) => {
+  const classes = useStyles();
+
+  const initialState = {
+    name: "",
+    color: "",
+    greetingMessage: "",
+    outOfHoursMessage: "",
+    orderQueue: "",
+    integrationId: "",
+    promptId: "",
+  };
+
+  const [colorPickerModalOpen, setColorPickerModalOpen] = useState(false);
+  const [queue, setQueue] = useState(initialState);
+  const [tab, setTab] = useState(0);
+  const [schedulesEnabled, setSchedulesEnabled] = useState(false);
+  const greetingRef = useRef();
+  const [integrations, setIntegrations] = useState([]);
+
+  const [schedules, setSchedules] = useState([
+    {
+      weekday: "Segunda-feira",
+      weekdayEn: "monday",
+      startTime: "08:00",
+      endTime: "18:00",
+    },
+    {
+      weekday: "Terça-feira",
+      weekdayEn: "tuesday",
+      startTime: "08:00",
+      endTime: "18:00",
+    },
+    {
+      weekday: "Quarta-feira",
+      weekdayEn: "wednesday",
+      startTime: "08:00",
+      endTime: "18:00",
+    },
+    {
+      weekday: "Quinta-feira",
+      weekdayEn: "thursday",
+      startTime: "08:00",
+      endTime: "18:00",
+    },
+    {
+      weekday: "Sexta-feira",
+      weekdayEn: "friday",
+      startTime: "08:00",
+      endTime: "18:00",
+    },
+    {
+      weekday: "Sábado",
+      weekdayEn: "saturday",
+      startTime: "08:00",
+      endTime: "12:00",
+    },
+    {
+      weekday: "Domingo",
+      weekdayEn: "sunday",
+      startTime: "00:00",
+      endTime: "00:00",
+    },
+  ]);
+  const [selectedPrompt, setSelectedPrompt] = useState(null);
+  const [prompts, setPrompts] = useState([]);
+
+  useEffect(() => {
+    (async () => {
+      try {
+        const { data } = await api.get("/prompt");
+        setPrompts(data.prompts);
+      } catch (err) {
+        toastError(err);
+      }
+    })();
+  }, []);
+
+  useEffect(() => {
+    api.get(`/settings`).then(({ data }) => {
+      if (Array.isArray(data)) {
+        const scheduleType = data.find((d) => d.key === "scheduleType");
+        if (scheduleType) {
+          setSchedulesEnabled(scheduleType.value === "queue");
+        }
+      }
+    });
+  }, []);
+
+  useEffect(() => {
+    (async () => {
+      try {
+        const { data } = await api.get("/queueIntegration");
+
+        setIntegrations(data.queueIntegrations);
+      } catch (err) {
+        toastError(err);
+      }
+    })();
+  }, []);
+
+  useEffect(() => {
+    (async () => {
+      if (!queueId) return;
+      try {
+        const { data } = await api.get(`/queue/${queueId}`);
+        setQueue((prevState) => {
+          return { ...prevState, ...data };
+        });
+        data.promptId
+          ? setSelectedPrompt(data.promptId)
+          : setSelectedPrompt(null);
+
+        setSchedules(data.schedules);
+      } catch (err) {
+        toastError(err);
+      }
+    })();
+
+    return () => {
+      setQueue({
+        name: "",
+        color: "",
+        greetingMessage: "",
+        outOfHoursMessage: "",
+        orderQueue: "",
+        integrationId: "",
+      });
+    };
+  }, [queueId, open]);
+
+  const handleClose = () => {
+    onClose();
+    setQueue(initialState);
+  };
+
+  const handleSaveQueue = async (values) => {
+    try {
+      if (queueId) {
+        await api.put(`/queue/${queueId}`, {
+          ...values,
+          schedules,
+          promptId: selectedPrompt ? selectedPrompt : null,
+        });
+      } else {
+        await api.post("/queue", {
+          ...values,
+          schedules,
+          promptId: selectedPrompt ? selectedPrompt : null,
+        });
+      }
+      toast.success(i18n.t("queueModal.toasts.success"));
+      handleClose();
+    } catch (err) {
+      toastError(err);
+    }
+  };
+
+  const handleSaveSchedules = async (values) => {
+    toast.success(i18n.t("queueModal.toasts.info"));
+    setSchedules(values);
+    setTab(0);
+  };
+
+  const handleChangePrompt = (e) => {
+    setSelectedPrompt(e.target.value);
+  };
+
+  return (
+    <div className={classes.root}>
+      <Dialog
+        maxWidth="md"
+        fullWidth={true}
+        open={open}
+        onClose={handleClose}
+        scroll="paper"
+      >
+        <DialogTitle>
+          {queueId
+            ? `${i18n.t("queueModal.title.edit")}`
+            : `${i18n.t("queueModal.title.add")}`}
+        </DialogTitle>
+        <Tabs
+          value={tab}
+          indicatorColor="primary"
+          textColor="primary"
+          onChange={(_, v) => setTab(v)}
+          aria-label="disabled tabs example"
+        >
+          <Tab label={i18n.t("queueModal.tabs.queueData")} />
+          {schedulesEnabled && <Tab label={i18n.t("queueModal.tabs.attendanceTime")} />}
+        </Tabs>
+        {tab === 0 && (
+          <Paper>
+            <Formik
+              initialValues={queue}
+              enableReinitialize={true}
+              validationSchema={QueueSchema}
+              onSubmit={(values, actions) => {
+                setTimeout(() => {
+                  handleSaveQueue(values);
+                  actions.setSubmitting(false);
+                }, 400);
+              }}
+            >
+              {({ touched, errors, isSubmitting, values }) => (
+                <Form>
+                  <DialogContent dividers>
+                    <Field
+                      as={TextField}
+                      label={i18n.t("queueModal.form.name")}
+                      autoFocus
+                      name="name"
+                      error={touched.name && Boolean(errors.name)}
+                      helperText={touched.name && errors.name}
+                      variant="outlined"
+                      margin="dense"
+                      className={classes.textField}
+                    />
+                    <Field
+                      as={TextField}
+                      label={i18n.t("queueModal.form.color")}
+                      name="color"
+                      id="color"
+                      onFocus={() => {
+                        setColorPickerModalOpen(true);
+                        greetingRef.current.focus();
+                      }}
+                      error={touched.color && Boolean(errors.color)}
+                      helperText={touched.color && errors.color}
+                      InputProps={{
+                        startAdornment: (
+                          <InputAdornment position="start">
+                            <div
+                              style={{ backgroundColor: values.color }}
+                              className={classes.colorAdorment}
+                            ></div>
+                          </InputAdornment>
+                        ),
+                        endAdornment: (
+                          <IconButton
+                            size="small"
+                            color="default"
+                            onClick={() => setColorPickerModalOpen(true)}
+                          >
+                            <Colorize />
+                          </IconButton>
+                        ),
+                      }}
+                      variant="outlined"
+                      margin="dense"
+                      className={classes.textField}
+                    />
+                    <ColorPicker
+                      open={colorPickerModalOpen}
+                      handleClose={() => setColorPickerModalOpen(false)}
+                      onChange={(color) => {
+                        values.color = color;
+                        setQueue(() => {
+                          return { ...values, color };
+                        });
+                      }}
+                    />
+                    <Field
+                      as={TextField}
+                      label={i18n.t("queueModal.form.orderQueue")}
+                      name="orderQueue"
+                      type="orderQueue"
+                      error={touched.orderQueue && Boolean(errors.orderQueue)}
+                      helperText={touched.orderQueue && errors.orderQueue}
+                      variant="outlined"
+                      margin="dense"
+                      className={classes.textField1}
+                    />
+                    <div>
+                      <FormControl
+                        variant="outlined"
+                        margin="dense"
+                        className={classes.FormControl}
+                        fullWidth
+                      >
+                        <InputLabel id="integrationId-selection-label">
+                          {i18n.t("queueModal.form.integrationId")}
+                        </InputLabel>
+                        <Field
+                          as={Select}
+                          label={i18n.t("queueModal.form.integrationId")}
+                          name="integrationId"
+                          id="integrationId"
+                          placeholder={i18n.t("queueModal.form.integrationId")}
+                          labelId="integrationId-selection-label"
+                          value={values.integrationId || ""}
+                        >
+                          <MenuItem value={""}>{"Nenhum"}</MenuItem>
+                          {integrations.map((integration) => (
+                            <MenuItem
+                              key={integration.id}
+                              value={integration.id}
+                            >
+                              {integration.name}
+                            </MenuItem>
+                          ))}
+                        </Field>
+                      </FormControl>
+                      <FormControl margin="dense" variant="outlined" fullWidth>
+                        <InputLabel>
+                          {i18n.t("whatsappModal.form.prompt")}
+                        </InputLabel>
+                        <Select
+                          labelId="dialog-select-prompt-label"
+                          id="dialog-select-prompt"
+                          name="promptId"
+                          value={selectedPrompt || ""}
+                          onChange={handleChangePrompt}
+                          label={i18n.t("whatsappModal.form.prompt")}
+                          fullWidth
+                          MenuProps={{
+                            anchorOrigin: {
+                              vertical: "bottom",
+                              horizontal: "left",
+                            },
+                            transformOrigin: {
+                              vertical: "top",
+                              horizontal: "left",
+                            },
+                            getContentAnchorEl: null,
+                          }}
+                        >
+                          {prompts.map((prompt) => (
+                            <MenuItem key={prompt.id} value={prompt.id}>
+                              {prompt.name}
+                            </MenuItem>
+                          ))}
+                        </Select>
+                      </FormControl>
+                    </div>
+                    <div style={{ marginTop: 5 }}>
+                      <Field
+                        as={TextField}
+                        label={i18n.t("queueModal.form.greetingMessage")}
+                        type="greetingMessage"
+                        multiline
+                        inputRef={greetingRef}
+                        rows={5}
+                        fullWidth
+                        name="greetingMessage"
+                        error={
+                          touched.greetingMessage &&
+                          Boolean(errors.greetingMessage)
+                        }
+                        helperText={
+                          touched.greetingMessage && errors.greetingMessage
+                        }
+                        variant="outlined"
+                        margin="dense"
+                      />
+                      {schedulesEnabled && (
+                        <Field
+                          as={TextField}
+                          label={i18n.t("queueModal.form.outOfHoursMessage")}
+                          type="outOfHoursMessage"
+                          multiline
+                          inputRef={greetingRef}
+                          rows={5}
+                          fullWidth
+                          name="outOfHoursMessage"
+                          error={
+                            touched.outOfHoursMessage &&
+                            Boolean(errors.outOfHoursMessage)
+                          }
+                          helperText={
+                            touched.outOfHoursMessage &&
+                            errors.outOfHoursMessage
+                          }
+                          variant="outlined"
+                          margin="dense"
+                        />
+                      )}
+                    </div>
+                    <QueueOptions queueId={queueId} />
+                  </DialogContent>
+                  <DialogActions>
+                    <Button
+                      onClick={handleClose}
+                      color="secondary"
+                      disabled={isSubmitting}
+                      variant="outlined"
+                    >
+                      {i18n.t("queueModal.buttons.cancel")}
+                    </Button>
+                    <Button
+                      type="submit"
+                      color="primary"
+                      disabled={isSubmitting}
+                      variant="contained"
+                      className={classes.btnWrapper}
+                    >
+                      {queueId
+                        ? `${i18n.t("queueModal.buttons.okEdit")}`
+                        : `${i18n.t("queueModal.buttons.okAdd")}`}
+                      {isSubmitting && (
+                        <CircularProgress
+                          size={24}
+                          className={classes.buttonProgress}
+                        />
+                      )}
+                    </Button>
+                  </DialogActions>
+                </Form>
+              )}
+            </Formik>
+          </Paper>
+        )}
+        {tab === 1 && (
+          <Paper style={{ padding: 20 }}>
+            <SchedulesForm
+              loading={false}
+              onSubmit={handleSaveSchedules}
+              initialValues={schedules}
+              labelSaveButton={i18n.t("queueModal.buttons.okAdd")}
+            />
+          </Paper>
+        )}
+      </Dialog>
+    </div>
+  );
+};
+
+export default QueueModal;

+ 345 - 0
frontend/src/components/QueueOptions/index.js

@@ -0,0 +1,345 @@
+import React, { useState, useEffect } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+import Stepper from "@material-ui/core/Stepper";
+import Step from "@material-ui/core/Step";
+import StepLabel from "@material-ui/core/StepLabel";
+import Typography from "@material-ui/core/Typography";
+import { Button, IconButton, StepContent, TextField } from "@material-ui/core";
+import AddIcon from "@material-ui/icons/Add";
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import SaveIcon from "@material-ui/icons/Save";
+import EditIcon from "@material-ui/icons/Edit";
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    width: "100%",
+    //height: 400,
+    [theme.breakpoints.down("sm")]: {
+      maxHeight: "20vh",
+    },
+  },
+  button: {
+    marginRight: theme.spacing(1),
+  },
+  input: {
+    marginTop: theme.spacing(1),
+    marginBottom: theme.spacing(1),
+  },
+  addButton: {
+    marginTop: theme.spacing(2),
+    marginBottom: theme.spacing(2),
+  },
+}));
+
+export function QueueOptionStepper({ queueId, options, updateOptions }) {
+  const classes = useStyles();
+  const [activeOption, setActiveOption] = useState(-1);
+
+  const handleOption = (index) => async () => {
+    setActiveOption(index);
+    const option = options[index];
+
+    if (option !== undefined && option.id !== undefined) {
+      try {
+        const { data } = await api.request({
+          url: "/queue-options",
+          method: "GET",
+          params: { queueId, parentId: option.id },
+        });
+        const optionList = data.map((option) => {
+          return {
+            ...option,
+            children: [],
+            edition: false,
+          };
+        });
+        option.children = optionList;
+        updateOptions();
+      } catch (e) {
+        toastError(e);
+      }
+    }
+  };
+
+  const handleSave = async (option) => {
+    try {
+      if (option.id) {
+        await api.request({
+          url: `/queue-options/${option.id}`,
+          method: "PUT",
+          data: option,
+        });
+      } else {
+        const { data } = await api.request({
+          url: `/queue-options`,
+          method: "POST",
+          data: option,
+        });
+        option.id = data.id;
+      }
+      option.edition = false;
+      updateOptions();
+    } catch (e) {
+      toastError(e);
+    }
+  };
+
+  const handleEdition = (index) => {
+    options[index].edition = !options[index].edition;
+    updateOptions();
+  };
+
+  const handleDeleteOption = async (index) => {
+    const option = options[index];
+    if (option !== undefined && option.id !== undefined) {
+      try {
+        await api.request({
+          url: `/queue-options/${option.id}`,
+          method: "DELETE",
+        });
+      } catch (e) {
+        toastError(e);
+      }
+    }
+    options.splice(index, 1);
+    options.forEach(async (option, order) => {
+      option.option = order + 1;
+      await handleSave(option);
+    });
+    updateOptions();
+  };
+
+  const handleOptionChangeTitle = (event, index) => {
+    options[index].title = event.target.value;
+    updateOptions();
+  };
+
+  const handleOptionChangeMessage = (event, index) => {
+    options[index].message = event.target.value;
+    updateOptions();
+  };
+
+  const renderTitle = (index) => {
+    const option = options[index];
+    if (option.edition) {
+      return (
+        <>
+          <TextField
+            value={option.title}
+            onChange={(event) => handleOptionChangeTitle(event, index)}
+            size="small"
+            className={classes.input}
+            placeholder="Título da opção"
+          />
+          {option.edition && (
+            <>
+              <IconButton
+                color="primary"
+                variant="outlined"
+                size="small"
+                className={classes.button}
+                onClick={() => handleSave(option)}
+              >
+                <SaveIcon />
+              </IconButton>
+              <IconButton
+                variant="outlined"
+                color="secondary"
+                size="small"
+                className={classes.button}
+                onClick={() => handleDeleteOption(index)}
+              >
+                <DeleteOutlineIcon />
+              </IconButton>
+            </>
+          )}
+        </>
+      );
+    }
+    return (
+      <>
+        <Typography>
+          {option.title !== "" ? option.title : "Título não definido"}
+          <IconButton
+            variant="outlined"
+            size="small"
+            className={classes.button}
+            onClick={() => handleEdition(index)}
+          >
+            <EditIcon />
+          </IconButton>
+        </Typography>
+      </>
+    );
+  };
+
+  const renderMessage = (index) => {
+    const option = options[index];
+    if (option.edition) {
+      return (
+        <>
+          <TextField
+            style={{ width: "100%" }}
+            multiline
+            value={option.message}
+            onChange={(event) => handleOptionChangeMessage(event, index)}
+            size="small"
+            className={classes.input}
+            placeholder="Digite o texto da opção"
+          />
+        </>
+      );
+    }
+    return (
+      <>
+        <Typography onClick={() => handleEdition(index)}>
+          {option.message}
+        </Typography>
+      </>
+    );
+  };
+
+  const handleAddOption = (index) => {
+    const optionNumber = options[index].children.length + 1;
+    options[index].children.push({
+      title: "",
+      message: "",
+      edition: false,
+      option: optionNumber,
+      queueId,
+      parentId: options[index].id,
+      children: [],
+    });
+    updateOptions();
+  };
+
+  const renderStep = (option, index) => {
+    return (
+      <Step key={index}>
+        <StepLabel style={{ cursor: "pointer" }} onClick={handleOption(index)}>
+          {renderTitle(index)}
+        </StepLabel>
+        <StepContent>
+          {renderMessage(index)}
+
+          {option.id !== undefined && (
+            <>
+              <Button
+                color="primary"
+                size="small"
+                onClick={() => handleAddOption(index)}
+                startIcon={<AddIcon />}
+                variant="outlined"
+                className={classes.addButton}
+              >
+                Adicionar
+              </Button>
+            </>
+          )}
+          <QueueOptionStepper
+            queueId={queueId}
+            options={option.children}
+            updateOptions={updateOptions}
+          />
+        </StepContent>
+      </Step>
+    );
+  };
+
+  const renderStepper = () => {
+    return (
+      <Stepper
+        style={{ marginBottom: 0, paddingBottom: 0 }}
+        nonLinear
+        activeStep={activeOption}
+        orientation="vertical"
+      >
+        {options.map((option, index) => renderStep(option, index))}
+      </Stepper>
+    );
+  };
+
+  return renderStepper();
+}
+
+export function QueueOptions({ queueId }) {
+  const classes = useStyles();
+  const [options, setOptions] = useState([]);
+
+  useEffect(() => {
+    if (queueId) {
+      const fetchOptions = async () => {
+        try {
+          const { data } = await api.request({
+            url: "/queue-options",
+            method: "GET",
+            params: { queueId, parentId: -1 },
+          });
+          const optionList = data.map((option) => {
+            return {
+              ...option,
+              children: [],
+              edition: false,
+            };
+          });
+          setOptions(optionList);
+        } catch (e) {
+          toastError(e);
+        }
+      };
+      fetchOptions();
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  const renderStepper = () => {
+    if (options.length > 0) {
+      return (
+        <QueueOptionStepper
+          queueId={queueId}
+          updateOptions={updateOptions}
+          options={options}
+        />
+      );
+    }
+  };
+
+  const updateOptions = () => {
+    setOptions([...options]);
+  };
+
+  const addOption = () => {
+    const newOption = {
+      title: "",
+      message: "",
+      edition: false,
+      option: options.length + 1,
+      queueId,
+      parentId: null,
+      children: [],
+    };
+    setOptions([...options, newOption]);
+  };
+
+  return (
+    <div className={classes.root}>
+      <br />
+      <Typography>
+        Opções
+        <Button
+          color="primary"
+          size="small"
+          onClick={addOption}
+          startIcon={<AddIcon />}
+          style={{ marginLeft: 10 }}
+          variant="outlined"
+        >
+          Adicionar
+        </Button>
+      </Typography>
+      {renderStepper()}
+    </div>
+  );
+}

+ 90 - 0
frontend/src/components/QueueSelect/index copy.js

@@ -0,0 +1,90 @@
+import React, { useEffect, useState } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+import InputLabel from "@material-ui/core/InputLabel";
+import MenuItem from "@material-ui/core/MenuItem";
+import FormControl from "@material-ui/core/FormControl";
+import Select from "@material-ui/core/Select";
+import Chip from "@material-ui/core/Chip";
+import toastError from "../../errors/toastError";
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles(theme => ({
+	chips: {
+		display: "flex",
+		flexWrap: "wrap",
+	},
+	chip: {
+		margin: 2,
+	},
+}));
+
+const QueueSelect = ({ selectedQueueIds, onChange }) => {
+	const classes = useStyles();
+	const [queues, setQueues] = useState([]);
+
+	useEffect(() => {
+		(async () => {
+			try {
+				const { data } = await api.get("/queue");
+				setQueues(data);
+			} catch (err) {
+				toastError(err);
+			}
+		})();
+	}, []);
+
+	const handleChange = e => {
+		onChange(e.target.value);
+	};
+
+	return (
+		<div style={{ marginTop: 6 }}>
+			<FormControl fullWidth margin="dense" variant="outlined">
+				<InputLabel>{i18n.t("queueSelect.inputLabel")}</InputLabel>
+				<Select
+					multiple
+					labelWidth={60}
+					value={selectedQueueIds}
+					onChange={handleChange}
+					MenuProps={{
+						anchorOrigin: {
+							vertical: "bottom",
+							horizontal: "left",
+						},
+						transformOrigin: {
+							vertical: "top",
+							horizontal: "left",
+						},
+						getContentAnchorEl: null,
+					}}
+					renderValue={selected => (
+						<div className={classes.chips}>
+							{selected?.length > 0 &&
+								selected.map(id => {
+									const queue = queues.find(q => q.id === id);
+									return queue ? (
+										<Chip
+											key={id}
+											style={{ backgroundColor: queue.color }}
+											variant="outlined"
+											label={queue.name}
+											className={classes.chip}
+										/>
+									) : null;
+								})}
+						</div>
+					)}
+				>
+					{queues.map(queue => (
+						<MenuItem key={queue.id} value={queue.id}>
+							{queue.name}
+						</MenuItem>
+					))}
+				</Select>
+			</FormControl>
+		</div>
+	);
+};
+
+export default QueueSelect;

+ 112 - 0
frontend/src/components/QueueSelect/index.js

@@ -0,0 +1,112 @@
+import React, { useEffect, useState } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+import InputLabel from "@material-ui/core/InputLabel";
+import MenuItem from "@material-ui/core/MenuItem";
+import FormControl from "@material-ui/core/FormControl";
+import Select from "@material-ui/core/Select";
+import Chip from "@material-ui/core/Chip";
+import toastError from "../../errors/toastError";
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles(theme => ({
+	chips: {
+		display: "flex",
+		flexWrap: "wrap",
+	},
+	chip: {
+		margin: 2,
+	},
+}));
+
+const QueueSelect = ({ selectedQueueIds, onChange, multiple = true, title = i18n.t("queueSelect.inputLabel") }) => {
+	const classes = useStyles();
+	const [queues, setQueues] = useState([]);
+
+	useEffect(() => {
+
+		fetchQueues();
+
+	}, []);
+
+	const fetchQueues = async () => {
+		try {
+			const { data } = await api.get("/queue");
+			setQueues(data);
+		} catch (err) {
+			toastError(err);
+		}
+	}
+
+	const handleChange = e => {
+		onChange(e.target.value);
+	};
+
+	return (
+		<div >
+			<FormControl fullWidth margin="dense" variant="outlined">
+				<InputLabel shrink={selectedQueueIds ? true : false} >{title}</InputLabel>
+				<Select
+					label={title}
+					multiple={multiple}
+					labelWidth={60}
+					value={selectedQueueIds}
+					onChange={handleChange}
+					MenuProps={{
+						anchorOrigin: {
+							vertical: "bottom",
+							horizontal: "left",
+						},
+						transformOrigin: {
+							vertical: "top",
+							horizontal: "left",
+						},
+						getContentAnchorEl: null,
+					}}
+
+					renderValue={selected => {
+						return (
+							<div className={classes.chips}>
+								{selected?.length > 0 && multiple ? (
+									selected.map(id => {
+										const queue = queues.find(q => q.id === id);
+										return queue ? (
+											<Chip
+												key={id}
+												style={{ backgroundColor: queue.color }}
+												variant="outlined"
+												label={queue.name}
+												className={classes.chip}
+											/>
+										) : null;
+									})
+
+								) :
+									(
+										<Chip
+											key={selected}
+											variant="outlined"
+											style={{ backgroundColor: queues.find(q => q.id === selected)?.color }}
+											label={queues.find(q => q.id === selected)?.name}
+											className={classes.chip}
+										/>
+									)
+								}
+
+							</div>
+						)
+					}}
+				>
+					{!multiple && <MenuItem value={null}>Nenhum</MenuItem>}
+					{queues.map(queue => (
+						<MenuItem key={queue.id} value={queue.id}>
+							{queue.name}
+						</MenuItem>
+					))}
+				</Select>
+			</FormControl>
+		</div>
+	);
+};
+
+export default QueueSelect;

+ 90 - 0
frontend/src/components/QueueSelect/index_erro.js

@@ -0,0 +1,90 @@
+import React, { useEffect, useState } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+import InputLabel from "@material-ui/core/InputLabel";
+import MenuItem from "@material-ui/core/MenuItem";
+import FormControl from "@material-ui/core/FormControl";
+import Select from "@material-ui/core/Select";
+import Chip from "@material-ui/core/Chip";
+import toastError from "../../errors/toastError";
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles(theme => ({
+	chips: {
+		display: "flex",
+		flexWrap: "wrap",
+	},
+	chip: {
+		margin: 2,
+	},
+}));
+
+const QueueSelect = ({ selectedQueueIds, onChange }) => {
+	const classes = useStyles();
+	const [queues, setQueues] = useState([]);
+
+	useEffect(() => {
+		(async () => {
+			try {
+				const { data } = await api.get("/queue");
+				setQueues(data);
+			} catch (err) {
+				toastError(err);
+			}
+		})();
+	}, []);
+
+	const handleChange = e => {
+		onChange(e.target.value);
+	};
+
+	return (
+		<div style={{ marginTop: 6 }}>
+			<FormControl fullWidth margin="dense" variant="outlined">
+				<InputLabel>{i18n.t("queueSelect.inputLabel")}</InputLabel>
+				<Select
+					multiple
+					labelWidth={60}
+					value={selectedQueueIds}
+					onChange={handleChange}
+					MenuProps={{
+						anchorOrigin: {
+							vertical: "bottom",
+							horizontal: "left",
+						},
+						transformOrigin: {
+							vertical: "top",
+							horizontal: "left",
+						},
+						getContentAnchorEl: null,
+					}}
+					renderValue={selected => (
+						<div className={classes.chips}>
+							{selected?.length > 0 &&
+								selected.map(id => {
+									const queue = queues.find(q => q.id === id);
+									return queue ? (
+										<Chip
+											key={id}
+											style={{ backgroundColor: queue.color }}
+											variant="outlined"
+											label={queue.name}
+											className={classes.chip}
+										/>
+									) : null;
+								})}
+						</div>
+					)}
+				>
+					{queues.map(queue => (
+						<MenuItem key={queue.id} value={queue.id}>
+							{queue.name}
+						</MenuItem>
+					))}
+				</Select>
+			</FormControl>
+		</div>
+	);
+};
+
+export default QueueSelect;

+ 92 - 0
frontend/src/components/QueueSelectCustom/index.js

@@ -0,0 +1,92 @@
+import React, { useEffect, useState } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+import InputLabel from "@material-ui/core/InputLabel";
+import MenuItem from "@material-ui/core/MenuItem";
+import FormControl from "@material-ui/core/FormControl";
+import Select from "@material-ui/core/Select";
+import Chip from "@material-ui/core/Chip";
+import toastError from "../../errors/toastError";
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles(theme => ({
+	chips: {
+		display: "flex",
+		flexWrap: "wrap",
+	},
+	chip: {
+		margin: 2,
+	},
+}));
+
+const QueueSelectCustom = ({ selectedQueueIds, companyId, onChange }) => {
+	const classes = useStyles();
+	const [queues, setQueues] = useState([]);
+
+	useEffect(() => {
+		(async () => {
+			try {
+				const { data } = await api.get("/queue", {
+					params: { companyId }
+				});
+				setQueues(data);
+			} catch (err) {
+				toastError(err);
+			}
+		})();
+	}, [companyId]);
+
+	const handleChange = e => {
+		onChange(e.target.value);
+	};
+
+	return (
+		<div style={{ marginTop: 6 }}>
+			<FormControl fullWidth margin="dense" variant="outlined">
+				<InputLabel>{i18n.t("queueSelect.inputLabel")}</InputLabel>
+				<Select
+					multiple
+					labelWidth={60}
+					value={selectedQueueIds}
+					onChange={handleChange}
+					MenuProps={{
+						anchorOrigin: {
+							vertical: "bottom",
+							horizontal: "left",
+						},
+						transformOrigin: {
+							vertical: "top",
+							horizontal: "left",
+						},
+						getContentAnchorEl: null,
+					}}
+					renderValue={selected => (
+						<div className={classes.chips}>
+							{selected?.length > 0 &&
+								selected.map(id => {
+									const queue = queues.find(q => q.id === id);
+									return queue ? (
+										<Chip
+											key={id}
+											style={{ backgroundColor: queue.color }}
+											variant="outlined"
+											label={queue.name}
+											className={classes.chip}
+										/>
+									) : null;
+								})}
+						</div>
+					)}
+				>
+					{queues.map(queue => (
+						<MenuItem key={queue.id} value={queue.id}>
+							{queue.name}
+						</MenuItem>
+					))}
+				</Select>
+			</FormControl>
+		</div>
+	);
+};
+
+export default QueueSelectCustom;

+ 68 - 0
frontend/src/components/QueueSelectSingle/index.js

@@ -0,0 +1,68 @@
+import React, { useEffect, useState } from "react";
+import { Field } from "formik";
+import { makeStyles } from "@material-ui/core/styles";
+import MenuItem from "@material-ui/core/MenuItem";
+import FormControl from "@material-ui/core/FormControl";
+import Select from "@material-ui/core/Select";
+import toastError from "../../errors/toastError";
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+import Typography from "@material-ui/core/Typography";
+
+const useStyles = makeStyles(theme => ({
+    formControl: {
+        margin: theme.spacing(1),
+        minWidth: 120,
+    },
+}));
+
+const QueueSelectSingle = (touched, errors) => {
+    const classes = useStyles();
+    const [queues, setQueues] = useState([]);
+
+    useEffect(() => {
+        (async () => {
+            try {
+                const { data } = await api.get("/queue");
+                setQueues(data);
+            } catch (err) {
+                toastError(`QUEUESELETSINGLE >>> ${err}`);
+            }
+        })();
+    }, []);
+
+    return (
+        <div style={{ marginTop: 6 }}>
+            <FormControl
+                variant="outlined"
+                className={classes.FormControl}
+                margin="dense"
+                fullWidth
+            >
+                <div>
+                    <Typography>
+                        {i18n.t("queueSelect.inputLabel")}
+                    </Typography>
+                    <Field
+                        as={Select}
+                        label={i18n.t("queueSelect.inputLabel")}
+                        name="queueId"
+                        labelId="queue-selection-label"
+                        id="queue-selection"
+                        error={touched.queueId && Boolean(errors.queueId)}
+                        helpertext={touched.name && errors.name}
+                        fullWidth
+                    >
+                        {queues.map(queue => (
+                            <MenuItem key={queue.id} value={queue.id}>
+                                {queue.name}
+                            </MenuItem>
+                        ))}
+                    </Field>
+                </div>
+            </FormControl>
+        </div>
+    );
+};
+
+export default QueueSelectSingle;

+ 332 - 0
frontend/src/components/QuickMessageDialog/index.js

@@ -0,0 +1,332 @@
+import React, { useContext, useState, useEffect, useRef } from "react";
+
+import * as Yup from "yup";
+import { Formik, Form, Field } from "formik";
+import { toast } from "react-toastify";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import CircularProgress from "@material-ui/core/CircularProgress";
+import AttachFileIcon from "@material-ui/icons/AttachFile";
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import IconButton from "@material-ui/core/IconButton";
+import { i18n } from "../../translate/i18n";
+import { head } from "lodash";
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import MessageVariablesPicker from "../MessageVariablesPicker";
+import ButtonWithSpinner from "../ButtonWithSpinner";
+
+import {
+    FormControl,
+    Grid,
+    InputLabel,
+    MenuItem,
+    Select,
+} from "@material-ui/core";
+import ConfirmationModal from "../ConfirmationModal";
+
+const path = require('path');
+
+const useStyles = makeStyles((theme) => ({
+    root: {
+        display: "flex",
+        flexWrap: "wrap",
+    },
+    multFieldLine: {
+        display: "flex",
+        "& > *:not(:last-child)": {
+            marginRight: theme.spacing(1),
+        },
+    },
+
+    btnWrapper: {
+        position: "relative",
+    },
+
+    buttonProgress: {
+        color: green[500],
+        position: "absolute",
+        top: "50%",
+        left: "50%",
+        marginTop: -12,
+        marginLeft: -12,
+    },
+    formControl: {
+        margin: theme.spacing(1),
+        minWidth: 120,
+    },
+    colorAdorment: {
+        width: 20,
+        height: 20,
+    },
+}));
+
+const QuickeMessageSchema = Yup.object().shape({
+    shortcode: Yup.string().required("Obrigatório"),
+    //   message: Yup.string().required("Obrigatório"),
+});
+
+const QuickMessageDialog = ({ open, onClose, quickemessageId, reload }) => {
+    const classes = useStyles();
+    const { user } = useContext(AuthContext);
+    const { profile } = user;
+    const messageInputRef = useRef();
+
+    const initialState = {
+        shortcode: "",
+        message: "",
+        geral: false,
+        status: true,
+    };
+
+    const [confirmationOpen, setConfirmationOpen] = useState(false);
+    const [quickemessage, setQuickemessage] = useState(initialState);
+    const [attachment, setAttachment] = useState(null);
+    const attachmentFile = useRef(null);
+
+    useEffect(() => {
+        try {
+            (async () => {
+                if (!quickemessageId) return;
+
+                const { data } = await api.get(`/quick-messages/${quickemessageId}`);
+
+                setQuickemessage((prevState) => {
+                    return { ...prevState, ...data };
+                });
+            })();
+        } catch (err) {
+            toastError(err);
+        }
+    }, [quickemessageId, open]);
+
+    const handleClose = () => {
+        setQuickemessage(initialState);
+        setAttachment(null);
+        onClose();
+    };
+
+    const handleAttachmentFile = (e) => {
+      
+        const file = head(e.target.files);
+        if (file) {
+            setAttachment(file);
+        }
+    };
+
+    const handleSaveQuickeMessage = async (values) => {
+
+        const quickemessageData = { ...values, isMedia: true, mediaPath: attachment ? String(attachment.name).replace(/ /g, "_") : values.mediaPath ? path.basename(values.mediaPath).replace(/ /g, "_") : null };
+
+        try {
+            if (quickemessageId) {
+                await api.put(`/quick-messages/${quickemessageId}`, quickemessageData);
+                if (attachment != null) {
+                    const formData = new FormData();
+                    formData.append("typeArch", "quickMessage");
+                    formData.append("file", attachment);
+                    await api.post(
+                        `/quick-messages/${quickemessageId}/media-upload`,
+                        formData
+                    );
+                }
+            } else {
+                const { data } = await api.post("/quick-messages", quickemessageData);
+                if (attachment != null) {
+                    const formData = new FormData();
+                    formData.append("typeArch", "quickMessage");
+                    formData.append("file", attachment);
+                    await api.post(`/quick-messages/${data.id}/media-upload`, formData);
+                }
+            }
+            toast.success(i18n.t("quickMessages.toasts.success"));
+            if (typeof reload == "function") {
+
+                reload();
+            }
+        } catch (err) {
+            toastError(err);
+        }
+        handleClose();
+    };
+
+    const deleteMedia = async () => {
+        if (attachment) {
+            setAttachment(null);
+            attachmentFile.current.value = null;
+        }
+
+        if (quickemessage.mediaPath) {
+            await api.delete(`/quick-messages/${quickemessage.id}/media-upload`);
+            setQuickemessage((prev) => ({
+                ...prev,
+                mediaPath: null,
+            }));
+            toast.success(i18n.t("quickMessages.toasts.deleted"));
+            if (typeof reload == "function") {
+
+                reload();
+            }
+        }
+    };
+
+    const handleClickMsgVar = async (msgVar, setValueFunc) => {
+        const el = messageInputRef.current;
+        const firstHalfText = el.value.substring(0, el.selectionStart);
+        const secondHalfText = el.value.substring(el.selectionEnd);
+        const newCursorPos = el.selectionStart + msgVar.length;
+
+        setValueFunc("message", `${firstHalfText}${msgVar}${secondHalfText}`);
+
+        await new Promise(r => setTimeout(r, 100));
+        messageInputRef.current.setSelectionRange(newCursorPos, newCursorPos);
+    };
+
+    return (
+        <div className={classes.root}>
+            <ConfirmationModal
+                title={i18n.t("quickMessages.confirmationModal.deleteTitle")}
+                open={confirmationOpen}
+                onClose={() => setConfirmationOpen(false)}
+                onConfirm={deleteMedia}
+            >
+                {i18n.t("quickMessages.confirmationModal.deleteMessage")}
+            </ConfirmationModal>
+            <Dialog
+                open={open}
+                onClose={handleClose}
+                maxWidth="xs"
+                fullWidth
+                scroll="paper"
+            >
+                <DialogTitle id="form-dialog-title">
+                    {quickemessageId
+                        ? `${i18n.t("quickMessages.dialog.edit")}`
+                        : `${i18n.t("quickMessages.dialog.add")}`}
+                </DialogTitle>
+                <div style={{ display: "none" }}>
+                    <input
+                        type="file"
+                        ref={attachmentFile}
+                        onChange={(e) => handleAttachmentFile(e)}
+                    />
+                </div>
+                <Formik
+                    initialValues={quickemessage}
+                    enableReinitialize={true}
+                    validationSchema={QuickeMessageSchema}
+                    onSubmit={(values, actions) => {
+                        setTimeout(() => {
+                            handleSaveQuickeMessage(values);
+                            actions.setSubmitting(false);
+                        }, 400);
+                    }}
+                >
+                    {({ touched, errors, isSubmitting, setFieldValue, values }) => (
+                        <Form>
+                            <DialogContent dividers>
+                                <Grid spacing={2} container>
+                                    <Grid xs={12} item>
+                                        <Field
+                                            as={TextField}
+                                            autoFocus
+                                            label={i18n.t("quickMessages.dialog.shortcode")}
+                                            name="shortcode"
+                                            error={touched.shortcode && Boolean(errors.shortcode)}
+                                            helperText={touched.shortcode && errors.shortcode}
+                                            variant="outlined"
+                                            margin="dense"
+                                            fullWidth
+                                        />
+                                    </Grid>
+                                    <Grid xs={12} item>
+                                        <Field
+                                            as={TextField}
+                                            label={i18n.t("quickMessages.dialog.message")}
+                                            name="message"
+                                            inputRef={messageInputRef}
+                                            error={touched.message && Boolean(errors.message)}
+                                            helperText={touched.message && errors.message}
+                                            variant="outlined"
+                                            margin="dense"
+                                            multiline={true}
+                                            rows={7}
+                                            fullWidth
+                                        // disabled={quickemessage.mediaPath || attachment ? true : false}
+                                        />
+                                    </Grid>
+                                    <Grid item>
+                                        <MessageVariablesPicker
+                                            disabled={isSubmitting}
+                                            onClick={value => handleClickMsgVar(value, setFieldValue)}
+                                        />
+                                    </Grid>
+                                    {(quickemessage.mediaPath || attachment) && (
+                                        <Grid xs={12} item>
+                                            <Button startIcon={<AttachFileIcon />}>
+                                                {attachment ? attachment.name : quickemessage.mediaName}
+                                            </Button>
+                                            <IconButton
+                                                onClick={() => setConfirmationOpen(true)}
+                                                color="secondary"
+                                            >
+                                                <DeleteOutlineIcon color="secondary" />
+                                            </IconButton>
+                                        </Grid>
+                                    )}
+                                </Grid>
+                            </DialogContent>
+                            <DialogActions>
+                                {!attachment && !quickemessage.mediaPath && (
+                                    <Button
+                                        color="primary"
+                                        onClick={() => attachmentFile.current.click()}
+                                        disabled={isSubmitting}
+                                        variant="outlined"
+                                    >
+                                        {i18n.t("quickMessages.buttons.attach")}
+                                    </Button>
+                                )}
+                                <Button
+                                    onClick={handleClose}
+                                    color="secondary"
+                                    disabled={isSubmitting}
+                                    variant="outlined"
+                                >
+                                    {i18n.t("quickMessages.buttons.cancel")}
+                                </Button>
+                                <Button
+                                    type="submit"
+                                    color="primary"
+                                    disabled={isSubmitting}
+                                    variant="contained"
+                                    className={classes.btnWrapper}
+                                >
+                                    {quickemessageId
+                                        ? `${i18n.t("quickMessages.buttons.edit")}`
+                                        : `${i18n.t("quickMessages.buttons.add")}`}
+                                    {isSubmitting && (
+                                        <CircularProgress
+                                            size={24}
+                                            className={classes.buttonProgress}
+                                        />
+                                    )}
+                                </Button>
+                            </DialogActions>
+                        </Form>
+                    )}
+                </Formik>
+            </Dialog>
+        </div>
+    );
+};
+
+export default QuickMessageDialog;

+ 93 - 0
frontend/src/components/QuickMessagesTable/index.js

@@ -0,0 +1,93 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import { 
+    Table,
+    TableHead,
+    TableBody,
+    TableCell,
+    TableRow,
+    IconButton
+} from '@material-ui/core';
+import {
+    Edit as EditIcon,
+    DeleteOutline as DeleteOutlineIcon
+} from "@material-ui/icons";
+
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+
+function QuickMessagesTable(props) {
+    const { messages, showLoading, editMessage, deleteMessage, readOnly } = props
+    const [loading, setLoading] = useState(true)
+    const [rows, setRows] = useState([])
+
+    useEffect(() => {
+        if (Array.isArray(messages)) {
+            setRows(messages)
+        }
+        if (showLoading !== undefined) {
+            setLoading(showLoading)    
+        }
+    }, [messages, showLoading])
+
+    const handleEdit = (message) => {
+        editMessage(message)
+    }
+
+    const handleDelete = (message) => {
+        deleteMessage(message)
+    }
+
+    const renderRows = () => {
+        return rows.map((message) => {
+            return (
+                <TableRow key={message.id}>
+                    <TableCell align="center">{message.shortcode}</TableCell>
+                    <TableCell align="left">{message.message}</TableCell>
+                    { !readOnly ? (
+                        <TableCell align="center">
+                            <IconButton
+                                size="small"
+                                onClick={() => handleEdit(message)}
+                            >
+                                <EditIcon />
+                            </IconButton>
+
+                            <IconButton
+                                size="small"
+                                onClick={() => handleDelete(message)}
+                            >
+                                <DeleteOutlineIcon />
+                            </IconButton>
+                        </TableCell>
+                    ) : null}
+                </TableRow>
+            )
+        })
+    }
+
+    return (
+        <Table size="small">
+            <TableHead>
+                <TableRow>
+                    <TableCell align="center">Atalho</TableCell>
+                    <TableCell align="left">Mensagem</TableCell>
+                    { !readOnly ? (
+                        <TableCell align="center">Ações</TableCell>
+                    ) : null}
+                </TableRow>
+            </TableHead>
+            <TableBody>
+                {loading ? <TableRowSkeleton columns={readOnly ? 2 : 3} /> : renderRows()}
+            </TableBody>
+        </Table>
+    )
+}
+
+QuickMessagesTable.propTypes = {
+    messages: PropTypes.array.isRequired,
+    showLoading: PropTypes.bool,
+    editMessage: PropTypes.func.isRequired,
+    deleteMessage: PropTypes.func.isRequired
+}
+
+export default QuickMessagesTable;

+ 382 - 0
frontend/src/components/ScheduleModal/index.js

@@ -0,0 +1,382 @@
+import React, { useState, useEffect, useContext, useRef } from "react";
+
+import * as Yup from "yup";
+import { Formik, Form, Field } from "formik";
+import { toast } from "react-toastify";
+import { useHistory } from "react-router-dom";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import CircularProgress from "@material-ui/core/CircularProgress";
+
+import { i18n } from "../../translate/i18n";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+import { FormControl, Grid, IconButton } from "@material-ui/core";
+import Autocomplete from "@material-ui/lab/Autocomplete";
+import moment from "moment"
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { isArray, capitalize } from "lodash";
+import DeleteOutline from "@material-ui/icons/DeleteOutline";
+import AttachFile from "@material-ui/icons/AttachFile";
+import { head } from "lodash";
+import ConfirmationModal from "../ConfirmationModal";
+import MessageVariablesPicker from "../MessageVariablesPicker";
+
+const useStyles = makeStyles(theme => ({
+	root: {
+		display: "flex",
+		flexWrap: "wrap",
+	},
+	multFieldLine: {
+		display: "flex",
+		"& > *:not(:last-child)": {
+			marginRight: theme.spacing(1),
+		},
+	},
+
+	btnWrapper: {
+		position: "relative",
+	},
+
+	buttonProgress: {
+		color: green[500],
+		position: "absolute",
+		top: "50%",
+		left: "50%",
+		marginTop: -12,
+		marginLeft: -12,
+	},
+	formControl: {
+		margin: theme.spacing(1),
+		minWidth: 120,
+	},
+}));
+
+const ScheduleSchema = Yup.object().shape({
+	body: Yup.string()
+		.min(5, "Mensagem muito curta")
+		.required("Obrigatório"),
+	contactId: Yup.number().required("Obrigatório"),
+	sendAt: Yup.string().required("Obrigatório")
+});
+
+const ScheduleModal = ({ open, onClose, scheduleId, contactId, cleanContact, reload }) => {
+	const classes = useStyles();
+	const history = useHistory();
+	const { user } = useContext(AuthContext);
+
+	const initialState = {
+		body: "",
+		contactId: "",
+		sendAt: moment().add(1, 'hour').format('YYYY-MM-DDTHH:mm'),
+		sentAt: ""
+	};
+
+	const initialContact = {
+		id: "",
+		name: ""
+	}
+
+	const [schedule, setSchedule] = useState(initialState);
+	const [currentContact, setCurrentContact] = useState(initialContact);
+	const [contacts, setContacts] = useState([initialContact]);
+	const [attachment, setAttachment] = useState(null);
+	const attachmentFile = useRef(null);
+	const [confirmationOpen, setConfirmationOpen] = useState(false);
+	const messageInputRef = useRef();
+
+	useEffect(() => {
+		if (contactId && contacts.length) {
+			const contact = contacts.find(c => c.id === contactId);
+			if (contact) {
+				setCurrentContact(contact);
+			}
+		}
+	}, [contactId, contacts]);
+
+	useEffect(() => {
+		const { companyId } = user;
+		if (open) {
+			try {
+				(async () => {
+					const { data: contactList } = await api.get('/contacts/list', { params: { companyId: companyId } });
+					let customList = contactList.map((c) => ({ id: c.id, name: c.name }));
+					if (isArray(customList)) {
+						setContacts([{ id: "", name: "" }, ...customList]);
+					}
+					if (contactId) {
+						setSchedule(prevState => {
+							return { ...prevState, contactId }
+						});
+					}
+
+					if (!scheduleId) return;
+
+					const { data } = await api.get(`/schedules/${scheduleId}`);
+					setSchedule(prevState => {
+						return { ...prevState, ...data, sendAt: moment(data.sendAt).format('YYYY-MM-DDTHH:mm') };
+					});
+					setCurrentContact(data.contact);
+				})()
+			} catch (err) {
+				toastError(err);
+			}
+		}
+	}, [scheduleId, contactId, open, user]);
+
+	const handleClose = () => {
+		onClose();
+		setAttachment(null);
+		setSchedule(initialState);
+	};
+
+	const handleAttachmentFile = (e) => {
+		const file = head(e.target.files);
+		if (file) {
+			setAttachment(file);
+		}
+	};
+
+	const handleSaveSchedule = async values => {
+		const scheduleData = { ...values, userId: user.id };
+		try {
+			if (scheduleId) {
+				await api.put(`/schedules/${scheduleId}`, scheduleData);
+				if (attachment != null) {
+					const formData = new FormData();
+					formData.append("file", attachment);
+					await api.post(
+						`/schedules/${scheduleId}/media-upload`,
+						formData
+					);
+				}
+			} else {
+				const { data } = await api.post("/schedules", scheduleData);
+				if (attachment != null) {
+					const formData = new FormData();
+					formData.append("file", attachment);
+					await api.post(`/schedules/${data.id}/media-upload`, formData);
+				}
+			}
+			toast.success(i18n.t("scheduleModal.success"));
+			if (typeof reload == 'function') {
+				reload();
+			}
+			if (contactId) {
+				if (typeof cleanContact === 'function') {
+					cleanContact();
+					history.push('/schedules');
+				}
+			}
+		} catch (err) {
+			toastError(err);
+		}
+		setCurrentContact(initialContact);
+		setSchedule(initialState);
+		handleClose();
+	};
+	const handleClickMsgVar = async (msgVar, setValueFunc) => {
+		const el = messageInputRef.current;
+		const firstHalfText = el.value.substring(0, el.selectionStart);
+		const secondHalfText = el.value.substring(el.selectionEnd);
+		const newCursorPos = el.selectionStart + msgVar.length;
+
+		setValueFunc("body", `${firstHalfText}${msgVar}${secondHalfText}`);
+
+		await new Promise(r => setTimeout(r, 100));
+		messageInputRef.current.setSelectionRange(newCursorPos, newCursorPos);
+	};
+
+	const deleteMedia = async () => {
+		if (attachment) {
+			setAttachment(null);
+			attachmentFile.current.value = null;
+		}
+
+		if (schedule.mediaPath) {
+			await api.delete(`/schedules/${schedule.id}/media-upload`);
+			setSchedule((prev) => ({
+				...prev,
+				mediaPath: null,
+			}));
+			toast.success(i18n.t("scheduleModal.toasts.deleted"));
+			if (typeof reload == "function") {
+				console.log(reload);
+				console.log("1");
+				reload();
+			}
+		}
+	};
+
+	return (
+		<div className={classes.root}>
+			<ConfirmationModal
+				title={i18n.t("scheduleModal.confirmationModal.deleteTitle")}
+				open={confirmationOpen}
+				onClose={() => setConfirmationOpen(false)}
+				onConfirm={deleteMedia}
+			>
+				{i18n.t("scheduleModal.confirmationModal.deleteMessage")}
+			</ConfirmationModal>
+			<Dialog
+				open={open}
+				onClose={handleClose}
+				maxWidth="xs"
+				fullWidth
+				scroll="paper"
+			>
+				<DialogTitle id="form-dialog-title">
+					{schedule.status === 'ERRO' ? 'Erro de Envio' : `Mensagem ${capitalize(schedule.status)}`}
+				</DialogTitle>
+				<div style={{ display: "none" }}>
+					<input
+						type="file"
+						accept=".png,.jpg,.jpeg"
+						ref={attachmentFile}
+						onChange={(e) => handleAttachmentFile(e)}
+					/>
+				</div>
+				<Formik
+					initialValues={schedule}
+					enableReinitialize={true}
+					validationSchema={ScheduleSchema}
+					onSubmit={(values, actions) => {
+						setTimeout(() => {
+							handleSaveSchedule(values);
+							actions.setSubmitting(false);
+						}, 400);
+					}}
+				>
+					{({ touched, errors, isSubmitting, values, setFieldValue }) => (
+						<Form>
+							<DialogContent dividers>
+								<div className={classes.multFieldLine}>
+									<FormControl
+										variant="outlined"
+										fullWidth
+									>
+										<Autocomplete
+											fullWidth
+											value={currentContact}
+											options={contacts}
+											onChange={(e, contact) => {
+												const contactId = contact ? contact.id : '';
+												setSchedule({ ...schedule, contactId });
+												setCurrentContact(contact ? contact : initialContact);
+											}}
+											getOptionLabel={(option) => option.name}
+											getOptionSelected={(option, value) => {
+												return value.id === option.id
+											}}
+											renderInput={(params) => <TextField {...params} variant="outlined" placeholder="Contato" />}
+										/>
+									</FormControl>
+								</div>
+								<br />
+								<div className={classes.multFieldLine}>
+									<Field
+										as={TextField}
+										rows={9}
+										multiline={true}
+										label={i18n.t("scheduleModal.form.body")}
+										name="body"
+										inputRef={messageInputRef}
+										error={touched.body && Boolean(errors.body)}
+										helperText={touched.body && errors.body}
+										variant="outlined"
+										margin="dense"
+										fullWidth
+									/>
+								</div>
+								<Grid item>
+									<MessageVariablesPicker
+										disabled={isSubmitting}
+										onClick={value => handleClickMsgVar(value, setFieldValue)}
+									/>
+								</Grid>
+								<br />
+								<div className={classes.multFieldLine}>
+									<Field
+										as={TextField}
+										label={i18n.t("scheduleModal.form.sendAt")}
+										type="datetime-local"
+										name="sendAt"
+										InputLabelProps={{
+											shrink: true,
+										}}
+										error={touched.sendAt && Boolean(errors.sendAt)}
+										helperText={touched.sendAt && errors.sendAt}
+										variant="outlined"
+										fullWidth
+									/>
+								</div>
+								{(schedule.mediaPath || attachment) && (
+									<Grid xs={12} item>
+										<Button startIcon={<AttachFile />}>
+											{attachment ? attachment.name : schedule.mediaName}
+										</Button>
+										<IconButton
+											onClick={() => setConfirmationOpen(true)}
+											color="secondary"
+										>
+											<DeleteOutline color="secondary" />
+										</IconButton>
+									</Grid>
+								)}
+							</DialogContent>
+							<DialogActions>
+								{!attachment && !schedule.mediaPath && (
+									<Button
+										color="primary"
+										onClick={() => attachmentFile.current.click()}
+										disabled={isSubmitting}
+										variant="outlined"
+									>
+										{i18n.t("quickMessages.buttons.attach")}
+									</Button>
+								)}
+								<Button
+									onClick={handleClose}
+									color="secondary"
+									disabled={isSubmitting}
+									variant="outlined"
+								>
+									{i18n.t("scheduleModal.buttons.cancel")}
+								</Button>
+								{(schedule.sentAt === null || schedule.sentAt === "") && (
+									<Button
+										type="submit"
+										color="primary"
+										disabled={isSubmitting}
+										variant="contained"
+										className={classes.btnWrapper}
+									>
+										{scheduleId
+											? `${i18n.t("scheduleModal.buttons.okEdit")}`
+											: `${i18n.t("scheduleModal.buttons.okAdd")}`}
+										{isSubmitting && (
+											<CircularProgress
+												size={24}
+												className={classes.buttonProgress}
+											/>
+										)}
+									</Button>
+								)}
+							</DialogActions>
+						</Form>
+					)}
+				</Formik>
+			</Dialog>
+		</div>
+	);
+};
+
+export default ScheduleModal;

+ 141 - 0
frontend/src/components/SchedulesForm/index.js

@@ -0,0 +1,141 @@
+import React, { useState, useEffect } from "react";
+import { makeStyles, TextField, Grid, Container } from "@material-ui/core";
+import { Formik, Form, FastField, FieldArray } from "formik";
+import { isArray } from "lodash";
+import NumberFormat from "react-number-format";
+import ButtonWithSpinner from "../ButtonWithSpinner";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    width: "100%",
+  },
+  fullWidth: {
+    width: "100%",
+  },
+  textfield: {
+    width: "100%",
+  },
+  row: {
+    paddingTop: theme.spacing(2),
+    paddingBottom: theme.spacing(2),
+  },
+  control: {
+    paddingRight: theme.spacing(1),
+    paddingLeft: theme.spacing(1),
+  },
+  buttonContainer: {
+    textAlign: "right",
+    padding: theme.spacing(1),
+  },
+}));
+
+function SchedulesForm(props) {
+  const { initialValues, onSubmit, loading, labelSaveButton } = props;
+  const classes = useStyles();
+
+  const [schedules, setSchedules] = useState([
+    { weekday: "Segunda-feira", weekdayEn: "monday", startTime: "", endTime: "", },
+    { weekday: "Terça-feira", weekdayEn: "tuesday", startTime: "", endTime: "", },
+    { weekday: "Quarta-feira", weekdayEn: "wednesday", startTime: "", endTime: "", },
+    { weekday: "Quinta-feira", weekdayEn: "thursday", startTime: "", endTime: "", },
+    { weekday: "Sexta-feira", weekdayEn: "friday", startTime: "", endTime: "" },
+    { weekday: "Sábado", weekdayEn: "saturday", startTime: "", endTime: "" },
+    { weekday: "Domingo", weekdayEn: "sunday", startTime: "", endTime: "" },
+  ]);
+
+  useEffect(() => {
+    if (isArray(initialValues) && initialValues.length > 0) {
+      setSchedules(initialValues);
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [initialValues]);
+
+  const handleSubmit = (data) => {
+    onSubmit(data);
+  };
+
+  return (
+    <Formik
+      enableReinitialize
+      className={classes.fullWidth}
+      initialValues={{ schedules }}
+      onSubmit={({ schedules }) =>
+        setTimeout(() => {
+          handleSubmit(schedules);
+        }, 500)
+      }
+    >
+      {({ values }) => (
+        <Form className={classes.fullWidth}>
+          <FieldArray
+            name="schedules"
+            render={(arrayHelpers) => (
+              <Grid spacing={4} container>
+                {values.schedules.map((item, index) => {
+                  return (
+                      <Container>
+                          <FastField
+                            as={TextField}
+                            label={i18n.t("settings.schedules.form.weekday")}
+                            name={`schedules[${index}].weekday`}
+                            disabled
+                            variant="outlined"
+                            style={{ marginRight: "3.2%", width: "30%" }}
+                            margin="dense"
+                          />
+                          <FastField
+                            name={`schedules[${index}].startTime`}
+                            >
+                            {({ field }) => (
+                              <NumberFormat
+                                label={i18n.t("settings.schedules.form.initialHour")}
+                                {...field}
+                                variant="outlined"
+                                margin="dense"
+                                customInput={TextField}
+                                format="##:##"
+                                style={{ marginRight: "3.2%", width: "30%" }}
+                              />
+                            )}
+                          </FastField>
+                          <FastField
+                            name={`schedules[${index}].endTime`}
+                            >
+                            {({ field }) => (
+                              <NumberFormat
+                                label={i18n.t("settings.schedules.form.finalHour")}
+                                {...field}
+                                variant="outlined"
+                                margin="dense"
+                                customInput={TextField}
+                                format="##:##"
+                                style={{ marginRight: "3.2%", width: "30%" }}
+                              />
+                            )}
+                          </FastField>
+
+                      </Container>
+
+                  );
+                })}
+              </Grid>
+            )}
+          ></FieldArray>
+          <div style={{ textAlign: "center", marginTop: "2%" }} className={classes.buttonContainer}>
+            <ButtonWithSpinner
+              loading={loading}
+              type="submit"
+              color="primary"
+              variant="contained"
+            >
+              {labelSaveButton ?? i18n.t("settings.schedules.form.save")}
+            </ButtonWithSpinner>
+          </div>
+        </Form>
+      )}
+    </Formik>
+  );
+}
+
+export default SchedulesForm;

+ 727 - 0
frontend/src/components/Settings/Options.js

@@ -0,0 +1,727 @@
+import React, { useEffect, useState } from "react";
+
+import Grid from "@material-ui/core/Grid";
+import MenuItem from "@material-ui/core/MenuItem";
+import FormControl from "@material-ui/core/FormControl";
+import InputLabel from "@material-ui/core/InputLabel";
+import Select from "@material-ui/core/Select";
+import FormHelperText from "@material-ui/core/FormHelperText";
+import TextField from "@material-ui/core/TextField";
+import Title from "../Title";
+import Paper from "@material-ui/core/Paper";
+import Typography from "@material-ui/core/Typography";
+import useSettings from "../../hooks/useSettings";
+import { ToastContainer, toast } from 'react-toastify';
+import { makeStyles } from "@material-ui/core/styles";
+import { grey, blue } from "@material-ui/core/colors";
+import { Tabs, Tab } from "@material-ui/core";
+import { i18n } from "../../translate/i18n";
+
+//import 'react-toastify/dist/ReactToastify.css';
+ 
+const useStyles = makeStyles((theme) => ({
+  container: {
+    paddingTop: theme.spacing(4),
+    paddingBottom: theme.spacing(4),
+  },
+  fixedHeightPaper: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+    height: 240,
+  },
+  tab: {
+    backgroundColor: theme.palette.options,  //DARK MODE PLW DESIGN//
+    borderRadius: 4,
+    width: "100%",
+    "& .MuiTab-wrapper": {
+      color: theme.palette.fontecor,
+    },   //DARK MODE PLW DESIGN//
+    "& .MuiTabs-flexContainer": {
+      justifyContent: "center"
+    }
+
+
+  },
+  paper: {
+    padding: theme.spacing(2),
+    display: "flex",
+    alignItems: "center",
+    marginBottom: 12,
+    width: "100%",
+  },
+  cardAvatar: {
+    fontSize: "55px",
+    color: grey[500],
+    backgroundColor: "#ffffff",
+    width: theme.spacing(7),
+    height: theme.spacing(7),
+  },
+  cardTitle: {
+    fontSize: "18px",
+    color: blue[700],
+  },
+  cardSubtitle: {
+    color: grey[600],
+    fontSize: "14px",
+  },
+  alignRight: {
+    textAlign: "right",
+  },
+  fullWidth: {
+    width: "100%",
+  },
+  selectContainer: {
+    width: "100%",
+    textAlign: "left",
+  },
+}));
+
+export default function Options(props) {
+  const { settings, scheduleTypeChanged } = props;
+  const classes = useStyles();
+  const [userRating, setUserRating] = useState("disabled");
+  const [scheduleType, setScheduleType] = useState("disabled");
+  const [callType, setCallType] = useState("enabled");
+  const [chatbotType, setChatbotType] = useState("");
+  const [CheckMsgIsGroup, setCheckMsgIsGroupType] = useState("enabled");
+
+  const [loadingUserRating, setLoadingUserRating] = useState(false);
+  const [loadingScheduleType, setLoadingScheduleType] = useState(false);
+  const [loadingCallType, setLoadingCallType] = useState(false);
+  const [loadingChatbotType, setLoadingChatbotType] = useState(false);
+  const [loadingCheckMsgIsGroup, setCheckMsgIsGroup] = useState(false);
+
+
+  //const [ipixcType, setIpIxcType] = useState("");
+  //const [loadingIpIxcType, setLoadingIpIxcType] = useState(false);
+  //const [tokenixcType, setTokenIxcType] = useState("");
+  //const [loadingTokenIxcType, setLoadingTokenIxcType] = useState(false);
+
+  //const [ipmkauthType, setIpMkauthType] = useState("");
+  //const [loadingIpMkauthType, setLoadingIpMkauthType] = useState(false);
+  //const [clientidmkauthType, setClientIdMkauthType] = useState("");
+  //const [loadingClientIdMkauthType, setLoadingClientIdMkauthType] = useState(false);
+  //const [clientsecretmkauthType, setClientSecrectMkauthType] = useState("");
+  //const [loadingClientSecrectMkauthType, setLoadingClientSecrectMkauthType] = useState(false);
+
+  const [asaasType, setAsaasType] = useState("");
+  const [loadingAsaasType, setLoadingAsaasType] = useState(false);
+  
+  // recursos a mais da plw design
+
+  const [SendGreetingAccepted, setSendGreetingAccepted] = useState("disabled");
+  const [loadingSendGreetingAccepted, setLoadingSendGreetingAccepted] = useState(false);
+  
+  const [SettingsTransfTicket, setSettingsTransfTicket] = useState("disabled");
+  const [loadingSettingsTransfTicket, setLoadingSettingsTransfTicket] = useState(false);
+  
+  const [sendGreetingMessageOneQueues, setSendGreetingMessageOneQueues] = useState("disabled");
+  const [loadingSendGreetingMessageOneQueues, setLoadingSendGreetingMessageOneQueues] = useState(false);
+
+  const { update } = useSettings();
+
+  useEffect(() => {
+    if (Array.isArray(settings) && settings.length) {
+      const userRating = settings.find((s) => s.key === "userRating");
+      if (userRating) {
+        setUserRating(userRating.value);
+      }
+      const scheduleType = settings.find((s) => s.key === "scheduleType");
+      if (scheduleType) {
+        setScheduleType(scheduleType.value);
+      }
+      const callType = settings.find((s) => s.key === "call");
+      if (callType) {
+        setCallType(callType.value);
+      }
+      const CheckMsgIsGroup = settings.find((s) => s.key === "CheckMsgIsGroup");
+      if (CheckMsgIsGroup) {
+        setCheckMsgIsGroupType(CheckMsgIsGroup.value);
+      }
+	  
+	  {/*PLW DESIGN SAUDAÇÃO*/}
+      const SendGreetingAccepted = settings.find((s) => s.key === "sendGreetingAccepted");
+      if (SendGreetingAccepted) {
+        setSendGreetingAccepted(SendGreetingAccepted.value);
+      }	 
+	  {/*PLW DESIGN SAUDAÇÃO*/}	 
+	  
+	  {/*TRANSFERIR TICKET*/}	
+	  const SettingsTransfTicket = settings.find((s) => s.key === "sendMsgTransfTicket");
+      if (SettingsTransfTicket) {
+        setSettingsTransfTicket(SettingsTransfTicket.value);
+      }
+	  {/*TRANSFERIR TICKET*/}
+
+      const sendGreetingMessageOneQueues = settings.find((s) => s.key === "sendGreetingMessageOneQueues");
+      if (sendGreetingMessageOneQueues) {
+        setSendGreetingMessageOneQueues(sendGreetingMessageOneQueues.value)
+      }	  
+	  
+      const chatbotType = settings.find((s) => s.key === "chatBotType");
+      if (chatbotType) {
+        setChatbotType(chatbotType.value);
+      }
+
+	    {/*const ipixcType = settings.find((s) => s.key === "ipixc");
+      if (ipixcType) {
+        setIpIxcType(ipixcType.value);
+      }*/}
+
+      {/*const tokenixcType = settings.find((s) => s.key === "tokenixc");
+      if (tokenixcType) {
+        setTokenIxcType(tokenixcType.value);
+      }*/}
+
+      {/*const ipmkauthType = settings.find((s) => s.key === "ipmkauth");
+      if (ipmkauthType) {
+        setIpMkauthType(ipmkauthType.value);
+      }*/}
+
+     {/* const clientidmkauthType = settings.find((s) => s.key === "clientidmkauth");
+      if (clientidmkauthType) {
+        setClientIdMkauthType(clientidmkauthType.value);
+      }*/}
+
+      {/*const clientsecretmkauthType = settings.find((s) => s.key === "clientsecretmkauth");
+      if (clientsecretmkauthType) {
+        setClientSecrectMkauthType(clientsecretmkauthType.value);
+      }*/}
+
+      const asaasType = settings.find((s) => s.key === "asaas");
+      if (asaasType) {
+        setAsaasType(asaasType.value);
+      }
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [settings]);
+
+  async function handleChangeUserRating(value) {
+    setUserRating(value);
+    setLoadingUserRating(true);
+    await update({
+      key: "userRating",
+      value,
+    });
+    toast.success(i18n.t("settings.options.toasts.success"));
+    setLoadingUserRating(false);
+  }
+  
+    async function handleSendGreetingMessageOneQueues(value) {
+    setSendGreetingMessageOneQueues(value);
+    setLoadingSendGreetingMessageOneQueues(true);
+    await update({
+      key: "sendGreetingMessageOneQueues",
+      value,
+    });
+	toast.success(i18n.t("settings.options.toasts.success"));
+    setLoadingSendGreetingMessageOneQueues(false);
+  }
+
+  async function handleScheduleType(value) {
+    setScheduleType(value);
+    setLoadingScheduleType(true);
+    await update({
+      key: "scheduleType",
+      value,
+    });
+    //toast.success("Oraçãpeo atualizada com sucesso.");
+    toast.success(i18n.t("settings.options.toasts.success"), {
+      position: "top-right",
+      autoClose: 2000,
+      hideProgressBar: false,
+      closeOnClick: true,
+      pauseOnHover: false,
+      draggable: true,
+      theme: "light",
+      });
+    setLoadingScheduleType(false);
+    if (typeof scheduleTypeChanged === "function") {
+      scheduleTypeChanged(value);
+    }
+  }
+
+  async function handleCallType(value) {
+    setCallType(value);
+    setLoadingCallType(true);
+    await update({
+      key: "call",
+      value,
+    });
+    toast.success(i18n.t("settings.options.toasts.success"));
+    setLoadingCallType(false);
+  }
+
+  async function handleChatbotType(value) {
+    setChatbotType(value);
+    setLoadingChatbotType(true);
+    await update({
+      key: "chatBotType",
+      value,
+    });
+    toast.success(i18n.t("settings.options.toasts.success"));
+    setLoadingChatbotType(false);
+  }
+
+  async function handleGroupType(value) {
+    setCheckMsgIsGroupType(value);
+    setCheckMsgIsGroup(true);
+    await update({
+      key: "CheckMsgIsGroup",
+      value,
+    });
+    toast.success(i18n.t("settings.options.toasts.success"));
+    setCheckMsgIsGroupType(false);
+    /*     if (typeof scheduleTypeChanged === "function") {
+          scheduleTypeChanged(value);
+        } */
+  }
+  
+  {/*NOVO CÓDIGO*/}  
+  async function handleSendGreetingAccepted(value) {
+    setSendGreetingAccepted(value);
+    setLoadingSendGreetingAccepted(true);
+    await update({
+      key: "sendGreetingAccepted",
+      value,
+    });
+	toast.success(i18n.t("settings.options.toasts.success"));
+    setLoadingSendGreetingAccepted(false);
+  }  
+  
+  
+  {/*NOVO CÓDIGO*/}    
+
+  async function handleSettingsTransfTicket(value) {
+    setSettingsTransfTicket(value);
+    setLoadingSettingsTransfTicket(true);
+    await update({
+      key: "sendMsgTransfTicket",
+      value,
+    });
+
+    toast.success(i18n.t("settings.options.toasts.success"));
+    setLoadingSettingsTransfTicket(false);
+  } 
+ 
+ {/*async function handleChangeIPIxc(value) {
+    setIpIxcType(value);
+    setLoadingIpIxcType(true);
+    await update({
+      key: "ipixc",
+      value,
+    });
+    toast.success(i18n.t("settings.options.toasts.success"));
+    setLoadingIpIxcType(false);
+  }
+
+   {/*async function handleChangeTokenIxc(value) {
+    setTokenIxcType(value);
+    setLoadingTokenIxcType(true);
+    await update({
+      key: "tokenixc",
+      value,
+    });
+    toast.success(i18n.t("settings.options.toasts.success"));
+    setLoadingTokenIxcType(false);
+  }
+
+  async function handleChangeIpMkauth(value) {
+    setIpMkauthType(value);
+    setLoadingIpMkauthType(true);
+    await update({
+      key: "ipmkauth",
+      value,
+    });
+    toast.success(i18n.t("settings.options.toasts.success"));
+    setLoadingIpMkauthType(false);
+  }
+
+  async function handleChangeClientIdMkauth(value) {
+    setClientIdMkauthType(value);
+    setLoadingClientIdMkauthType(true);
+    await update({
+      key: "clientidmkauth",
+      value,
+    });
+    toast.success(i18n.t("settings.options.toasts.success"));
+    setLoadingClientIdMkauthType(false);
+  }
+
+  async function handleChangeClientSecrectMkauth(value) {
+    setClientSecrectMkauthType(value);
+    setLoadingClientSecrectMkauthType(true);
+    await update({
+      key: "clientsecretmkauth",
+      value,
+    });
+    toast.success(i18n.t("settings.options.toasts.success"));
+    setLoadingClientSecrectMkauthType(false);
+  }*/}
+
+  async function handleChangeAsaas(value) {
+    setAsaasType(value);
+    setLoadingAsaasType(true);
+    await update({
+      key: "asaas",
+      value,
+    });
+    toast.success(i18n.t("settings.options.toasts.success"));
+    setLoadingAsaasType(false);
+  }
+  return (
+    <>
+      <Grid spacing={3} container>
+        {/* <Grid xs={12} item>
+                    <Title>Configurações Gerais</Title>
+                </Grid> */}
+        <Grid xs={12} sm={6} md={4} item>
+          <FormControl className={classes.selectContainer}>
+            <InputLabel id="ratings-label">{i18n.t("settings.options.fields.ratings.title")}</InputLabel>
+            <Select
+              labelId="ratings-label"
+              value={userRating}
+              onChange={async (e) => {
+                handleChangeUserRating(e.target.value);
+              }}
+            >
+              <MenuItem value={"disabled"}>{i18n.t("settings.options.fields.ratings.disabled")}</MenuItem>
+              <MenuItem value={"enabled"}>{i18n.t("settings.options.fields.ratings.enabled")}</MenuItem>
+            </Select>
+            <FormHelperText>
+              {loadingUserRating && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+        <Grid xs={12} sm={6} md={4} item>
+          <FormControl className={classes.selectContainer}>
+            <InputLabel id="schedule-type-label">
+              {i18n.t("settings.options.fields.expedientManager.title")}
+            </InputLabel>
+            <Select
+              labelId="schedule-type-label"
+              value={scheduleType}
+              onChange={async (e) => {
+                handleScheduleType(e.target.value);
+              }}
+            >
+              <MenuItem value={"disabled"}>{i18n.t("settings.options.fields.disabled")}</MenuItem>
+              <MenuItem value={"queue"}>{i18n.t("settings.options.fields.expedientManager.queue")}</MenuItem>
+              <MenuItem value={"company"}>{i18n.t("settings.options.fields.expedientManager.company")}</MenuItem>
+            </Select>
+            <FormHelperText>
+              {loadingScheduleType && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+        <Grid xs={12} sm={6} md={4} item>
+          <FormControl className={classes.selectContainer}>
+            <InputLabel id="group-type-label">
+              {i18n.t("settings.options.fields.ignoreMessages.title")}
+            </InputLabel>
+            <Select
+              labelId="group-type-label"
+              value={CheckMsgIsGroup}
+              onChange={async (e) => {
+                handleGroupType(e.target.value);
+              }}
+            >
+              <MenuItem value={"disabled"}>{i18n.t("settings.options.fields.disabled")}</MenuItem>
+              <MenuItem value={"enabled"}>{i18n.t("settings.options.fields.active")}</MenuItem>
+            </Select>
+            <FormHelperText>
+              {loadingScheduleType && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+        <Grid xs={12} sm={6} md={4} item>
+          <FormControl className={classes.selectContainer}>
+            <InputLabel id="call-type-label">
+              {i18n.t("settings.options.fields.acceptCall.title")}
+            </InputLabel>
+            <Select
+              labelId="call-type-label"
+              value={callType}
+              onChange={async (e) => {
+                handleCallType(e.target.value);
+              }}
+            >
+              <MenuItem value={"disabled"}>{i18n.t("settings.options.fields.acceptCall.disabled")}</MenuItem>
+              <MenuItem value={"enabled"}>{i18n.t("settings.options.fields.acceptCall.enabled")}</MenuItem>
+            </Select>
+            <FormHelperText>
+              {loadingCallType && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+        <Grid xs={12} sm={6} md={4} item>
+          <FormControl className={classes.selectContainer}>
+            <InputLabel id="chatbot-type-label">
+              {i18n.t("settings.options.fields.chatbotType.title")}
+            </InputLabel>
+            <Select
+              labelId="chatbot-type-label"
+              value={chatbotType}
+              onChange={async (e) => {
+                handleChatbotType(e.target.value);
+              }}
+            >
+              <MenuItem value={"text"}>{i18n.t("settings.options.fields.chatbotType.text")}</MenuItem>
+			 {/*<MenuItem value={"button"}>Botão</MenuItem>*/}
+             {/*<MenuItem value={"list"}>Lista</MenuItem>*/}
+            </Select>
+            <FormHelperText>
+              {loadingChatbotType && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+		{/* ENVIAR SAUDAÇÃO AO ACEITAR O TICKET */}
+        <Grid xs={12} sm={6} md={4} item>
+          <FormControl className={classes.selectContainer}>
+            <InputLabel id="sendGreetingAccepted-label">
+              {i18n.t("settings.options.fields.sendGreetingAccepted.title")}
+            </InputLabel>
+            <Select
+              labelId="sendGreetingAccepted-label"
+              value={SendGreetingAccepted}
+              onChange={async (e) => {
+                handleSendGreetingAccepted(e.target.value);
+              }}
+            >
+              <MenuItem value={"disabled"}>{i18n.t("settings.options.fields.disabled")}</MenuItem>
+              <MenuItem value={"enabled"}>{i18n.t("settings.options.fields.enabled")}</MenuItem>
+            </Select>
+            <FormHelperText>
+              {loadingSendGreetingAccepted && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+		{/* ENVIAR SAUDAÇÃO AO ACEITAR O TICKET */}
+		
+		{/* ENVIAR MENSAGEM DE TRANSFERENCIA DE SETOR/ATENDENTE */}
+        <Grid xs={12} sm={6} md={4} item>
+          <FormControl className={classes.selectContainer}>
+            <InputLabel id="sendMsgTransfTicket-label">
+              {i18n.t("settings.options.fields.sendMsgTransfTicket.title")}
+            </InputLabel>
+            <Select
+              labelId="sendMsgTransfTicket-label"
+              value={SettingsTransfTicket}
+              onChange={async (e) => {
+                handleSettingsTransfTicket(e.target.value);
+              }}
+            >
+              <MenuItem value={"disabled"}>{i18n.t("settings.options.fields.disabled")}</MenuItem>
+              <MenuItem value={"enabled"}>{i18n.t("settings.options.fields.enabled")}</MenuItem>
+            </Select>
+            <FormHelperText>
+              {loadingSettingsTransfTicket && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+		
+		{/* ENVIAR SAUDAÇÃO QUANDO HOUVER SOMENTE 1 FILA */}
+        <Grid xs={12} sm={6} md={4} item>
+          <FormControl className={classes.selectContainer}>
+            <InputLabel id="sendGreetingMessageOneQueues-label">
+              {i18n.t("settings.options.fields.sendGreetingMessageOneQueues.title")}
+            </InputLabel>
+            <Select
+              labelId="sendGreetingMessageOneQueues-label"
+              value={sendGreetingMessageOneQueues}
+              onChange={async (e) => {
+                handleSendGreetingMessageOneQueues(e.target.value);
+              }}
+            >
+              <MenuItem value={"disabled"}>{i18n.t("settings.options.fields.disabled")}</MenuItem>
+              <MenuItem value={"enabled"}>{i18n.t("settings.options.fields.enabled")}</MenuItem>
+            </Select>
+            <FormHelperText>
+              {loadingSendGreetingMessageOneQueues && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+		
+      </Grid>
+      <Grid spacing={3} container>
+        <Tabs
+          indicatorColor="primary"
+          textColor="primary"
+          scrollButtons="on"
+          variant="scrollable"
+          className={classes.tab}
+          style={{
+            marginBottom: 20,
+            marginTop: 20
+          }}
+        >
+          <Tab
+
+            label={i18n.t("settings.options.tabs.integrations")} />
+
+        </Tabs>
+
+      </Grid>
+      {/*-----------------IXC DESATIVADO 4.6.5-----------------*/}
+      {/*<Grid spacing={3} container
+        style={{ marginBottom: 10 }}>
+        <Tabs
+          indicatorColor="primary"
+          textColor="primary"
+          scrollButtons="on"
+          variant="scrollable"
+          className={classes.tab}
+        >
+          <Tab
+
+            label="IXC" />
+
+        </Tabs>
+        <Grid xs={12} sm={6} md={6} item>
+          <FormControl className={classes.selectContainer}>
+            <TextField
+              id="ipixc"
+              name="ipixc"
+              margin="dense"
+              label="IP do IXC"
+              variant="outlined"
+              value={ipixcType}
+              onChange={async (e) => {
+                handleChangeIPIxc(e.target.value);
+              }}
+            >
+            </TextField>
+            <FormHelperText>
+              {loadingIpIxcType && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+        <Grid xs={12} sm={6} md={6} item>
+          <FormControl className={classes.selectContainer}>
+            <TextField
+              id="tokenixc"
+              name="tokenixc"
+              margin="dense"
+              label="Token do IXC"
+              variant="outlined"
+              value={tokenixcType}
+              onChange={async (e) => {
+                handleChangeTokenIxc(e.target.value);
+              }}
+            >
+            </TextField>
+            <FormHelperText>
+              {loadingTokenIxcType && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+      </Grid>*/}
+      {/*-----------------MK-AUTH DESATIVADO 4.6.5-----------------*/}
+      {/*<Grid spacing={3} container
+        style={{ marginBottom: 10 }}>
+        <Tabs
+          indicatorColor="primary"
+          textColor="primary"
+          scrollButtons="on"
+          variant="scrollable"
+          className={classes.tab}
+        >
+          <Tab label="MK-AUTH" />
+
+        </Tabs>
+        <Grid xs={12} sm={12} md={4} item>
+          <FormControl className={classes.selectContainer}>
+            <TextField
+              id="ipmkauth"
+              name="ipmkauth"
+              margin="dense"
+              label="Ip Mk-Auth"
+              variant="outlined"
+              value={ipmkauthType}
+              onChange={async (e) => {
+                handleChangeIpMkauth(e.target.value);
+              }}
+            >
+            </TextField>
+            <FormHelperText>
+              {loadingIpMkauthType && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+        <Grid xs={12} sm={12} md={4} item>
+          <FormControl className={classes.selectContainer}>
+            <TextField
+              id="clientidmkauth"
+              name="clientidmkauth"
+              margin="dense"
+              label="Client Id"
+              variant="outlined"
+              value={clientidmkauthType}
+              onChange={async (e) => {
+                handleChangeClientIdMkauth(e.target.value);
+              }}
+            >
+            </TextField>
+            <FormHelperText>
+              {loadingClientIdMkauthType && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+        <Grid xs={12} sm={12} md={4} item>
+          <FormControl className={classes.selectContainer}>
+            <TextField
+              id="clientsecretmkauth"
+              name="clientsecretmkauth"
+              margin="dense"
+              label="Client Secret"
+              variant="outlined"
+              value={clientsecretmkauthType}
+              onChange={async (e) => {
+                handleChangeClientSecrectMkauth(e.target.value);
+              }}
+            >
+            </TextField>
+            <FormHelperText>
+              {loadingClientSecrectMkauthType && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+      </Grid>*/}
+      {/*-----------------ASAAS-----------------*/}
+      <Grid spacing={3} container
+        style={{ marginBottom: 10 }}>
+        <Tabs
+          indicatorColor="primary"
+          textColor="primary"
+          scrollButtons="on"
+          variant="scrollable"
+          className={classes.tab}
+        >
+          <Tab label="ASAAS" />
+
+        </Tabs>
+        <Grid xs={12} sm={12} md={12} item>
+          <FormControl className={classes.selectContainer}>
+            <TextField
+              id="asaas"
+              name="asaas"
+              margin="dense"
+              label="Token Asaas"
+              variant="outlined"
+              value={asaasType}
+              onChange={async (e) => {
+                handleChangeAsaas(e.target.value);
+              }}
+            >
+            </TextField>
+            <FormHelperText>
+              {loadingAsaasType && i18n.t("settings.options.updating")}
+            </FormHelperText>
+          </FormControl>
+        </Grid>
+      </Grid>
+    </>
+  );
+}

+ 70 - 0
frontend/src/components/SubscriptionModal/index.js

@@ -0,0 +1,70 @@
+import React, { useEffect, useRef } from "react";
+
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Dialog from "@material-ui/core/Dialog";
+import DialogContent from "@material-ui/core/DialogContent";
+import CheckoutPage from "../CheckoutPage/";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    display: "flex",
+    flexWrap: "wrap",
+  },
+  textField: {
+    marginRight: theme.spacing(1),
+    flex: 1,
+  },
+
+  extraAttr: {
+    display: "flex",
+    justifyContent: "center",
+    alignItems: "center",
+  },
+
+  btnWrapper: {
+    position: "relative",
+  },
+
+  buttonProgress: {
+    color: green[500],
+    position: "absolute",
+    top: "50%",
+    left: "50%",
+    marginTop: -12,
+    marginLeft: -12,
+  },
+}));
+
+
+const ContactModal = ({ open, onClose, Invoice, contactId, initialValues, onSave }) => {
+  const classes = useStyles();
+  const isMounted = useRef(true);
+
+
+  useEffect(() => {
+    return () => {
+      isMounted.current = false;
+    };
+  }, []);
+
+
+  const handleClose = () => {
+    onClose();
+  };
+
+  return (
+    <div className={classes.root}>
+      <Dialog open={open} onClose={handleClose} maxWidth="md" scroll="paper">
+        <DialogContent dividers>
+          <CheckoutPage
+            Invoice={Invoice}
+          />
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+};
+
+export default ContactModal;

+ 9 - 0
frontend/src/components/SubscriptionStepper/index.js

@@ -0,0 +1,9 @@
+import React from 'react'
+
+function index() {
+  return (
+    <div>index</div>
+  )
+}
+
+export default index

+ 18 - 0
frontend/src/components/TabPanel/index.js

@@ -0,0 +1,18 @@
+import React from "react";
+
+const TabPanel = ({ children, value, name, ...rest }) => {
+	if (value === name) {
+		return (
+			<div
+				role="tabpanel"
+				id={`simple-tabpanel-${name}`}
+				aria-labelledby={`simple-tab-${name}`}
+				{...rest}
+			>
+				<>{children}</>
+			</div>
+		);
+	} else return null;
+};
+
+export default TabPanel;

+ 52 - 0
frontend/src/components/TableRowSkeleton/index.js

@@ -0,0 +1,52 @@
+import React from "react";
+import TableCell from "@material-ui/core/TableCell";
+import TableRow from "@material-ui/core/TableRow";
+import Skeleton from "@material-ui/lab/Skeleton";
+import { makeStyles } from "@material-ui/core";
+
+const useStyles = makeStyles(theme => ({
+	customTableCell: {
+		display: "flex",
+		alignItems: "center",
+		justifyContent: "center",
+	},
+}));
+
+const TableRowSkeleton = ({ avatar, columns }) => {
+	const classes = useStyles();
+	return (
+		<>
+			<TableRow>
+				{avatar && (
+					<>
+						<TableCell style={{ paddingRight: 0 }}>
+							<Skeleton
+								animation="wave"
+								variant="circle"
+								width={40}
+								height={40}
+							/>
+						</TableCell>
+						<TableCell>
+							<Skeleton animation="wave" height={30} width={80} />
+						</TableCell>
+					</>
+				)}
+				{Array.from({ length: columns }, (_, index) => (
+					<TableCell align="center" key={index}>
+						<div className={classes.customTableCell}>
+							<Skeleton
+								align="center"
+								animation="wave"
+								height={30}
+								width={80}
+							/>
+						</div>
+					</TableCell>
+				))}
+			</TableRow>
+		</>
+	);
+};
+
+export default TableRowSkeleton;

+ 269 - 0
frontend/src/components/TagModal/index.js

@@ -0,0 +1,269 @@
+import React, { useState, useEffect, useContext } from "react";
+
+import * as Yup from "yup";
+import { Formik, Form, Field } from "formik";
+import { toast } from "react-toastify";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import CircularProgress from "@material-ui/core/CircularProgress";
+import { Colorize } from "@material-ui/icons";
+import { ColorBox } from 'material-ui-color';
+
+import { i18n } from "../../translate/i18n";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { IconButton, InputAdornment } from "@material-ui/core";
+import { FormControlLabel, Switch } from '@material-ui/core';
+import Select from "@material-ui/core/Select";
+import MenuItem from "@material-ui/core/MenuItem";
+import InputLabel from "@material-ui/core/InputLabel";
+import Checkbox from '@material-ui/core/Checkbox';
+
+const useStyles = makeStyles(theme => ({
+	root: {
+		display: "flex",
+		flexWrap: "wrap",
+	},
+	multFieldLine: {
+		display: "flex",
+		"& > *:not(:last-child)": {
+			marginRight: theme.spacing(1),
+		},
+	},
+
+	btnWrapper: {
+		position: "relative",
+	},
+
+	buttonProgress: {
+		color: green[500],
+		position: "absolute",
+		top: "50%",
+		left: "50%",
+		marginTop: -12,
+		marginLeft: -12,
+	},
+	formControl: {
+		margin: theme.spacing(1),
+		minWidth: 120,
+	},
+	colorAdorment: {
+		width: 20,
+		height: 20,
+	},
+}));
+
+const TagSchema = Yup.object().shape({
+	name: Yup.string()
+		.min(3, "Mensagem muito curta")
+		.required("Obrigatório")
+});
+
+const TagModal = ({ open, onClose, tagId, reload }) => {
+	const classes = useStyles();
+	const { user } = useContext(AuthContext);
+	const [colorPickerModalOpen, setColorPickerModalOpen] = useState(false);
+
+	const initialState = {
+		name: "",
+		color: "",
+		kanban: 0
+	};
+
+	const [tag, setTag] = useState(initialState);
+	const [ kanban, setKanban] = useState(0);
+
+	useEffect(() => {
+		try {
+			(async () => {
+				if (!tagId) return;
+
+				const { data } = await api.get(`/tags/${tagId}`);
+				setKanban(data.kanban);
+				setTag(prevState => {
+					return { ...prevState, ...data };
+				});
+			})()
+		} catch (err) {
+			toastError(err);
+		}
+	}, [tagId, open]);
+
+	const handleClose = () => {
+		setTag(initialState);
+		setColorPickerModalOpen(false);
+		onClose();
+	};
+
+	const handleKanbanChange = (e) => {
+		setKanban( e.target.checked ? 1 : 0);
+	};
+
+	const handleSaveTag = async values => {
+		const tagData = { ...values, userId: user.id, kanban };
+		try {
+			if (tagId) {
+				await api.put(`/tags/${tagId}`, tagData);
+			} else {
+				await api.post("/tags", tagData);
+			}
+			toast.success(i18n.t("tagModal.success"));
+			if (typeof reload == 'function') {
+				reload();
+			}
+		} catch (err) {
+			toastError(err);
+		}
+		handleClose();
+	};
+
+	return (
+		<div className={classes.root}>
+			<Dialog
+				open={open}
+				onClose={handleClose}
+				maxWidth="xs"
+				fullWidth
+				scroll="paper"
+			>
+				<DialogTitle id="form-dialog-title">
+					{(tagId ? `${i18n.t("tagModal.title.edit")}` : `${i18n.t("tagModal.title.add")}`)}
+				</DialogTitle>
+				<Formik
+					initialValues={tag}
+					enableReinitialize={true}
+					validationSchema={TagSchema}
+					onSubmit={(values, actions) => {
+						setTimeout(() => {
+							handleSaveTag(values);
+							actions.setSubmitting(false);
+						}, 400);
+					}}
+				>
+					{({ touched, errors, isSubmitting, values }) => (
+						<Form>
+							<DialogContent dividers>
+								<div className={classes.multFieldLine}>
+									<Field
+										as={TextField}
+										label={i18n.t("tagModal.form.name")}
+										name="name"
+										error={touched.name && Boolean(errors.name)}
+										helperText={touched.name && errors.name}
+										variant="outlined"
+										margin="dense"
+										onChange={(e) => setTag(prev => ({ ...prev, name: e.target.value }))}
+										fullWidth
+									/>
+								</div>
+								<br />
+								<div className={classes.multFieldLine}>
+									<Field
+										as={TextField}
+										fullWidth
+										label={i18n.t("tagModal.form.color")}
+										name="color"
+										id="color"
+										error={touched.color && Boolean(errors.color)}
+										helperText={touched.color && errors.color}
+										InputProps={{
+											startAdornment: (
+												<InputAdornment position="start">
+													<div
+														style={{ backgroundColor: values.color }}
+														className={classes.colorAdorment}
+													></div>
+												</InputAdornment>
+											),
+											endAdornment: (
+												<IconButton
+													size="small"
+													color="default"
+													onClick={() => setColorPickerModalOpen(!colorPickerModalOpen)}
+												>
+													<Colorize />
+												</IconButton>
+											),
+										}}
+										variant="outlined"
+										margin="dense"
+									/>
+								</div>
+								{(user.profile === "admin" || user.profile === "supervisor") && (
+                                <>
+								<div className={classes.multFieldLine}>
+        							<FormControlLabel
+          								control={
+            								<Checkbox
+             									checked={kanban === 1}
+             									onChange={handleKanbanChange}
+              									value={kanban}
+              									color="primary"
+            								/>
+          								}
+          								label="Kanban"
+          								labelPlacement="start"
+        							/>
+      							</div>
+      							<br />
+                                </>
+								)}
+								{colorPickerModalOpen && (
+									<div>
+										<ColorBox
+											disableAlpha={true}
+											hslGradient={false}
+											style={{ margin: '20px auto 0' }}
+											value={tag.color}
+											onChange={val => {
+												setTag(prev => ({ ...prev, color: `#${val.hex}` }));
+											}}
+										/>
+									</div>
+								)}
+							</DialogContent>
+							<DialogActions>
+								<Button
+									onClick={handleClose}
+									color="secondary"
+									disabled={isSubmitting}
+									variant="outlined"
+								>
+									{i18n.t("tagModal.buttons.cancel")}
+								</Button>
+								<Button
+									type="submit"
+									color="primary"
+									disabled={isSubmitting}
+									variant="contained"
+									className={classes.btnWrapper}
+								>
+									{tagId
+										? `${i18n.t("tagModal.buttons.okEdit")}`
+										: `${i18n.t("tagModal.buttons.okAdd")}`}
+									{isSubmitting && (
+										<CircularProgress
+											size={24}
+											className={classes.buttonProgress}
+										/>
+									)}
+								</Button>
+							</DialogActions>
+						</Form>
+					)}
+				</Formik>
+			</Dialog>
+		</div>
+	);
+};
+
+export default TagModal;

+ 239 - 0
frontend/src/components/TagModal/index.js_Backup

@@ -0,0 +1,239 @@
+import React, { useState, useEffect, useContext } from "react";
+
+import * as Yup from "yup";
+import { Formik, Form, Field } from "formik";
+import { toast } from "react-toastify";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import CircularProgress from "@material-ui/core/CircularProgress";
+import { Colorize } from "@material-ui/icons";
+import { ColorBox } from 'material-ui-color';
+
+import { i18n } from "../../translate/i18n";
+
+import api from "../../services/api";
+import toastError from "../../errors/toastError";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { IconButton, InputAdornment } from "@material-ui/core";
+
+const useStyles = makeStyles(theme => ({
+	root: {
+		display: "flex",
+		flexWrap: "wrap",
+	},
+	multFieldLine: {
+		display: "flex",
+		"& > *:not(:last-child)": {
+			marginRight: theme.spacing(1),
+		},
+	},
+
+	btnWrapper: {
+		position: "relative",
+	},
+
+	buttonProgress: {
+		color: green[500],
+		position: "absolute",
+		top: "50%",
+		left: "50%",
+		marginTop: -12,
+		marginLeft: -12,
+	},
+	formControl: {
+		margin: theme.spacing(1),
+		minWidth: 120,
+	},
+	colorAdorment: {
+		width: 20,
+		height: 20,
+	},
+}));
+
+const TagSchema = Yup.object().shape({
+	name: Yup.string()
+		.min(3, "Mensagem muito curta")
+		.required("Obrigatório")
+});
+
+const TagModal = ({ open, onClose, tagId, reload }) => {
+	const classes = useStyles();
+	const { user } = useContext(AuthContext);
+	const [colorPickerModalOpen, setColorPickerModalOpen] = useState(false);
+
+	const initialState = {
+		name: "",
+		color: ""
+	};
+
+	const [tag, setTag] = useState(initialState);
+
+	useEffect(() => {
+		try {
+			(async () => {
+				if (!tagId) return;
+
+				const { data } = await api.get(`/tags/${tagId}`);
+				setTag(prevState => {
+					return { ...prevState, ...data };
+				});
+			})()
+		} catch (err) {
+			toastError(err);
+		}
+	}, [tagId, open]);
+
+	const handleClose = () => {
+		setTag(initialState);
+		setColorPickerModalOpen(false);
+		onClose();
+	};
+
+	const handleSaveTag = async values => {
+		const tagData = { ...values, userId: user.id };
+		try {
+			if (tagId) {
+				await api.put(`/tags/${tagId}`, tagData);
+			} else {
+				await api.post("/tags", tagData);
+			}
+			toast.success(i18n.t("tagModal.success"));
+			if (typeof reload == 'function') {
+				reload();
+			}
+		} catch (err) {
+			toastError(err);
+		}
+		handleClose();
+	};
+
+	return (
+		<div className={classes.root}>
+			<Dialog
+				open={open}
+				onClose={handleClose}
+				maxWidth="xs"
+				fullWidth
+				scroll="paper"
+			>
+				<DialogTitle id="form-dialog-title">
+					{ (tagId ? `${i18n.t("tagModal.title.edit")}` : `${i18n.t("tagModal.title.add")}`) }
+				</DialogTitle>
+				<Formik
+					initialValues={tag}
+					enableReinitialize={true}
+					validationSchema={TagSchema}
+					onSubmit={(values, actions) => {
+						setTimeout(() => {
+							handleSaveTag(values);
+							actions.setSubmitting(false);
+						}, 400);
+					}}
+				>
+					{({ touched, errors, isSubmitting, values }) => (
+						<Form>
+							<DialogContent dividers>
+								<div className={classes.multFieldLine}>
+									<Field
+										as={TextField}
+										label={i18n.t("tagModal.form.name")}
+										name="name"
+										error={touched.name && Boolean(errors.name)}
+										helperText={touched.name && errors.name}
+										variant="outlined"
+										margin="dense"
+										onChange={(e) => setTag(prev => ({ ...prev, name: e.target.value }))}
+										fullWidth
+									/>
+								</div>
+								<br />
+								<div className={classes.multFieldLine}>
+									<Field
+										as={TextField}
+										fullWidth
+										label={i18n.t("tagModal.form.color")}
+										name="color"
+										id="color"
+										error={touched.color && Boolean(errors.color)}
+										helperText={touched.color && errors.color}
+										InputProps={{
+											startAdornment: (
+												<InputAdornment position="start">
+													<div
+														style={{ backgroundColor: values.color }}
+														className={classes.colorAdorment}
+													></div>
+												</InputAdornment>
+											),
+											endAdornment: (
+												<IconButton
+													size="small"
+													color="default"
+													onClick={() => setColorPickerModalOpen(!colorPickerModalOpen)}
+												>
+													<Colorize />
+												</IconButton>
+											),
+										}}
+										variant="outlined"
+										margin="dense"
+									/>
+								</div>
+
+								{ colorPickerModalOpen && (
+									<div>
+										<ColorBox
+											disableAlpha={true}
+											hslGradient={false}
+											style={{margin: '20px auto 0'}}
+											value={tag.color}
+											onChange={val => {
+												setTag(prev => ({ ...prev, color: `#${val.hex}` }));
+											}}
+										/>
+									</div>
+								)}
+							</DialogContent>
+							<DialogActions>
+								<Button
+									onClick={handleClose}
+									color="secondary"
+									disabled={isSubmitting}
+									variant="outlined"
+								>
+									{i18n.t("tagModal.buttons.cancel")}
+								</Button>
+								<Button
+									type="submit"
+									color="primary"
+									disabled={isSubmitting}
+									variant="contained"
+									className={classes.btnWrapper}
+								>
+									{tagId
+										? `${i18n.t("tagModal.buttons.okEdit")}`
+										: `${i18n.t("tagModal.buttons.okAdd")}`}
+									{isSubmitting && (
+										<CircularProgress
+											size={24}
+											className={classes.buttonProgress}
+										/>
+									)}
+								</Button>
+							</DialogActions>
+						</Form>
+					)}
+				</Formik>
+			</Dialog>
+		</div>
+	);
+};
+
+export default TagModal;

+ 120 - 0
frontend/src/components/TagsContainer/index.js

@@ -0,0 +1,120 @@
+import { Chip, Paper, TextField } from "@material-ui/core";
+import Autocomplete from "@material-ui/lab/Autocomplete";
+import React, { useEffect, useRef, useState } from "react";
+import { isArray, isString } from "lodash";
+import toastError from "../../errors/toastError";
+import api from "../../services/api";
+
+export function TagsContainer({ ticket }) {
+
+    const [tags, setTags] = useState([]);
+    const [selecteds, setSelecteds] = useState([]);
+    const isMounted = useRef(true);
+
+    useEffect(() => {
+        return () => {
+            isMounted.current = false
+        }
+    }, [])
+
+    useEffect(() => {
+        if (isMounted.current) {
+            loadTags().then(() => {
+                if (Array.isArray(ticket.tags)) {
+                    setSelecteds(ticket.tags);
+                } else {
+                    setSelecteds([]);
+                }
+            });
+        }
+    }, [ticket]);
+
+    const createTag = async (data) => {
+        try {
+            const { data: responseData } = await api.post(`/tags`, data);
+            return responseData;
+        } catch (err) {
+            toastError(err);
+        }
+    }
+
+    const loadTags = async () => {
+        try {
+            const { data } = await api.get(`/tags/list`);
+            setTags(data);
+        } catch (err) {
+            toastError(err);
+        }
+    }
+
+    const syncTags = async (data) => {
+        try {
+            const { data: responseData } = await api.post(`/tags/sync`, data);
+            return responseData;
+        } catch (err) {
+            toastError(err);
+        }
+    }
+
+    const onChange = async (value, reason) => {
+        let optionsChanged = []
+        if (reason === 'create-option') {
+            if (isArray(value)) {
+                for (let item of value) {
+                    if (isString(item)) {
+                        const newTag = await createTag({ name: item })
+                        optionsChanged.push(newTag);
+                    } else {
+                        optionsChanged.push(item);
+                    }
+                }
+            }
+            await loadTags();
+        } else {
+            optionsChanged = value;
+        }
+        setSelecteds(optionsChanged);
+        await syncTags({ ticketId: ticket.id, tags: optionsChanged });
+    }
+
+    return (
+        <Paper style={{ padding: 12 }}>
+            <Autocomplete
+                multiple
+                size="small"
+                options={tags}
+                value={selecteds}
+                freeSolo
+                onChange={(e, v, r) => onChange(v, r)}
+                getOptionLabel={(option) => option.name}
+                renderTags={(value, getTagProps) =>
+                    value.map((option, index) => (
+                        <Chip
+                            variant="outlined"
+                            style={{
+                                background: option.color || '#eee',
+                                color: "#FFF",
+                                marginRight: 1,
+                                fontWeight: 600,
+                                borderRadius: 3,
+                                fontSize: "0.8em",
+                                whiteSpace: "nowrap"
+                            }}
+                            label={option.name.toUpperCase()}
+                            {...getTagProps({ index })}
+                            size="small"
+                        />
+                    ))
+                }
+                renderInput={(params) => (
+                    <TextField {...params} variant="outlined" placeholder="Tags" />
+                )}
+                PaperComponent={({ children }) => (
+                    <Paper style={{ width: 400, marginLeft: 12 }}>
+                        {children}
+                    </Paper>
+                )}
+            />
+        </Paper>
+    )
+}

+ 67 - 0
frontend/src/components/TagsFilter/index.js

@@ -0,0 +1,67 @@
+import { Box, Chip, TextField } from "@material-ui/core";
+import Autocomplete from "@material-ui/lab/Autocomplete";
+import React, { useEffect, useState } from "react";
+import toastError from "../../errors/toastError";
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+
+export function TagsFilter({ onFiltered }) {
+  const [tags, setTags] = useState([]);
+  const [selecteds, setSelecteds] = useState([]);
+
+  useEffect(() => {
+    async function fetchData() {
+      await loadTags();
+    }
+    fetchData();
+  }, []);
+
+  const loadTags = async () => {
+    try {
+      const { data } = await api.get(`/tags/list`);
+      setTags(data);
+    } catch (err) {
+      toastError(err);
+    }
+  };
+
+  const onChange = async (value) => {
+    setSelecteds(value);
+    onFiltered(value);
+  };
+
+  return (
+    <Box style={{ padding: 10 }}>
+      <Autocomplete
+        multiple
+        size="small"
+        options={tags}
+        value={selecteds}
+        onChange={(e, v, r) => onChange(v)}
+        getOptionLabel={(option) => option.name}
+        renderTags={(value, getTagProps) =>
+          value.map((option, index) => (
+            <Chip
+              variant="outlined"
+              style={{
+                backgroundColor: option.color || "#eee",
+                textShadow: "1px 1px 1px #000",
+                color: "white",
+              }}
+              label={option.name}
+              {...getTagProps({ index })}
+              size="small"
+            />
+          ))
+        }
+        renderInput={(params) => (
+          <TextField
+            {...params}
+            variant="outlined"
+            placeholder={i18n.t("tickets.filters.tags")}
+          />
+        )}
+      />
+    </Box>
+  );
+}

+ 198 - 0
frontend/src/components/Ticket/index.js

@@ -0,0 +1,198 @@
+import React, { useState, useEffect, useContext } from "react";
+import { useParams, useHistory } from "react-router-dom";
+
+import { toast } from "react-toastify";
+import clsx from "clsx";
+
+import { Paper, makeStyles } from "@material-ui/core";
+
+import ContactDrawer from "../ContactDrawer";
+import MessageInput from "../MessageInputCustom/";
+import TicketHeader from "../TicketHeader";
+import TicketInfo from "../TicketInfo";
+import TicketActionButtons from "../TicketActionButtonsCustom";
+import MessagesList from "../MessagesList";
+import api from "../../services/api";
+import { ReplyMessageProvider } from "../../context/ReplyingMessage/ReplyingMessageContext";
+import toastError from "../../errors/toastError";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { TagsContainer } from "../TagsContainer";
+import { SocketContext } from "../../context/Socket/SocketContext";
+import { i18n } from "../../translate/i18n";
+
+const drawerWidth = 320;
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    display: "flex",
+    height: "100%",
+    position: "relative",
+    overflow: "hidden",
+  },
+
+  mainWrapper: {
+    flex: 1,
+    height: "100%",
+    display: "flex",
+    flexDirection: "column",
+    overflow: "hidden",
+    borderTopLeftRadius: 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 { ticketId } = useParams();
+  const history = useHistory();
+  const classes = useStyles();
+
+  const { user } = useContext(AuthContext);
+
+  const [drawerOpen, setDrawerOpen] = useState(false);
+  const [loading, setLoading] = useState(true);
+  const [contact, setContact] = useState({});
+  const [ticket, setTicket] = useState({});
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      const fetchTicket = async () => {
+        try {
+          const { data } = await api.get("/tickets/u/" + ticketId);
+          const { queueId } = data;
+          const { queues, profile } = user;
+
+          const queueAllowed = queues.find((q) => q.id === queueId);
+          if (queueAllowed === undefined && profile !== "admin") {
+            toast.error(i18n.t("tickets.toasts.unauthorized"));
+            history.push("/tickets");
+            return;
+          }
+
+          setContact(data.contact);
+          setTicket(data);
+          setLoading(false);
+        } catch (err) {
+          setLoading(false);
+          toastError(err);
+        }
+      };
+      fetchTicket();
+    }, 500);
+    return () => clearTimeout(delayDebounceFn);
+  }, [ticketId, user, history]);
+
+  useEffect(() => {
+    const companyId = localStorage.getItem("companyId");
+    const socket = socketManager.getSocket(companyId);
+
+    socket.on("ready", () => socket.emit("joinChatBox", `${ticket.id}`));
+
+    socket.on(`company-${companyId}-ticket`, (data) => {
+      if (data.action === "update" && data.ticket.id === ticket.id) {
+        setTicket(data.ticket);
+      }
+
+      if (data.action === "delete" && data.ticketId === ticket.id) {
+        // toast.success("Ticket deleted sucessfully.");
+        history.push("/tickets");
+      }
+    });
+
+    socket.on(`company-${companyId}-contact`, (data) => {
+      if (data.action === "update") {
+        setContact((prevState) => {
+          if (prevState.id === data.contact?.id) {
+            return { ...prevState, ...data.contact };
+          }
+          return prevState;
+        });
+      }
+    });
+
+    return () => {
+      socket.disconnect();
+    };
+  }, [ticketId, ticket, history, socketManager]);
+
+  const handleDrawerOpen = () => {
+    setDrawerOpen(true);
+  };
+
+  const handleDrawerClose = () => {
+    setDrawerOpen(false);
+  };
+
+  const renderTicketInfo = () => {
+    if (ticket.user !== undefined) {
+      return (
+        <TicketInfo
+          contact={contact}
+          ticket={ticket}
+          onClick={handleDrawerOpen}
+        />
+      );
+    }
+  };
+
+  const renderMessagesList = () => {
+    return (
+      <>
+        <MessagesList
+          ticket={ticket}
+          ticketId={ticket.id}
+          isGroup={ticket.isGroup}
+        ></MessagesList>
+        <MessageInput ticketId={ticket.id} ticketStatus={ticket.status} />
+      </>
+    );
+  };
+
+  return (
+    <div className={classes.root} id="drawer-container">
+      <Paper
+        variant="outlined"
+        elevation={0}
+        className={clsx(classes.mainWrapper, {
+          [classes.mainWrapperShift]: drawerOpen,
+        })}
+      >
+        <TicketHeader loading={loading}>
+          {renderTicketInfo()}
+          <TicketActionButtons ticket={ticket} />
+        </TicketHeader>
+        <Paper>
+          <TagsContainer ticket={ticket} />
+        </Paper>
+        <ReplyMessageProvider>{renderMessagesList()}</ReplyMessageProvider>
+      </Paper>
+      <ContactDrawer
+        open={drawerOpen}
+        handleDrawerClose={handleDrawerClose}
+        contact={contact}
+        loading={loading}
+        ticket={ticket}
+      />
+    </div>
+  );
+};
+
+export default Ticket;

+ 121 - 0
frontend/src/components/TicketActionButtons/index.js

@@ -0,0 +1,121 @@
+import React, { useContext, useState } from "react";
+import { useHistory } from "react-router-dom";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { IconButton } from "@material-ui/core";
+import { MoreVert, Replay } from "@material-ui/icons";
+
+import { i18n } from "../../translate/i18n";
+import api from "../../services/api";
+import TicketOptionsMenu from "../TicketOptionsMenu";
+import ButtonWithSpinner from "../ButtonWithSpinner";
+import toastError from "../../errors/toastError";
+import { AuthContext } from "../../context/Auth/AuthContext";
+
+const useStyles = makeStyles(theme => ({
+	actionButtons: {
+		marginRight: 6,
+		flex: "none",
+		alignSelf: "center",
+		marginLeft: "auto",
+		"& > *": {
+			marginRight: theme.spacing(1),
+			marginLeft: theme.spacing(1),
+		},
+	},
+}));
+
+const TicketActionButtons = ({ ticket }) => {
+	const classes = useStyles();
+	const history = useHistory();
+	const [anchorEl, setAnchorEl] = useState(null);
+	const [loading, setLoading] = useState(false);
+	const ticketOptionsMenuOpen = Boolean(anchorEl);
+	const { user } = useContext(AuthContext);
+
+	const handleOpenTicketOptionsMenu = e => {
+		setAnchorEl(e.currentTarget);
+	};
+
+	const handleCloseTicketOptionsMenu = e => {
+		setAnchorEl(null);
+	};
+
+	const handleUpdateTicketStatus = async (e, status, userId) => {
+		setLoading(true);
+		try {
+			await api.put(`/tickets/${ticket.id}`, {
+				status: status,
+				userId: userId || null,
+			});
+
+			setLoading(false);
+			if (status === "open") {
+				history.push(`/tickets/${ticket.id}`);
+			} else {
+				history.push("/tickets");
+			}
+		} catch (err) {
+			setLoading(false);
+			toastError(err);
+		}
+	};
+
+	return (
+		<div className={classes.actionButtons}>
+			{ticket.status === "closed" && (
+				<ButtonWithSpinner
+					loading={loading}
+					startIcon={<Replay />}
+					size="small"
+					onClick={e => handleUpdateTicketStatus(e, "open", user?.id)}
+				>
+					{i18n.t("messagesList.header.buttons.reopen")}
+				</ButtonWithSpinner>
+			)}
+			{ticket.status === "open" && (
+				<>
+					<ButtonWithSpinner
+						loading={loading}
+						startIcon={<Replay />}
+						size="small"
+						onClick={e => handleUpdateTicketStatus(e, "pending", null)}
+					>
+						{i18n.t("messagesList.header.buttons.return")}
+					</ButtonWithSpinner>
+					<ButtonWithSpinner
+						loading={loading}
+						size="small"
+						variant="contained"
+						color="primary"
+						onClick={e => handleUpdateTicketStatus(e, "closed", user?.id)}
+					>
+						{i18n.t("messagesList.header.buttons.resolve")}
+					</ButtonWithSpinner>
+					<IconButton onClick={handleOpenTicketOptionsMenu}>
+						<MoreVert />
+					</IconButton>
+					<TicketOptionsMenu
+						ticket={ticket}
+						anchorEl={anchorEl}
+						menuOpen={ticketOptionsMenuOpen}
+						handleClose={handleCloseTicketOptionsMenu}
+					/>
+				</>
+			)}
+			{ticket.status === "pending" && (
+				<ButtonWithSpinner
+					loading={loading}
+					size="small"
+					variant="contained"
+					color="primary"
+					onClick={e => handleUpdateTicketStatus(e, "open", user?.id)}
+				>
+					{i18n.t("messagesList.header.buttons.accept")}
+				</ButtonWithSpinner>
+			)}
+		</div>
+	);
+};
+
+export default TicketActionButtons;

+ 149 - 0
frontend/src/components/TicketActionButtonsCustom/index.js

@@ -0,0 +1,149 @@
+import React, { useContext, useState } from "react";
+import { useHistory } from "react-router-dom";
+
+import { makeStyles, createTheme, ThemeProvider } from "@material-ui/core/styles";
+import { IconButton } from "@material-ui/core";
+import { MoreVert, Replay } from "@material-ui/icons";
+
+import { i18n } from "../../translate/i18n";
+import api from "../../services/api";
+import TicketOptionsMenu from "../TicketOptionsMenu";
+import ButtonWithSpinner from "../ButtonWithSpinner";
+import toastError from "../../errors/toastError";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { TicketsContext } from "../../context/Tickets/TicketsContext";
+import CheckCircleIcon from '@material-ui/icons/CheckCircle';
+import UndoRoundedIcon from '@material-ui/icons/UndoRounded';
+import Tooltip from '@material-ui/core/Tooltip';
+import { green } from '@material-ui/core/colors';
+
+
+const useStyles = makeStyles(theme => ({
+	actionButtons: {
+		marginRight: 6,
+		flex: "none",
+		alignSelf: "center",
+		marginLeft: "auto",
+		"& > *": {
+			margin: theme.spacing(0.5),
+		},
+	},
+}));
+
+const TicketActionButtonsCustom = ({ ticket }) => {
+	const classes = useStyles();
+	const history = useHistory();
+	const [anchorEl, setAnchorEl] = useState(null);
+	const [loading, setLoading] = useState(false);
+	const ticketOptionsMenuOpen = Boolean(anchorEl);
+	const { user } = useContext(AuthContext);
+	const { setCurrentTicket } = useContext(TicketsContext);
+
+	const customTheme = createTheme({
+		palette: {
+		  	primary: green,
+		}
+	});
+
+	const handleOpenTicketOptionsMenu = e => {
+		setAnchorEl(e.currentTarget);
+	};
+
+	const handleCloseTicketOptionsMenu = e => {
+		setAnchorEl(null);
+	};
+
+	const handleUpdateTicketStatus = async (e, status, userId) => {
+		setLoading(true);
+		try {
+			await api.put(`/tickets/${ticket.id}`, {
+				status: status,
+				userId: userId || null,
+				useIntegration: status === "closed" ? false : ticket.useIntegration,
+				promptId: status === "closed" ? false : ticket.promptId,
+				integrationId: status === "closed" ? false : ticket.integrationId
+			});
+
+			setLoading(false);
+			if (status === "open") {
+				setCurrentTicket({ ...ticket, code: "#open" });
+			} else {
+				setCurrentTicket({ id: null, code: null })
+				history.push("/tickets");
+			}
+		} catch (err) {
+			setLoading(false);
+			toastError(err);
+		}
+	};
+
+	return (
+		<div className={classes.actionButtons}>
+			{ticket.status === "closed" && (
+				<ButtonWithSpinner
+					loading={loading}
+					startIcon={<Replay />}
+					size="small"
+					onClick={e => handleUpdateTicketStatus(e, "open", user?.id)}
+				>
+					{i18n.t("messagesList.header.buttons.reopen")}
+				</ButtonWithSpinner>
+			)}
+			{ticket.status === "open" && (
+				<>
+					<Tooltip title={i18n.t("messagesList.header.buttons.return")}>
+						<IconButton onClick={e => handleUpdateTicketStatus(e, "pending", null)}>
+							<UndoRoundedIcon />
+						</IconButton>
+					</Tooltip>
+					<ThemeProvider theme={customTheme}>
+						<Tooltip title={i18n.t("messagesList.header.buttons.resolve")}>
+							<IconButton onClick={e => handleUpdateTicketStatus(e, "closed", user?.id)} color="primary">
+								<CheckCircleIcon />
+							</IconButton>
+						</Tooltip>
+					</ThemeProvider>
+					{/* <ButtonWithSpinner
+						loading={loading}
+						startIcon={<Replay />}
+						size="small"
+						onClick={e => handleUpdateTicketStatus(e, "pending", null)}
+					>
+						{i18n.t("messagesList.header.buttons.return")}
+					</ButtonWithSpinner>
+					<ButtonWithSpinner
+						loading={loading}
+						size="small"
+						variant="contained"
+						color="primary"
+						onClick={e => handleUpdateTicketStatus(e, "closed", user?.id)}
+					>
+						{i18n.t("messagesList.header.buttons.resolve")}
+					</ButtonWithSpinner> */}
+					<IconButton onClick={handleOpenTicketOptionsMenu}>
+						<MoreVert />
+					</IconButton>
+					<TicketOptionsMenu
+						ticket={ticket}
+						anchorEl={anchorEl}
+						menuOpen={ticketOptionsMenuOpen}
+						handleClose={handleCloseTicketOptionsMenu}
+					/>
+				</>
+			)}
+			{ticket.status === "pending" && (
+				<ButtonWithSpinner
+					loading={loading}
+					size="small"
+					variant="contained"
+					color="primary"
+					onClick={e => handleUpdateTicketStatus(e, "open", user?.id)}
+				>
+					{i18n.t("messagesList.header.buttons.accept")}
+				</ButtonWithSpinner>
+			)}
+		</div>
+	);
+};
+
+export default TicketActionButtonsCustom;

+ 10 - 0
frontend/src/components/TicketAdvancedLayout/index.js

@@ -0,0 +1,10 @@
+import { styled } from '@material-ui/core/styles';
+import Paper from '@material-ui/core/Paper';
+
+const TicketAdvancedLayout = styled(Paper)({
+    height: `calc(100% - 48px)`,
+    display: "grid",
+    gridTemplateRows: "56px 1fr"
+})
+
+export default TicketAdvancedLayout;

+ 36 - 0
frontend/src/components/TicketHeader/index.js

@@ -0,0 +1,36 @@
+import React from "react";
+
+import { Card } from "@material-ui/core";
+import { makeStyles } from "@material-ui/core/styles";
+import TicketHeaderSkeleton from "../TicketHeaderSkeleton";
+
+const useStyles = makeStyles(theme => ({
+	ticketHeader: {
+		display: "flex",
+		backgroundColor: theme.palette.tabHeaderBackground,
+		flex: "none",
+		borderBottom: "1px solid rgba(0, 0, 0, 0.12)",
+		[theme.breakpoints.down("sm")]: {
+			flexWrap: "wrap"
+		}
+	},
+}));
+
+
+const TicketHeader = ({ loading, children }) => {
+	const classes = useStyles();
+
+	return (
+		<>
+			{loading ? (
+				<TicketHeaderSkeleton />
+			) : (
+				<Card square className={classes.ticketHeader}>
+					{children}
+				</Card>
+			)}
+		</>
+	);
+};
+
+export default TicketHeader;

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů