hiesoftrd před 1 rokem
rodič
revize
eafe15be3c
53 změnil soubory, kde provedl 11550 přidání a 0 odebrání
  1. 358 0
      frontend/src/pages/Annoucements/index.js
  2. 216 0
      frontend/src/pages/CampaignReport/index.js
  3. 419 0
      frontend/src/pages/Campaigns/index.js
  4. 325 0
      frontend/src/pages/CampaignsConfig/index.js
  5. 180 0
      frontend/src/pages/Chat/ChatList.js
  6. 184 0
      frontend/src/pages/Chat/ChatMessages.js
  7. 294 0
      frontend/src/pages/Chat/ChatPopover.js
  8. 456 0
      frontend/src/pages/Chat/index.js
  9. 182 0
      frontend/src/pages/Companies/index.js
  10. 435 0
      frontend/src/pages/Connections/index.js
  11. 437 0
      frontend/src/pages/ContactListItems/index.js
  12. 326 0
      frontend/src/pages/ContactLists/index.js
  13. 401 0
      frontend/src/pages/Contacts/index.js
  14. 119 0
      frontend/src/pages/Dashboard/Chart.js
  15. 131 0
      frontend/src/pages/Dashboard/ChartsDate.js
  16. 144 0
      frontend/src/pages/Dashboard/ChartsUser.js
  17. 110 0
      frontend/src/pages/Dashboard/Filters.js
  18. 12 0
      frontend/src/pages/Dashboard/Title.js
  19. 9 0
      frontend/src/pages/Dashboard/button.css
  20. 694 0
      frontend/src/pages/Dashboard/index.js
  21. 351 0
      frontend/src/pages/Dashboard/index_old.js
  22. 290 0
      frontend/src/pages/Files/index.js
  23. 244 0
      frontend/src/pages/Financeiro/index.js
  24. 356 0
      frontend/src/pages/ForgetPassWord/index.js
  25. 172 0
      frontend/src/pages/Helps/index.js
  26. 183 0
      frontend/src/pages/Kanban/index.js
  27. 13 0
      frontend/src/pages/Kanban/responsive.css
  28. 221 0
      frontend/src/pages/Login/index.js
  29. 222 0
      frontend/src/pages/Login/style.css
  30. 345 0
      frontend/src/pages/MessagesAPI/index.js
  31. 287 0
      frontend/src/pages/Prompts/index.js
  32. 331 0
      frontend/src/pages/QueueIntegration/index.js
  33. 288 0
      frontend/src/pages/Queues/index.js
  34. 327 0
      frontend/src/pages/QuickMessages/index.js
  35. binární
      frontend/src/pages/Schedules.bkp
  36. 22 0
      frontend/src/pages/Schedules/Schedules.css
  37. 325 0
      frontend/src/pages/Schedules/index.js
  38. 129 0
      frontend/src/pages/Settings/index.js
  39. 235 0
      frontend/src/pages/SettingsCustom/index.js
  40. 259 0
      frontend/src/pages/Signup/index.js
  41. 126 0
      frontend/src/pages/Signup/style.css
  42. 140 0
      frontend/src/pages/Subscription/index.js
  43. 304 0
      frontend/src/pages/Tags/index.js
  44. 14 0
      frontend/src/pages/TicketResponsiveContainer/index.js
  45. 83 0
      frontend/src/pages/Tickets/index.js
  46. 110 0
      frontend/src/pages/TicketsAdvanced/index.js
  47. 81 0
      frontend/src/pages/TicketsCustom/index.js
  48. 132 0
      frontend/src/pages/ToDoList/index.js
  49. 294 0
      frontend/src/pages/Users/index.js
  50. 36 0
      frontend/src/routes/Route.js
  51. 183 0
      frontend/src/routes/index.js
  52. 12 0
      frontend/src/services/api.js
  53. 3 0
      frontend/src/services/socket.js

+ 358 - 0
frontend/src/pages/Annoucements/index.js

@@ -0,0 +1,358 @@
+import React, { useState, useEffect, useReducer, useContext } from "react";
+import { toast } from "react-toastify";
+import { useHistory } from "react-router-dom";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+import Button from "@material-ui/core/Button";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableHead from "@material-ui/core/TableHead";
+import TableRow from "@material-ui/core/TableRow";
+import IconButton from "@material-ui/core/IconButton";
+import SearchIcon from "@material-ui/icons/Search";
+import TextField from "@material-ui/core/TextField";
+import InputAdornment from "@material-ui/core/InputAdornment";
+
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import EditIcon from "@material-ui/icons/Edit";
+
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import Title from "../../components/Title";
+
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+import AnnouncementModal from "../../components/AnnouncementModal";
+import ConfirmationModal from "../../components/ConfirmationModal";
+import toastError from "../../errors/toastError";
+import { Grid } from "@material-ui/core";
+import { isArray } from "lodash";
+import { SocketContext } from "../../context/Socket/SocketContext";
+import { AuthContext } from "../../context/Auth/AuthContext";
+
+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 [];
+  }
+};
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    // padding: theme.spacing(1),
+    padding: theme.padding,
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+}));
+
+const Announcements = () => {
+  const classes = useStyles();
+  const history = useHistory();
+
+  const { user } = useContext(AuthContext);
+
+  const [loading, setLoading] = useState(false);
+  const [pageNumber, setPageNumber] = useState(1);
+  const [hasMore, setHasMore] = useState(false);
+  const [selectedAnnouncement, setSelectedAnnouncement] = useState(null);
+  const [deletingAnnouncement, setDeletingAnnouncement] = useState(null);
+  const [announcementModalOpen, setAnnouncementModalOpen] = useState(false);
+  const [confirmModalOpen, setConfirmModalOpen] = useState(false);
+  const [searchParam, setSearchParam] = useState("");
+  const [announcements, dispatch] = useReducer(reducer, []);
+
+  const socketManager = useContext(SocketContext);
+
+  // trava para nao acessar pagina que não pode  
+  useEffect(() => {
+    async function fetchData() {
+      if (!user.super) {
+        toast.error(i18n.t("announcements.toasts.info"));
+        setTimeout(() => {
+          history.push(`/`)
+        }, 1000);
+      }
+    }
+    fetchData();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  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 = user.companyId;
+    const socket = socketManager.getSocket(companyId);
+
+    socket.on(`company-announcement`, (data) => {
+      if (data.action === "update" || data.action === "create") {
+        dispatch({ type: "UPDATE_ANNOUNCEMENTS", payload: data.record });
+      }
+      if (data.action === "delete") {
+        dispatch({ type: "DELETE_ANNOUNCEMENT", payload: +data.id });
+      }
+    });
+    return () => {
+      socket.disconnect();
+    };
+  }, [socketManager, user.companyId]);
+
+  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 handleOpenAnnouncementModal = () => {
+    setSelectedAnnouncement(null);
+    setAnnouncementModalOpen(true);
+  };
+
+  const handleCloseAnnouncementModal = () => {
+    setSelectedAnnouncement(null);
+    setAnnouncementModalOpen(false);
+  };
+
+  const handleSearch = (event) => {
+    setSearchParam(event.target.value.toLowerCase());
+  };
+
+  const handleEditAnnouncement = (announcement) => {
+    setSelectedAnnouncement(announcement);
+    setAnnouncementModalOpen(true);
+  };
+
+  const handleDeleteAnnouncement = async (announcement) => {
+    try {
+      if (announcement.mediaName)
+      await api.delete(`/announcements/${announcement.id}/media-upload`);
+
+      await api.delete(`/announcements/${announcement.id}`);
+      
+      toast.success(i18n.t("announcements.toasts.deleted"));
+    } catch (err) {
+      toastError(err);
+    }
+    setDeletingAnnouncement(null);
+    setSearchParam("");
+    setPageNumber(1);
+  };
+
+  const loadMore = () => {
+    setPageNumber((prevState) => prevState + 1);
+  };
+
+  const handleScroll = (e) => {
+    if (!hasMore || loading) return;
+    const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
+    if (scrollHeight - (scrollTop + 100) < clientHeight) {
+      loadMore();
+    }
+  };
+
+  const translatePriority = (val) => {
+    if (val === 1) {
+      return i18n.t("announcements.high");
+    }
+    if (val === 2) {
+      return i18n.t("announcements.medium");
+    }
+    if (val === 3) {
+      return i18n.t("announcements.low");
+    }
+  };
+
+  return (
+    <MainContainer >
+      <ConfirmationModal
+        title={
+          deletingAnnouncement &&
+          `${i18n.t("announcements.confirmationModal.deleteTitle")} ${deletingAnnouncement.name
+          }?`
+        }
+        open={confirmModalOpen}
+        onClose={setConfirmModalOpen}
+        onConfirm={() => handleDeleteAnnouncement(deletingAnnouncement)}
+      >
+        {i18n.t("announcements.confirmationModal.deleteMessage")}
+      </ConfirmationModal>
+      <AnnouncementModal
+        resetPagination={() => {
+          setPageNumber(1);
+          fetchAnnouncements();
+        }}
+        open={announcementModalOpen}
+        onClose={handleCloseAnnouncementModal}
+        aria-labelledby="form-dialog-title"
+        announcementId={selectedAnnouncement && selectedAnnouncement.id}
+      />
+      <MainHeader>
+        <Grid style={{ width: "99.6%" }} container>
+          <Grid xs={12} sm={8} item>
+            <Title>{i18n.t("announcements.title")} ({announcements.length})</Title>
+          </Grid>
+          <Grid xs={12} sm={4} item>
+            <Grid spacing={2} container>
+              <Grid xs={6} sm={6} item>
+                <TextField
+                  fullWidth
+                  placeholder={i18n.t("announcements.searchPlaceholder")}
+                  type="search"
+                  value={searchParam}
+                  onChange={handleSearch}
+                  InputProps={{
+                    startAdornment: (
+                      <InputAdornment position="start">
+                        <SearchIcon style={{ color: "gray" }} />
+                      </InputAdornment>
+                    ),
+                  }}
+                />
+              </Grid>
+              <Grid xs={6} sm={6} item>
+                <Button
+                  fullWidth
+                  variant="contained"
+                  onClick={handleOpenAnnouncementModal}
+                  color="primary"
+                >
+                  {i18n.t("announcements.buttons.add")}
+                </Button>
+              </Grid>
+            </Grid>
+          </Grid>
+        </Grid>
+      </MainHeader>
+      <Paper
+        className={classes.mainPaper}
+        variant="outlined"
+        onScroll={handleScroll}
+      >
+        <Table size="small">
+          <TableHead>
+            <TableRow>
+              <TableCell align="center">
+                {i18n.t("announcements.table.title")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("announcements.table.priority")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("announcements.table.mediaName")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("announcements.table.status")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("announcements.table.actions")}
+              </TableCell>
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            <>
+              {announcements.map((announcement) => (
+                <TableRow key={announcement.id}>
+                  <TableCell align="center">{announcement.title}</TableCell>
+                  <TableCell align="center">
+                    {translatePriority(announcement.priority)}
+                  </TableCell>
+                  <TableCell align="center">
+                    {announcement.mediaName ?? i18n.t("quickMessages.noAttachment")}
+                  </TableCell>
+                  <TableCell align="center">
+                    {announcement.status ? i18n.t("announcements.active") : i18n.t("announcements.inactive")}
+                  </TableCell>
+                  <TableCell align="center">
+                    <IconButton
+                      size="small"
+                      onClick={() => handleEditAnnouncement(announcement)}
+                    >
+                      <EditIcon />
+                    </IconButton>
+
+                    <IconButton
+                      size="small"
+                      onClick={(e) => {
+                        setConfirmModalOpen(true);
+                        setDeletingAnnouncement(announcement);
+                      }}
+                    >
+                      <DeleteOutlineIcon />
+                    </IconButton>
+                  </TableCell>
+                </TableRow>
+              ))}
+              {loading && <TableRowSkeleton columns={5} />}
+            </>
+          </TableBody>
+        </Table>
+      </Paper>
+    </MainContainer >
+  )
+};
+
+export default Announcements;

+ 216 - 0
frontend/src/pages/CampaignReport/index.js

@@ -0,0 +1,216 @@
+import React, { useEffect, useRef, useState, useContext } from "react";
+import { useParams } from "react-router-dom";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import Title from "../../components/Title";
+
+import { Grid, LinearProgress, Typography } from "@material-ui/core";
+import api from "../../services/api";
+import { has, get, isNull } from "lodash";
+import CardCounter from "../../components/Dashboard/CardCounter";
+import GroupIcon from "@material-ui/icons/Group";
+import ScheduleIcon from "@material-ui/icons/Schedule";
+import EventAvailableIcon from "@material-ui/icons/EventAvailable";
+import CheckCircleIcon from "@material-ui/icons/CheckCircle";
+import WhatsAppIcon from "@material-ui/icons/WhatsApp";
+import ListAltIcon from "@material-ui/icons/ListAlt";
+import { useDate } from "../../hooks/useDate";
+
+import { SocketContext } from "../../context/Socket/SocketContext";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(2),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+  textRight: {
+    textAlign: "right",
+  },
+  tabPanelsContainer: {
+    padding: theme.spacing(2),
+  },
+}));
+
+const CampaignReport = () => {
+  const classes = useStyles();
+
+  const { campaignId } = useParams();
+
+  const [campaign, setCampaign] = useState({});
+  const [validContacts, setValidContacts] = useState(0);
+  const [delivered, setDelivered] = useState(0);
+  const [percent, setPercent] = useState(0);
+  const [loading, setLoading] = useState(false);
+  const mounted = useRef(true);
+
+  const { datetimeToClient } = useDate();
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    if (mounted.current) {
+      findCampaign();
+    }
+
+    return () => {
+      mounted.current = false;
+    };
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  useEffect(() => {
+    if (mounted.current && has(campaign, "shipping")) {
+      if (has(campaign, "contactList")) {
+        const contactList = get(campaign, "contactList");
+        if (!contactList) return;
+        const valids = contactList.contacts.filter((c) => c.isWhatsappValid);
+        setValidContacts(valids.length);
+      }
+
+      if (has(campaign, "shipping")) {
+        const contacts = get(campaign, "shipping");
+        if(!contacts) return;
+        const delivered = contacts.filter((c) => !isNull(c.deliveredAt));
+        setDelivered(delivered.length);
+      }
+    }
+  }, [campaign]);
+
+  useEffect(() => {
+    setPercent((delivered / validContacts) * 100);
+  }, [delivered, validContacts]);
+
+  useEffect(() => {
+    const companyId = localStorage.getItem("companyId");
+    const socket = socketManager.getSocket(companyId);
+
+    socket.on(`company-${companyId}-campaign`, (data) => {
+     
+      if (data.record.id === +campaignId) {
+        setCampaign(data.record);
+
+        if (data.record.status === "FINALIZADA") {
+          setTimeout(() => {
+            findCampaign();
+          }, 5000);
+        }
+      }
+    });
+
+    return () => {
+      socket.disconnect();
+    };
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [campaignId, socketManager]);
+
+  const findCampaign = async () => {
+    setLoading(true);
+    const { data } = await api.get(`/campaigns/${campaignId}`);
+    setCampaign(data);
+    setLoading(false);
+  };
+
+  const formatStatus = (val) => {
+    switch (val) {
+      case "INATIVA":
+        return i18n.t("campaigns.status.inactive");
+      case "PROGRAMADA":
+        return i18n.t("campaigns.status.programmed");
+      case "EM_ANDAMENTO":
+        return i18n.t("campaigns.status.inProgress");
+      case "CANCELADA":
+        return i18n.t("campaigns.status.canceled");
+      case "FINALIZADA":
+        return i18n.t("campaigns.status.finished");
+      default:
+        return val;
+    }
+  };
+
+  return (
+    <MainContainer>
+      <MainHeader>
+        <Grid style={{ width: "99.6%" }} container>
+          <Grid xs={12} item>
+            <Title>{i18n.t("campaigns.report.title")} {campaign.name || i18n.t("campaigns.report.title2")}</Title>
+          </Grid>
+        </Grid>
+      </MainHeader>
+      <Paper className={classes.mainPaper} variant="outlined">
+        <Typography variant="h6" component="h2">
+          Status: {formatStatus(campaign.status)} {delivered} {i18n.t("campaigns.report.of")} {validContacts}
+        </Typography>
+        <Grid spacing={2} container>
+          <Grid xs={12} item>
+            <LinearProgress
+              variant="determinate"
+              style={{ height: 15, borderRadius: 3, margin: "20px 0" }}
+              value={percent}
+            />
+          </Grid>
+          <Grid xs={12} md={4} item>
+            <CardCounter
+              icon={<GroupIcon fontSize="inherit" />}
+              title={i18n.t("campaigns.report.validContacts")}
+              value={validContacts}
+              loading={loading}
+            />
+          </Grid>
+          <Grid xs={12} md={4} item>
+            <CardCounter
+              icon={<CheckCircleIcon fontSize="inherit" />}
+              title={i18n.t("campaigns.report.delivered")}
+              value={delivered}
+              loading={loading}
+            />
+          </Grid>
+          {campaign.whatsappId && (
+            <Grid xs={12} md={4} item>
+              <CardCounter
+                icon={<WhatsAppIcon fontSize="inherit" />}
+                title={i18n.t("campaigns.report.connection")}
+                value={campaign.whatsapp.name}
+                loading={loading}
+              />
+            </Grid>
+          )}
+          {campaign.contactListId && (
+            <Grid xs={12} md={4} item>
+              <CardCounter
+                icon={<ListAltIcon fontSize="inherit" />}
+                title={i18n.t("campaigns.report.contactList")}
+                value={campaign.contactList.name}
+                loading={loading}
+              />
+            </Grid>
+          )}
+          <Grid xs={12} md={4} item>
+            <CardCounter
+              icon={<ScheduleIcon fontSize="inherit" />}
+              title={i18n.t("campaigns.report.schedule")}
+              value={datetimeToClient(campaign.scheduledAt)}
+              loading={loading}
+            />
+          </Grid>
+          <Grid xs={12} md={4} item>
+            <CardCounter
+              icon={<EventAvailableIcon fontSize="inherit" />}
+              title={i18n.t("campaigns.report.conclusion")}
+              value={datetimeToClient(campaign.completedAt)}
+              loading={loading}
+            />
+          </Grid>
+        </Grid>
+      </Paper>
+    </MainContainer>
+  );
+};
+
+export default CampaignReport;

+ 419 - 0
frontend/src/pages/Campaigns/index.js

@@ -0,0 +1,419 @@
+/* eslint-disable no-unused-vars */
+
+import React, { useState, useEffect, useReducer, useContext } from "react";
+import { toast } from "react-toastify";
+
+import { useHistory } from "react-router-dom";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+import Button from "@material-ui/core/Button";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableHead from "@material-ui/core/TableHead";
+import TableRow from "@material-ui/core/TableRow";
+import IconButton from "@material-ui/core/IconButton";
+import SearchIcon from "@material-ui/icons/Search";
+import TextField from "@material-ui/core/TextField";
+import InputAdornment from "@material-ui/core/InputAdornment";
+
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import EditIcon from "@material-ui/icons/Edit";
+import DescriptionIcon from "@material-ui/icons/Description";
+import TimerOffIcon from "@material-ui/icons/TimerOff";
+import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline";
+import PauseCircleOutlineIcon from "@material-ui/icons/PauseCircleOutline";
+
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import Title from "../../components/Title";
+
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+import CampaignModal from "../../components/CampaignModal";
+import ConfirmationModal from "../../components/ConfirmationModal";
+import toastError from "../../errors/toastError";
+import { Grid } from "@material-ui/core";
+import { isArray } from "lodash";
+import { useDate } from "../../hooks/useDate";
+import { SocketContext } from "../../context/Socket/SocketContext";
+
+const reducer = (state, action) => {
+  if (action.type === "LOAD_CAMPAIGNS") {
+    const campaigns = action.payload;
+    const newCampaigns = [];
+
+    if (isArray(campaigns)) {
+      campaigns.forEach((campaign) => {
+        const campaignIndex = state.findIndex((u) => u.id === campaign.id);
+        if (campaignIndex !== -1) {
+          state[campaignIndex] = campaign;
+        } else {
+          newCampaigns.push(campaign);
+        }
+      });
+    }
+
+    return [...state, ...newCampaigns];
+  }
+
+  if (action.type === "UPDATE_CAMPAIGNS") {
+    const campaign = action.payload;
+    const campaignIndex = state.findIndex((u) => u.id === campaign.id);
+
+    if (campaignIndex !== -1) {
+      state[campaignIndex] = campaign;
+      return [...state];
+    } else {
+      return [campaign, ...state];
+    }
+  }
+
+  if (action.type === "DELETE_CAMPAIGN") {
+    const campaignId = action.payload;
+
+    const campaignIndex = state.findIndex((u) => u.id === campaignId);
+    if (campaignIndex !== -1) {
+      state.splice(campaignIndex, 1);
+    }
+    return [...state];
+  }
+
+  if (action.type === "RESET") {
+    return [];
+  }
+};
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+}));
+
+const Campaigns = () => {
+  const classes = useStyles();
+
+  const history = useHistory();
+
+  const [loading, setLoading] = useState(false);
+  const [pageNumber, setPageNumber] = useState(1);
+  const [hasMore, setHasMore] = useState(false);
+  const [selectedCampaign, setSelectedCampaign] = useState(null);
+  const [deletingCampaign, setDeletingCampaign] = useState(null);
+  const [campaignModalOpen, setCampaignModalOpen] = useState(false);
+  const [confirmModalOpen, setConfirmModalOpen] = useState(false);
+  const [searchParam, setSearchParam] = useState("");
+  const [campaigns, dispatch] = useReducer(reducer, []);
+
+  const { datetimeToClient } = useDate();
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+  }, [searchParam]);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      fetchCampaigns();
+    }, 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);
+
+    socket.on(`company-${companyId}-campaign`, (data) => {
+      if (data.action === "update" || data.action === "create") {
+        dispatch({ type: "UPDATE_CAMPAIGNS", payload: data.record });
+      }
+      if (data.action === "delete") {
+        dispatch({ type: "DELETE_CAMPAIGN", payload: +data.id });
+      }
+    });
+    return () => {
+      socket.disconnect();
+    };
+  }, [socketManager]);
+
+  const fetchCampaigns = async () => {
+    try {
+      const { data } = await api.get("/campaigns/", {
+        params: { searchParam, pageNumber },
+      });
+      dispatch({ type: "LOAD_CAMPAIGNS", payload: data.records });
+      setHasMore(data.hasMore);
+      setLoading(false);
+    } catch (err) {
+      toastError(err);
+    }
+  };
+
+  const handleOpenCampaignModal = () => {
+    setSelectedCampaign(null);
+    setCampaignModalOpen(true);
+  };
+
+  const handleCloseCampaignModal = () => {
+    setSelectedCampaign(null);
+    setCampaignModalOpen(false);
+  };
+
+  const handleSearch = (event) => {
+    setSearchParam(event.target.value.toLowerCase());
+  };
+
+  const handleEditCampaign = (campaign) => {
+    setSelectedCampaign(campaign);
+    setCampaignModalOpen(true);
+  };
+
+  const handleDeleteCampaign = async (campaignId) => {
+    try {
+      await api.delete(`/campaigns/${campaignId}`);
+      toast.success(i18n.t("campaigns.toasts.deleted"));
+    } catch (err) {
+      toastError(err);
+    }
+    setDeletingCampaign(null);
+    setSearchParam("");
+    setPageNumber(1);
+  };
+
+  const loadMore = () => {
+    setPageNumber((prevState) => prevState + 1);
+  };
+
+  const handleScroll = (e) => {
+    if (!hasMore || loading) return;
+    const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
+    if (scrollHeight - (scrollTop + 100) < clientHeight) {
+      loadMore();
+    }
+  };
+
+  const formatStatus = (val) => {
+    switch (val) {
+      case "INATIVA":
+        return i18n.t("campaigns.status.inactive");
+      case "PROGRAMADA":
+        return i18n.t("campaigns.status.programmed");
+      case "EM_ANDAMENTO":
+        return i18n.t("campaigns.status.inProgress");
+      case "CANCELADA":
+        return i18n.t("campaigns.status.canceled");
+      case "FINALIZADA":
+        return i18n.t("campaigns.status.finished");
+      default:
+        return val;
+    }
+  };
+
+  const cancelCampaign = async (campaign) => {
+    try {
+      await api.post(`/campaigns/${campaign.id}/cancel`);
+      toast.success(i18n.t("campaigns.toasts.cancel"));
+      setPageNumber(1);
+      fetchCampaigns();
+    } catch (err) {
+      toast.error(err.message);
+    }
+  };
+
+  const restartCampaign = async (campaign) => {
+    try {
+      await api.post(`/campaigns/${campaign.id}/restart`);
+      toast.success(i18n.t("campaigns.toasts.restart"));
+      setPageNumber(1);
+      fetchCampaigns();
+    } catch (err) {
+      toast.error(err.message);
+    }
+  };
+
+  return (
+    <MainContainer>
+      <ConfirmationModal
+        title={
+          deletingCampaign &&
+          `${i18n.t("campaigns.confirmationModal.deleteTitle")} ${
+            deletingCampaign.name
+          }?`
+        }
+        open={confirmModalOpen}
+        onClose={setConfirmModalOpen}
+        onConfirm={() => handleDeleteCampaign(deletingCampaign.id)}
+      >
+        {i18n.t("campaigns.confirmationModal.deleteMessage")}
+      </ConfirmationModal>
+      <CampaignModal
+        resetPagination={() => {
+          setPageNumber(1);
+          fetchCampaigns();
+        }}
+        open={campaignModalOpen}
+        onClose={handleCloseCampaignModal}
+        aria-labelledby="form-dialog-title"
+        campaignId={selectedCampaign && selectedCampaign.id}
+      />
+      <MainHeader>
+        <Grid style={{ width: "99.6%" }} container>
+          <Grid xs={12} sm={8} item>
+            <Title>{i18n.t("campaigns.title")}</Title>
+          </Grid>
+          <Grid xs={12} sm={4} item>
+            <Grid spacing={2} container>
+              <Grid xs={6} sm={6} item>
+                <TextField
+                  fullWidth
+                  placeholder={i18n.t("campaigns.searchPlaceholder")}
+                  type="search"
+                  value={searchParam}
+                  onChange={handleSearch}
+                  InputProps={{
+                    startAdornment: (
+                      <InputAdornment position="start">
+                        <SearchIcon style={{ color: "gray" }} />
+                      </InputAdornment>
+                    ),
+                  }}
+                />
+              </Grid>
+              <Grid xs={6} sm={6} item>
+                <Button
+                  fullWidth
+                  variant="contained"
+                  onClick={handleOpenCampaignModal}
+                  color="primary"
+                >
+                  {i18n.t("campaigns.buttons.add")}
+                </Button>
+              </Grid>
+            </Grid>
+          </Grid>
+        </Grid>
+      </MainHeader>
+      <Paper
+        className={classes.mainPaper}
+        variant="outlined"
+        onScroll={handleScroll}
+      >
+        <Table size="small">
+          <TableHead>
+            <TableRow>
+              <TableCell align="center">
+                {i18n.t("campaigns.table.name")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("campaigns.table.status")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("campaigns.table.contactList")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("campaigns.table.whatsapp")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("campaigns.table.scheduledAt")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("campaigns.table.completedAt")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("campaigns.table.actions")}
+              </TableCell>
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            <>
+              {campaigns.map((campaign) => (
+                <TableRow key={campaign.id}>
+                  <TableCell align="center">{campaign.name}</TableCell>
+                  <TableCell align="center">
+                    {formatStatus(campaign.status)}
+                  </TableCell>
+                  <TableCell align="center">
+                    {campaign.contactListId
+                      ? campaign.contactList.name
+                      : i18n.t("campaigns.table.notDefined")}
+                  </TableCell>
+                  <TableCell align="center">
+                    {campaign.whatsappId
+                      ? campaign.whatsapp.name
+                      : i18n.t("campaigns.table.notDefined2")}
+                  </TableCell>
+                  <TableCell align="center">
+                    {campaign.scheduledAt
+                      ? datetimeToClient(campaign.scheduledAt)
+                      : i18n.t("campaigns.table.notScheduled")}
+                  </TableCell>
+                  <TableCell align="center">
+                    {campaign.completedAt
+                      ? datetimeToClient(campaign.completedAt)
+                      : i18n.t("campaigns.table.notConcluded")}
+                  </TableCell>
+                  <TableCell align="center">
+                    {campaign.status === "EM_ANDAMENTO" && (
+                      <IconButton
+                        onClick={() => cancelCampaign(campaign)}
+                        title={i18n.t("campaigns.table.stopCampaign")}
+                        size="small"
+                      >
+                        <PauseCircleOutlineIcon />
+                      </IconButton>
+                    )}
+                    {campaign.status === "CANCELADA" && (
+                      <IconButton
+                        onClick={() => restartCampaign(campaign)}
+                        title={i18n.t("campaigns.table.stopCampaign")}
+                        size="small"
+                      >
+                        <PlayCircleOutlineIcon />
+                      </IconButton>
+                    )}
+                    <IconButton
+                      onClick={() =>
+                        history.push(`/campaign/${campaign.id}/report`)
+                      }
+                      size="small"
+                    >
+                      <DescriptionIcon />
+                    </IconButton>
+                    <IconButton
+                      size="small"
+                      onClick={() => handleEditCampaign(campaign)}
+                    >
+                      <EditIcon />
+                    </IconButton>
+
+                    <IconButton
+                      size="small"
+                      onClick={(e) => {
+                        setConfirmModalOpen(true);
+                        setDeletingCampaign(campaign);
+                      }}
+                    >
+                      <DeleteOutlineIcon />
+                    </IconButton>
+                  </TableCell>
+                </TableRow>
+              ))}
+              {loading && <TableRowSkeleton columns={8} />}
+            </>
+          </TableBody>
+        </Table>
+      </Paper>
+    </MainContainer>
+  );
+};
+
+export default Campaigns;

+ 325 - 0
frontend/src/pages/CampaignsConfig/index.js

@@ -0,0 +1,325 @@
+import React, { useEffect, useState } from "react";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+import { toast } from "react-toastify";
+
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import Title from "../../components/Title";
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import api from "../../services/api";
+
+import { i18n } from "../../translate/i18n";
+import {
+  Box,
+  Button,
+  FormControl,
+  Grid,
+  IconButton,
+  InputLabel,
+  MenuItem,
+  Select,
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableRow,
+  TextField,
+  Typography,
+} from "@material-ui/core";
+import ConfirmationModal from "../../components/ConfirmationModal";
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+  textRight: {
+    textAlign: "right",
+  },
+  tabPanelsContainer: {
+    padding: theme.spacing(2),
+  },
+}));
+
+const initialSettings = {
+  messageInterval: 20,
+  longerIntervalAfter: 20,
+  greaterInterval: 60,
+  variables: [],
+};
+
+const CampaignsConfig = () => {
+  const classes = useStyles();
+
+  const [settings, setSettings] = useState(initialSettings);
+  const [showVariablesForm, setShowVariablesForm] = useState(false);
+  const [confirmationOpen, setConfirmationOpen] = useState(false);
+  const [selectedKey, setSelectedKey] = useState(null);
+  const [variable, setVariable] = useState({ key: "", value: "" });
+
+  useEffect(() => {
+    api.get("/campaign-settings").then(({ data }) => {
+      const settingsList = [];
+      if (Array.isArray(data) && data.length > 0) {
+        data.forEach((item) => {
+          settingsList.push([item.key, JSON.parse(item.value)]);
+        });
+        setSettings(Object.fromEntries(settingsList));
+      }
+    });
+  }, []);
+
+  const handleOnChangeVariable = (e) => {
+    if (e.target.value !== null) {
+      const changedProp = {};
+      changedProp[e.target.name] = e.target.value;
+      setVariable((prev) => ({ ...prev, ...changedProp }));
+    }
+  };
+
+  const handleOnChangeSettings = (e) => {
+    const changedProp = {};
+    changedProp[e.target.name] = e.target.value;
+    setSettings((prev) => ({ ...prev, ...changedProp }));
+  };
+
+  const addVariable = () => {
+    setSettings((prev) => {
+      const variablesExists = settings.variables.filter(
+        (v) => v.key === variable.key
+      );
+      const variables = prev.variables;
+      if (variablesExists.length === 0) {
+        variables.push(Object.assign({}, variable));
+        setVariable({ key: "", value: "" });
+      }
+      return { ...prev, variables };
+    });
+  };
+
+  const removeVariable = () => {
+    const newList = settings.variables.filter((v) => v.key !== selectedKey);
+    setSettings((prev) => ({ ...prev, variables: newList }));
+    setSelectedKey(null);
+  };
+
+  const saveSettings = async () => {
+    await api.post("/campaign-settings", { settings });
+    toast.success(i18n.t("campaigns.toasts.configSaved"));
+  };
+
+  return (
+    <MainContainer>
+      <ConfirmationModal
+        title={i18n.t("campaigns.confirmationModal.deleteTitle")}
+        open={confirmationOpen}
+        onClose={() => setConfirmationOpen(false)}
+        onConfirm={removeVariable}
+      >
+        {i18n.t("campaigns.confirmationModal.deleteMessage")}
+      </ConfirmationModal>
+      <MainHeader>
+        <Grid style={{ width: "99.6%" }} container>
+          <Grid xs={12} item>
+            <Title>{i18n.t("campaignsConfig.title")}</Title>
+          </Grid>
+        </Grid>
+      </MainHeader>
+      <Paper className={classes.mainPaper} variant="outlined">
+        <Box className={classes.tabPanelsContainer}>
+          <Grid spacing={2} container>
+            <Grid xs={12} item>
+              <Typography component={"h3"}>
+                {i18n.t("campaigns.config.interval")}
+              </Typography>
+            </Grid>
+            <Grid xs={12} md={4} item>
+              <FormControl
+                variant="outlined"
+                className={classes.formControl}
+                fullWidth
+              >
+                <InputLabel id="messageInterval-label">
+                  {i18n.t("campaigns.config.randomInterval")}
+                </InputLabel>
+                <Select
+                  name="messageInterval"
+                  id="messageInterval"
+                  labelId="messageInterval-label"
+                  label={i18n.t("campaigns.config.randomInterval")}
+                  value={settings.messageInterval}
+                  onChange={(e) => handleOnChangeSettings(e)}
+                >
+                  <MenuItem value={0}>{i18n.t("campaigns.config.noInterval")}</MenuItem>
+                  <MenuItem value={5}>5 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={10}>10 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={15}>15 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={20}>20 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                </Select>
+              </FormControl>
+            </Grid>
+            <Grid xs={12} md={4} item>
+              <FormControl
+                variant="outlined"
+                className={classes.formControl}
+                fullWidth
+              >
+                <InputLabel id="longerIntervalAfter-label">
+                  {i18n.t("campaigns.config.biggerInterval")}
+                </InputLabel>
+                <Select
+                  name="longerIntervalAfter"
+                  id="longerIntervalAfter"
+                  labelId="longerIntervalAfter-label"
+                  label={i18n.t("campaigns.config.biggerInterval")}
+                  value={settings.longerIntervalAfter}
+                  onChange={(e) => handleOnChangeSettings(e)}
+                >
+                  <MenuItem value={0}>{i18n.t("campaigns.config.notDefined")}</MenuItem>
+                  <MenuItem value={1}>1 {i18n.t("campaigns.config.second")}</MenuItem>
+                  <MenuItem value={5}>5 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={10}>10 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={15}>15 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={20}>20 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={30}>30 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={40}>40 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={60}>60 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={80}>80 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={100}>100 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={120}>120 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                </Select>
+              </FormControl>
+            </Grid>
+            <Grid xs={12} md={4} item>
+              <FormControl
+                variant="outlined"
+                className={classes.formControl}
+                fullWidth
+              >
+                <InputLabel id="greaterInterval-label">
+                  {i18n.t("campaigns.config.greaterInterval")}
+                </InputLabel>
+                <Select
+                  name="greaterInterval"
+                  id="greaterInterval"
+                  labelId="greaterInterval-label"
+                  label={i18n.t("campaigns.config.greaterInterval")}
+                  value={settings.greaterInterval}
+                  onChange={(e) => handleOnChangeSettings(e)}
+                >
+                  <MenuItem value={0}>{i18n.t("campaigns.config.noInterval")}</MenuItem>
+                  <MenuItem value={1}>1 {i18n.t("campaigns.config.second")}</MenuItem>
+                  <MenuItem value={5}>5 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={10}>10 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={15}>15 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={20}>20 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={30}>30 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={40}>40 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={60}>60 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={80}>80 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={100}>100 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                  <MenuItem value={120}>120 {i18n.t("campaigns.config.seconds")}</MenuItem>
+                </Select>
+              </FormControl>
+            </Grid>
+            <Grid xs={12} className={classes.textRight} item>
+              <Button
+                onClick={() => setShowVariablesForm(!showVariablesForm)}
+                color="primary"
+                style={{ marginRight: 10 }}
+              >
+                {i18n.t("campaigns.config.addVariable")}
+              </Button>
+              <Button
+                onClick={saveSettings}
+                color="primary"
+                variant="contained"
+              >
+                {i18n.t("campaigns.config.save")}
+              </Button>
+            </Grid>
+            {showVariablesForm && (
+              <>
+                <Grid xs={12} md={6} item>
+                  <TextField
+                    label={i18n.t("campaigns.config.shortcut")}
+                    variant="outlined"
+                    value={variable.key}
+                    name="key"
+                    onChange={handleOnChangeVariable}
+                    fullWidth
+                  />
+                </Grid>
+                <Grid xs={12} md={6} item>
+                  <TextField
+                    label={i18n.t("campaigns.config.content")}
+                    variant="outlined"
+                    value={variable.value}
+                    name="value"
+                    onChange={handleOnChangeVariable}
+                    fullWidth
+                  />
+                </Grid>
+                <Grid xs={12} className={classes.textRight} item>
+                  <Button
+                    onClick={() => setShowVariablesForm(!showVariablesForm)}
+                    color="primary"
+                    style={{ marginRight: 10 }}
+                  >
+                    {i18n.t("campaigns.config.close")}
+                  </Button>
+                  <Button
+                    onClick={addVariable}
+                    color="primary"
+                    variant="contained"
+                  >
+                    {i18n.t("campaigns.config.add")}
+                  </Button>
+                </Grid>
+              </>
+            )}
+            {settings.variables.length > 0 && (
+              <Grid xs={12} className={classes.textRight} item>
+                <Table size="small">
+                  <TableHead>
+                    <TableRow>
+                      <TableCell style={{ width: "1%" }}></TableCell>
+                      <TableCell>{i18n.t("campaigns.config.shortcut")}</TableCell>
+                      <TableCell>{i18n.t("campaigns.config.content")}</TableCell>
+                    </TableRow>
+                  </TableHead>
+                  <TableBody>
+                    {Array.isArray(settings.variables) &&
+                      settings.variables.map((v, k) => (
+                        <TableRow key={k}>
+                          <TableCell>
+                            <IconButton
+                              size="small"
+                              onClick={() => {
+                                setSelectedKey(v.key);
+                                setConfirmationOpen(true);
+                              }}
+                            >
+                              <DeleteOutlineIcon />
+                            </IconButton>
+                          </TableCell>
+                          <TableCell>{"{" + v.key + "}"}</TableCell>
+                          <TableCell>{v.value}</TableCell>
+                        </TableRow>
+                      ))}
+                  </TableBody>
+                </Table>
+              </Grid>
+            )}
+          </Grid>
+        </Box>
+      </Paper>
+    </MainContainer>
+  );
+};
+
+export default CampaignsConfig;

+ 180 - 0
frontend/src/pages/Chat/ChatList.js

@@ -0,0 +1,180 @@
+import React, { useContext, useState } from "react";
+import {
+  Chip,
+  IconButton,
+  List,
+  ListItem,
+  ListItemSecondaryAction,
+  ListItemText,
+  makeStyles,
+} from "@material-ui/core";
+
+import { useHistory, useParams } from "react-router-dom";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { useDate } from "../../hooks/useDate";
+
+import DeleteIcon from "@material-ui/icons/Delete";
+import EditIcon from "@material-ui/icons/Edit";
+
+import ConfirmationModal from "../../components/ConfirmationModal";
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles((theme) => ({
+  mainContainer: {
+    display: "flex",
+    flexDirection: "column",
+    position: "relative",
+    flex: 1,
+    height: "calc(100% - 58px)",
+    overflow: "hidden",
+    borderRadius: 0,
+    backgroundColor: theme.palette.boxlist, //DARK MODE PLW DESIGN//
+  },
+  chatList: {
+    display: "flex",
+    flexDirection: "column",
+    position: "relative",
+    flex: 1,
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+  listItem: {
+    cursor: "pointer",
+  },
+}));
+
+export default function ChatList({
+  chats,
+  handleSelectChat,
+  handleDeleteChat,
+  handleEditChat,
+  pageInfo,
+  loading,
+}) {
+  const classes = useStyles();
+  const history = useHistory();
+  const { user } = useContext(AuthContext);
+  const { datetimeToClient } = useDate();
+
+  const [confirmationModal, setConfirmModalOpen] = useState(false);
+  const [selectedChat, setSelectedChat] = useState({});
+
+  const { id } = useParams();
+
+  const goToMessages = async (chat) => {
+    if (unreadMessages(chat) > 0) {
+      try {
+        await api.post(`/chats/${chat.id}/read`, { userId: user.id });
+      } catch (err) {}
+    }
+
+    if (id !== chat.uuid) {
+      history.push(`/chats/${chat.uuid}`);
+      handleSelectChat(chat);
+    }
+  };
+
+  const handleDelete = () => {
+    handleDeleteChat(selectedChat);
+  };
+
+  const unreadMessages = (chat) => {
+    const currentUser = chat.users.find((u) => u.userId === user.id);
+    return currentUser.unreads;
+  };
+
+  const getPrimaryText = (chat) => {
+    const mainText = chat.title;
+    const unreads = unreadMessages(chat);
+    return (
+      <>
+        {mainText}
+        {unreads > 0 && (
+          <Chip
+            size="small"
+            style={{ marginLeft: 5 }}
+            label={unreads}
+            color="secondary"
+          />
+        )}
+      </>
+    );
+  };
+
+  const getSecondaryText = (chat) => {
+    return chat.lastMessage !== ""
+      ? `${datetimeToClient(chat.updatedAt)}: ${chat.lastMessage}`
+      : "";
+  };
+
+  const getItemStyle = (chat) => {
+    return {
+      borderLeft: chat.uuid === id ? "6px solid #002d6e" : null,
+      backgroundColor: chat.uuid === id ? "theme.palette.chatlist" : null,
+    };
+  };
+
+  return (
+    <>
+      <ConfirmationModal
+        title={i18n.t("chat.confirm.title")}
+        open={confirmationModal}
+        onClose={setConfirmModalOpen}
+        onConfirm={handleDelete}
+      >
+        {i18n.t("chat.confirm.message")}
+      </ConfirmationModal>
+      <div className={classes.mainContainer}>
+        <div className={classes.chatList}>
+          <List>
+            {Array.isArray(chats) &&
+              chats.length > 0 &&
+              chats.map((chat, key) => (
+                <ListItem
+                  onClick={() => goToMessages(chat)}
+                  key={key}
+                  className={classes.listItem}
+                  style={getItemStyle(chat)}
+                  button
+                >
+                  <ListItemText
+                    primary={getPrimaryText(chat)}
+                    secondary={getSecondaryText(chat)}
+                  />
+                  {chat.ownerId === user.id && (
+                    <ListItemSecondaryAction>
+                      <IconButton
+                        onClick={() => {
+                          goToMessages(chat).then(() => {
+                            handleEditChat(chat);
+                          });
+                        }}
+                        edge="end"
+                        aria-label="delete"
+                        size="small"
+                        style={{ marginRight: 5 }}
+                      >
+                        <EditIcon />
+                      </IconButton>
+                      <IconButton
+                        onClick={() => {
+                          setSelectedChat(chat);
+                          setConfirmModalOpen(true);
+                        }}
+                        edge="end"
+                        aria-label="delete"
+                        size="small"
+                      >
+                        <DeleteIcon />
+                      </IconButton>
+                    </ListItemSecondaryAction>
+                  )}
+                </ListItem>
+              ))}
+          </List>
+        </div>
+      </div>
+    </>
+  );
+}

+ 184 - 0
frontend/src/pages/Chat/ChatMessages.js

@@ -0,0 +1,184 @@
+import React, { useContext, useEffect, useRef, useState } from "react";
+import {
+  Box,
+  FormControl,
+  IconButton,
+  Input,
+  InputAdornment,
+  makeStyles,
+  Paper,
+  Typography,
+} from "@material-ui/core";
+import SendIcon from "@material-ui/icons/Send";
+
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { useDate } from "../../hooks/useDate";
+import api from "../../services/api";
+import { green } from "@material-ui/core/colors";
+
+const useStyles = makeStyles((theme) => ({
+  mainContainer: {
+    display: "flex",
+    flexDirection: "column",
+    position: "relative",
+    flex: 1,
+    overflow: "hidden",
+    borderRadius: 0,
+    height: "100%",
+    borderLeft: "1px solid rgba(0, 0, 0, 0.12)",
+  },
+  messageList: {
+    position: "relative",
+    overflowY: "auto",
+    height: "100%",
+    ...theme.scrollbarStyles,
+    backgroundColor: theme.palette.chatlist, //DARK MODE PLW DESIGN//
+  },
+  inputArea: {
+    position: "relative",
+    height: "auto",
+  },
+  input: {
+    padding: "20px",
+  },
+  buttonSend: {
+    margin: theme.spacing(1),
+  },
+  boxLeft: {
+    padding: "10px 10px 5px",
+    margin: "10px",
+    position: "relative",
+    backgroundColor: "blue",
+    maxWidth: 300,
+    borderRadius: 10,
+    borderBottomLeftRadius: 0,
+    border: "1px solid rgba(0, 0, 0, 0.12)",
+  },
+  boxRight: {
+    padding: "10px 10px 5px",
+    margin: "10px 10px 10px auto",
+    position: "relative",
+    backgroundColor: "green", //DARK MODE PLW DESIGN//
+    textAlign: "right",
+    maxWidth: 300,
+    borderRadius: 10,
+    borderBottomRightRadius: 0,
+    border: "1px solid rgba(0, 0, 0, 0.12)",
+  },
+}));
+
+export default function ChatMessages({
+  chat,
+  messages,
+  handleSendMessage,
+  handleLoadMore,
+  scrollToBottomRef,
+  pageInfo,
+  loading,
+}) {
+  const classes = useStyles();
+  const { user } = useContext(AuthContext);
+  const { datetimeToClient } = useDate();
+  const baseRef = useRef();
+
+  const [contentMessage, setContentMessage] = useState("");
+
+  const scrollToBottom = () => {
+    if (baseRef.current) {
+      baseRef.current.scrollIntoView({});
+    }
+  };
+
+  const unreadMessages = (chat) => {
+    if (chat !== undefined) {
+      const currentUser = chat.users.find((u) => u.userId === user.id);
+      return currentUser.unreads > 0;
+    }
+    return 0;
+  };
+
+  useEffect(() => {
+    if (unreadMessages(chat) > 0) {
+      try {
+        api.post(`/chats/${chat.id}/read`, { userId: user.id });
+      } catch (err) {}
+    }
+    scrollToBottomRef.current = scrollToBottom;
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  const handleScroll = (e) => {
+    const { scrollTop } = e.currentTarget;
+    if (!pageInfo.hasMore || loading) return;
+    if (scrollTop < 600) {
+      handleLoadMore();
+    }
+  };
+
+  return (
+    <Paper className={classes.mainContainer}>
+      <div onScroll={handleScroll} className={classes.messageList}>
+        {Array.isArray(messages) &&
+          messages.map((item, key) => {
+            if (item.senderId === user.id) {
+              return (
+                <Box key={key} className={classes.boxRight}>
+                  <Typography variant="subtitle2">
+                    {item.sender.name}
+                  </Typography>
+                  {item.message}
+                  <Typography variant="caption" display="block">
+                    {datetimeToClient(item.createdAt)}
+                  </Typography>
+                </Box>
+              );
+            } else {
+              return (
+                <Box key={key} className={classes.boxLeft}>
+                  <Typography variant="subtitle2">
+                    {item.sender.name}
+                  </Typography>
+                  {item.message}
+                  <Typography variant="caption" display="block">
+                    {datetimeToClient(item.createdAt)}
+                  </Typography>
+                </Box>
+              );
+            }
+          })}
+        <div ref={baseRef}></div>
+      </div>
+      <div className={classes.inputArea}>
+        <FormControl variant="outlined" fullWidth>
+          <Input
+            multiline
+            value={contentMessage}
+            onKeyUp={(e) => {
+              if (e.key === "Enter" && contentMessage.trim() !== "") {
+                handleSendMessage(contentMessage);
+                setContentMessage("");
+              }
+            }}
+            onChange={(e) => setContentMessage(e.target.value)}
+            className={classes.input}
+            endAdornment={
+              <InputAdornment position="end">
+                <IconButton
+                  onClick={() => {
+                    if (contentMessage.trim() !== "") {
+                      handleSendMessage(contentMessage);
+                      setContentMessage("");
+                    }
+                  }}
+                  className={classes.buttonSend}
+                >
+                  <SendIcon />
+                </IconButton>
+              </InputAdornment>
+            }
+          />
+        </FormControl>
+      </div>
+    </Paper>
+  );
+}

+ 294 - 0
frontend/src/pages/Chat/ChatPopover.js

@@ -0,0 +1,294 @@
+import React, {
+  useContext,
+  useEffect,
+  useReducer,
+  useRef,
+  useState,
+} from "react";
+import { makeStyles } from "@material-ui/core/styles";
+import toastError from "../../errors/toastError";
+import Popover from "@material-ui/core/Popover";
+import ForumIcon from "@material-ui/icons/Forum";
+import {
+  Badge,
+  IconButton,
+  List,
+  ListItem,
+  ListItemText,
+  Paper,
+  Typography,
+} from "@material-ui/core";
+import api from "../../services/api";
+import { isArray } from "lodash";
+import { SocketContext } from "../../context/Socket/SocketContext";
+import { useDate } from "../../hooks/useDate";
+import { AuthContext } from "../../context/Auth/AuthContext";
+
+import notifySound from "../../assets/chat_notify.mp3";
+import useSound from "use-sound";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    maxHeight: 300,
+    maxWidth: 500,
+    padding: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+}));
+
+const reducer = (state, action) => {
+  if (action.type === "LOAD_CHATS") {
+    const chats = action.payload;
+    const newChats = [];
+
+    if (isArray(chats)) {
+      chats.forEach((chat) => {
+        const chatIndex = state.findIndex((u) => u.id === chat.id);
+        if (chatIndex !== -1) {
+          state[chatIndex] = chat;
+        } else {
+          newChats.push(chat);
+        }
+      });
+    }
+
+    return [...state, ...newChats];
+  }
+
+  if (action.type === "UPDATE_CHATS") {
+    const chat = action.payload;
+    const chatIndex = state.findIndex((u) => u.id === chat.id);
+
+    if (chatIndex !== -1) {
+      state[chatIndex] = chat;
+      return [...state];
+    } else {
+      return [chat, ...state];
+    }
+  }
+
+  if (action.type === "DELETE_CHAT") {
+    const chatId = action.payload;
+
+    const chatIndex = state.findIndex((u) => u.id === chatId);
+    if (chatIndex !== -1) {
+      state.splice(chatIndex, 1);
+    }
+    return [...state];
+  }
+
+  if (action.type === "RESET") {
+    return [];
+  }
+
+  if (action.type === "CHANGE_CHAT") {
+    const changedChats = state.map((chat) => {
+      if (chat.id === action.payload.chat.id) {
+        return action.payload.chat;
+      }
+      return chat;
+    });
+    return changedChats;
+  }
+};
+
+export default function ChatPopover() {
+  const classes = useStyles();
+
+  const { user } = useContext(AuthContext);
+
+  const [loading, setLoading] = useState(false);
+  const [anchorEl, setAnchorEl] = useState(null);
+  const [pageNumber, setPageNumber] = useState(1);
+  const [hasMore, setHasMore] = useState(false);
+  const [searchParam] = useState("");
+  const [chats, dispatch] = useReducer(reducer, []);
+  const [invisible, setInvisible] = useState(true);
+  const { datetimeToClient } = useDate();
+  const [play] = useSound(notifySound);
+  const soundAlertRef = useRef();
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    soundAlertRef.current = play;
+
+    if (!("Notification" in window)) {
+      console.log("This browser doesn't support notifications");
+    } else {
+      Notification.requestPermission();
+    }
+  }, [play]);
+
+  useEffect(() => {
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+  }, [searchParam]);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      fetchChats();
+    }, 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-${companyId}-chat`, (data) => {
+      if (data.action === "new-message") {
+        dispatch({ type: "CHANGE_CHAT", payload: data });
+        const userIds = data.newMessage.chat.users.map(userObj => userObj.userId);
+
+        if (userIds.includes(user.id) && data.newMessage.senderId !== user.id) {
+          soundAlertRef.current();
+        }
+      }
+      if (data.action === "update") {
+        dispatch({ type: "CHANGE_CHAT", payload: data });
+      }
+    });
+    return () => {
+      socket.disconnect();
+    };
+  }, [socketManager, user.id]);
+
+  useEffect(() => {
+    let unreadsCount = 0;
+    if (chats.length > 0) {
+      for (let chat of chats) {
+        for (let chatUser of chat.users) {
+          if (chatUser.userId === user.id) {
+            unreadsCount += chatUser.unreads;
+          }
+        }
+      }
+    }
+    if (unreadsCount > 0) {
+      setInvisible(false);
+    } else {
+      setInvisible(true);
+    }
+  }, [chats, user.id]);
+
+  const fetchChats = async () => {
+    try {
+      const { data } = await api.get("/chats/", {
+        params: { searchParam, pageNumber },
+      });
+      dispatch({ type: "LOAD_CHATS", 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 goToMessages = (chat) => {
+    window.location.href = `/chats/${chat.uuid}`;
+  };
+
+  const open = Boolean(anchorEl);
+  const id = open ? "simple-popover" : undefined;
+
+  return (
+    <div>
+      <IconButton
+        aria-describedby={id}
+        variant="contained"
+        color={invisible ? "default" : "inherit"}
+        onClick={handleClick}
+        style={{ color: "white" }}
+      >
+        <Badge color="secondary" variant="dot" invisible={invisible}>
+          <ForumIcon />
+        </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(chats) &&
+              chats.map((item, key) => (
+                <ListItem
+                  key={key}
+                  style={{
+                    background: key % 2 === 0 ? "#ededed" : "white",
+                    border: "1px solid #eee",
+                    cursor: "pointer",
+                  }}
+                  onClick={() => goToMessages(item)}
+                  button
+                >
+                  <ListItemText
+                    primary={item.lastMessage}
+                    secondary={
+                      <>
+                        <Typography component="span" style={{ fontSize: 12 }}>
+                          {datetimeToClient(item.updatedAt)}
+                        </Typography>
+                        <span style={{ marginTop: 5, display: "block" }}></span>
+                      </>
+                    }
+                  />
+                </ListItem>
+              ))}
+            {isArray(chats) && chats.length === 0 && (
+              <ListItemText primary={i18n.t("mainDrawer.appBar.notRegister")} />
+            )}
+          </List>
+        </Paper>
+      </Popover>
+    </div>
+  );
+}

+ 456 - 0
frontend/src/pages/Chat/index.js

@@ -0,0 +1,456 @@
+import React, { useContext, useEffect, useRef, useState } from "react";
+
+import { useParams, useHistory } from "react-router-dom";
+
+import {
+  Button,
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+  Grid,
+  makeStyles,
+  Paper,
+  Tab,
+  Tabs,
+  TextField,
+} from "@material-ui/core";
+import ChatList from "./ChatList";
+import ChatMessages from "./ChatMessages";
+import { UsersFilter } from "../../components/UsersFilter";
+import api from "../../services/api";
+import { SocketContext } from "../../context/Socket/SocketContext";
+
+import { has, isObject } from "lodash";
+
+import { AuthContext } from "../../context/Auth/AuthContext";
+import withWidth, { isWidthUp } from "@material-ui/core/withWidth";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles((theme) => ({
+  mainContainer: {
+    display: "flex",
+    flexDirection: "column",
+    position: "relative",
+    flex: 1,
+    padding: theme.spacing(2),
+    height: `calc(100% - 48px)`,
+    overflowY: "hidden",
+    border: "1px solid rgba(0, 0, 0, 0.12)",
+  },
+  gridContainer: {
+    flex: 1,
+    height: "100%",
+    border: "1px solid rgba(0, 0, 0, 0.12)",
+    backgroundColor: theme.palette.dark,
+  },
+  gridItem: {
+    height: "100%",
+  },
+  gridItemTab: {
+    height: "92%",
+    width: "100%",
+  },
+  btnContainer: {
+    textAlign: "right",
+    padding: 10,
+  },
+}));
+
+export function ChatModal({
+  open,
+  chat,
+  type,
+  handleClose,
+  handleLoadNewChat,
+}) {
+  const [users, setUsers] = useState([]);
+  const [title, setTitle] = useState("");
+
+  useEffect(() => {
+    setTitle("");
+    setUsers([]);
+    if (type === "edit") {
+      const userList = chat.users.map((u) => ({
+        id: u.user.id,
+        name: u.user.name,
+      }));
+      setUsers(userList);
+      setTitle(chat.title);
+    }
+  }, [chat, open, type]);
+
+  const handleSave = async () => {
+    try {
+      if (!title) {
+        alert(i18n.t("chat.toasts.fillTitle"));
+        return;
+      }
+
+      if (!users || users.length === 0) {
+        alert(i18n.t("chat.toasts.fillUser"));
+        return;
+      }
+
+      if (type === "edit") {
+        await api.put(`/chats/${chat.id}`, {
+          users,
+          title,
+        });
+      } else {
+        const { data } = await api.post("/chats", {
+          users,
+          title,
+        });
+        handleLoadNewChat(data);
+      }
+      handleClose();
+    } catch (err) {}
+  };  
+
+  return (
+    <Dialog
+      open={open}
+      onClose={handleClose}
+      aria-labelledby="alert-dialog-title"
+      aria-describedby="alert-dialog-description"
+    >
+      <DialogTitle id="alert-dialog-title">{i18n.t("chat.modal.title")}</DialogTitle>
+      <DialogContent>
+        <Grid spacing={2} container>
+          <Grid xs={12} style={{ padding: 18 }} item>
+            <TextField
+              label={i18n.t("chat.modal.titleField")}
+              placeholder={i18n.t("chat.modal.titleField")}
+              value={title}
+              onChange={(e) => setTitle(e.target.value)}
+              variant="outlined"
+              size="small"
+              fullWidth
+            />
+          </Grid>
+          <Grid xs={12} item>
+            <UsersFilter
+              onFiltered={(users) => setUsers(users)}
+              initialUsers={users}
+            />
+          </Grid>
+        </Grid>
+      </DialogContent>
+      <DialogActions>
+        <Button onClick={handleClose} color="primary">
+          {i18n.t("chat.buttons.close")}
+        </Button>
+        <Button onClick={handleSave} color="primary" variant="contained">
+          {i18n.t("chat.buttons.save")}
+        </Button>
+      </DialogActions>
+    </Dialog>
+  );
+}
+
+function Chat(props) {
+  const classes = useStyles();
+  const { user } = useContext(AuthContext);
+  const history = useHistory();
+
+  const [showDialog, setShowDialog] = useState(false);
+  const [dialogType, setDialogType] = useState("new");
+  const [currentChat, setCurrentChat] = useState({});
+  const [chats, setChats] = useState([]);
+  const [chatsPageInfo, setChatsPageInfo] = useState({ hasMore: false });
+  const [messages, setMessages] = useState([]);
+  const [messagesPageInfo, setMessagesPageInfo] = useState({ hasMore: false });
+  const [messagesPage, setMessagesPage] = useState(1);
+  const [loading, setLoading] = useState(false);
+  const [tab, setTab] = useState(0);
+  const isMounted = useRef(true);
+  const scrollToBottomRef = useRef();
+  const { id } = useParams();
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    return () => {
+      isMounted.current = false;
+    };
+  }, []);
+
+  useEffect(() => {
+    if (isMounted.current) {
+      findChats().then((data) => {
+        const { records } = data;
+        if (records.length > 0) {
+          setChats(records);
+          setChatsPageInfo(data);
+
+          if (id && records.length) {
+            const chat = records.find((r) => r.uuid === id);
+            selectChat(chat);
+          }
+        }
+      });
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  useEffect(() => {
+    if (isObject(currentChat) && has(currentChat, "id")) {
+      findMessages(currentChat.id).then(() => {
+        if (typeof scrollToBottomRef.current === "function") {
+          setTimeout(() => {
+            scrollToBottomRef.current();
+          }, 300);
+        }
+      });
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [currentChat]);
+
+  useEffect(() => {
+    const companyId = localStorage.getItem("companyId");
+    const socket = socketManager.getSocket(companyId);
+
+    socket.on(`company-${companyId}-chat-user-${user.id}`, (data) => {
+      if (data.action === "create") {
+        setChats((prev) => [data.record, ...prev]);
+      }
+      if (data.action === "update") {
+        const changedChats = chats.map((chat) => {
+          if (chat.id === data.record.id) {
+            setCurrentChat(data.record);
+            return {
+              ...data.record,
+            };
+          }
+          return chat;
+        });
+        setChats(changedChats);
+      }
+    });
+
+    socket.on(`company-${companyId}-chat`, (data) => {
+      if (data.action === "delete") {
+        const filteredChats = chats.filter((c) => c.id !== +data.id);
+        setChats(filteredChats);
+        setMessages([]);
+        setMessagesPage(1);
+        setMessagesPageInfo({ hasMore: false });
+        setCurrentChat({});
+        history.push("/chats");
+      }
+    });
+
+    if (isObject(currentChat) && has(currentChat, "id")) {
+      socket.on(`company-${companyId}-chat-${currentChat.id}`, (data) => {
+        if (data.action === "new-message") {
+          setMessages((prev) => [...prev, data.newMessage]);
+          const changedChats = chats.map((chat) => {
+            if (chat.id === data.newMessage.chatId) {
+              return {
+                ...data.chat,
+              };
+            }
+            return chat;
+          });
+          setChats(changedChats);
+          scrollToBottomRef.current();
+        }
+
+        if (data.action === "update") {
+          const changedChats = chats.map((chat) => {
+            if (chat.id === data.chat.id) {
+              return {
+                ...data.chat,
+              };
+            }
+            return chat;
+          });
+          setChats(changedChats);
+          scrollToBottomRef.current();
+        }
+      });
+    }
+
+    return () => {
+      socket.disconnect();
+    };
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [currentChat, socketManager]);
+
+  const selectChat = (chat) => {
+    try {
+      setMessages([]);
+      setMessagesPage(1);
+      setCurrentChat(chat);
+      setTab(1);
+    } catch (err) {}
+  };
+
+  const sendMessage = async (contentMessage) => {
+    setLoading(true);
+    try {
+      await api.post(`/chats/${currentChat.id}/messages`, {
+        message: contentMessage,
+      });
+    } catch (err) {}
+    setLoading(false);
+  };
+
+  const deleteChat = async (chat) => {
+    try {
+      await api.delete(`/chats/${chat.id}`);
+    } catch (err) {}
+  };
+
+  const findMessages = async (chatId) => {
+    setLoading(true);
+    try {
+      const { data } = await api.get(
+        `/chats/${chatId}/messages?pageNumber=${messagesPage}`
+      );
+      setMessagesPage((prev) => prev + 1);
+      setMessagesPageInfo(data);
+      setMessages((prev) => [...data.records, ...prev]);
+    } catch (err) {}
+    setLoading(false);
+  };
+
+  const loadMoreMessages = async () => {
+    if (!loading) {
+      findMessages(currentChat.id);
+    }
+  };
+
+  const findChats = async () => {
+    try {
+      const { data } = await api.get("/chats");
+      return data;
+    } catch (err) {
+      console.log(err);
+    }
+  };
+
+  const renderGrid = () => {
+    return (
+      <Grid className={classes.gridContainer} container>
+        <Grid className={classes.gridItem} md={3} item>
+          
+            <div className={classes.btnContainer}>
+              <Button
+                onClick={() => {
+                  setDialogType("new");
+                  setShowDialog(true);
+                }}
+                color="primary"
+                variant="contained"
+              >
+                {i18n.t("chat.buttons.new")}
+              </Button>
+            </div>
+          
+          <ChatList
+            chats={chats}
+            pageInfo={chatsPageInfo}
+            loading={loading}
+            handleSelectChat={(chat) => selectChat(chat)}
+            handleDeleteChat={(chat) => deleteChat(chat)}
+            handleEditChat={() => {
+              setDialogType("edit");
+              setShowDialog(true);
+            }}
+          />
+        </Grid>
+        <Grid className={classes.gridItem} md={9} item>
+          {isObject(currentChat) && has(currentChat, "id") && (
+            <ChatMessages
+              chat={currentChat}
+              scrollToBottomRef={scrollToBottomRef}
+              pageInfo={messagesPageInfo}
+              messages={messages}
+              loading={loading}
+              handleSendMessage={sendMessage}
+              handleLoadMore={loadMoreMessages}
+            />
+          )}
+        </Grid>
+      </Grid>
+    );
+  };
+
+  const renderTab = () => {
+    return (
+      <Grid className={classes.gridContainer} container>
+        <Grid md={12} item>
+          <Tabs
+            value={tab}
+            indicatorColor="primary"
+            textColor="primary"
+            onChange={(e, v) => setTab(v)}
+            aria-label="disabled tabs example"
+          >
+            <Tab label={i18n.t("chat.chats")} />
+            <Tab label={i18n.t("chat.messages")} />
+          </Tabs>
+        </Grid>
+        {tab === 0 && (
+          <Grid className={classes.gridItemTab} md={12} item>
+            <div className={classes.btnContainer}>
+              <Button
+                onClick={() => setShowDialog(true)}
+                color="primary"
+                variant="contained"
+              >
+                {i18n.t("chat.buttons.newChat")}
+              </Button>
+            </div>
+            <ChatList
+              chats={chats}
+              pageInfo={chatsPageInfo}
+              loading={loading}
+              handleSelectChat={(chat) => selectChat(chat)}
+              handleDeleteChat={(chat) => deleteChat(chat)}
+            />
+          </Grid>
+        )}
+        {tab === 1 && (
+          <Grid className={classes.gridItemTab} md={12} item>
+            {isObject(currentChat) && has(currentChat, "id") && (
+              <ChatMessages
+                scrollToBottomRef={scrollToBottomRef}
+                pageInfo={messagesPageInfo}
+                messages={messages}
+                loading={loading}
+                handleSendMessage={sendMessage}
+                handleLoadMore={loadMoreMessages}
+              />
+            )}
+          </Grid>
+        )}
+      </Grid>
+    );
+  };
+
+  return (
+    <>
+      <ChatModal
+        type={dialogType}
+        open={showDialog}
+        chat={currentChat}
+        handleLoadNewChat={(data) => {
+          setMessages([]);
+          setMessagesPage(1);
+          setCurrentChat(data);
+          setTab(1);
+          history.push(`/chats/${data.uuid}`);
+        }}
+        handleClose={() => setShowDialog(false)}
+      />
+      <Paper className={classes.mainContainer}>
+        {isWidthUp("md", props.width) ? renderGrid() : renderTab()}
+      </Paper>
+    </>
+  );
+}
+
+export default withWidth()(Chat);

+ 182 - 0
frontend/src/pages/Companies/index.js

@@ -0,0 +1,182 @@
+import React, { useState, useEffect } from "react";
+
+import Avatar from "@material-ui/core/Avatar";
+import Button from "@material-ui/core/Button";
+import CssBaseline from "@material-ui/core/CssBaseline";
+import FormControl from "@material-ui/core/FormControl";
+import InputLabel from '@material-ui/core/InputLabel';
+import TextField from "@material-ui/core/TextField";
+import Select from "@material-ui/core/Select"
+import MenuItem from "@material-ui/core/MenuItem"
+import StoreIcon from "@material-ui/icons/Store";
+import Grid from '@material-ui/core/Grid';
+import Typography from "@material-ui/core/Typography";
+import { makeStyles } from "@material-ui/core/styles";
+import Container from "@material-ui/core/Container";
+
+import { i18n } from "../../translate/i18n";
+import useCompanies from '../../hooks/useCompanies';
+import usePlans from '../../hooks/usePlans';
+import { toast } from "react-toastify";
+import toastError from "../../errors/toastError";
+import { isEqual } from 'lodash'
+
+const useStyles = makeStyles(theme => ({
+	paper: {
+		marginTop: theme.spacing(8),
+		display: "flex",
+		flexDirection: "column",
+		alignItems: "center",
+	},
+	avatar: {
+		margin: theme.spacing(1),
+		backgroundColor: theme.palette.secondary.main,
+	},
+	form: {
+		width: "100%", // Fix IE 11 issue.
+		marginTop: theme.spacing(2),
+	},
+	submit: {
+		margin: theme.spacing(3, 0, 2),
+	}
+}));
+
+const FormCompany = () => {
+	const classes = useStyles();
+	const { getPlanList } = usePlans()
+    const { save: saveCompany } = useCompanies()
+	const [company, setCompany] = useState({ name: "", planId: "", token: "" });
+	const [plans, setPlans] = useState([])
+
+	useEffect(() => {
+		const fetchData = async () => {
+			const list = await getPlanList()
+			setPlans(list);
+		}
+		fetchData();
+		// eslint-disable-next-line react-hooks/exhaustive-deps
+	}, []);
+
+	const handleChangeInput = e => {
+		setCompany({ ...company, [e.target.name]: e.target.value });
+	};
+
+	const handlSubmit = async e => {
+		e.preventDefault();
+		try {
+			await saveCompany(company)
+			setCompany({ name: "", planId: "", token: "" })
+			toast.success(i18n.t("companies.form.success"));
+		} catch (e) {
+			toastError(e)
+		}
+	};
+
+	const renderPlanField = () => {
+		if (plans.length) {
+			return <>
+				<Grid item>
+					<FormControl fullWidth variant="outlined">
+						<InputLabel>Plano</InputLabel>
+						<Select 
+							required
+							id="planId"
+							label={i18n.t("companies.form.plan")}
+							name="planId"
+							value={company.planId}
+							onChange={handleChangeInput}
+							autoComplete="plan"
+						>
+							<MenuItem value={""}>&nbsp;</MenuItem>
+							{ plans.map((plan, index) => {
+								return <MenuItem value={plan.id} key={index}>{ plan.name }</MenuItem>
+							})}
+						</Select>
+					</FormControl>
+				</Grid>
+			</>
+		}
+	}
+
+	const renderNameField = () => {
+		if (plans.length && !isEqual(company.planId, "")) {
+			return <>
+				<Grid item>
+					<TextField
+						variant="outlined"
+						required
+						fullWidth
+						id="name"
+						label={i18n.t("companies.form.name")}
+						name="name"
+						value={company.name}
+						onChange={handleChangeInput}
+						autoComplete="name"
+						autoFocus
+					/>
+				</Grid>
+			</>
+		}
+	}
+
+	const renderTokenField = () => {
+		if (plans.length && !isEqual(company.planId, "")) {
+			return <>
+				<Grid item>
+					<TextField
+						variant="outlined"
+						required
+						fullWidth
+						id="token"
+						label={i18n.t("companies.form.token")}
+						name="token"
+						value={company.token}
+						onChange={handleChangeInput}
+						autoComplete="token"
+						autoFocus
+					/>
+				</Grid>
+			</>
+		}
+	}
+
+	const renderSubmitButton = () => {
+		if (plans.length && !isEqual(company.planId, "")) {
+			return <>
+				<Button
+					type="submit"
+					fullWidth
+					variant="contained"
+					color="primary"
+					className={classes.submit}
+				>
+					{i18n.t("companies.form.submit")}
+				</Button>
+			</>
+		}
+	}
+
+	return (
+		<Container component="main" maxWidth="xs">
+			<CssBaseline />
+			<div className={classes.paper}>
+				<Avatar className={classes.avatar}>
+					<StoreIcon />
+				</Avatar>
+				<Typography component="h1" variant="h5">
+					{i18n.t("companies.title")}
+				</Typography>
+				<form className={classes.form} noValidate onSubmit={handlSubmit}>
+					<Grid container direction="column" spacing={2}>
+						{ renderPlanField() }
+						{ renderNameField() }
+						{ renderTokenField() }
+					</Grid>
+					{ renderSubmitButton() }
+				</form>
+			</div>
+		</Container>
+	);
+};
+
+export default FormCompany;

+ 435 - 0
frontend/src/pages/Connections/index.js

@@ -0,0 +1,435 @@
+import React, { useState, useCallback, useContext } from "react";
+import { toast } from "react-toastify";
+import { format, parseISO } from "date-fns";
+
+import { makeStyles } from "@material-ui/core/styles";
+import { green } from "@material-ui/core/colors";
+import {
+	Button,
+	TableBody,
+	TableRow,
+	TableCell,
+	IconButton,
+	Table,
+	TableHead,
+	Paper,
+	Tooltip,
+	Typography,
+	CircularProgress,
+} from "@material-ui/core";
+import {
+	Edit,
+	CheckCircle,
+	SignalCellularConnectedNoInternet2Bar,
+	SignalCellularConnectedNoInternet0Bar,
+	SignalCellular4Bar,
+	CropFree,
+	DeleteOutline,
+} from "@material-ui/icons";
+
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
+import Title from "../../components/Title";
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+
+import api from "../../services/api";
+import WhatsAppModal from "../../components/WhatsAppModal";
+import ConfirmationModal from "../../components/ConfirmationModal";
+import QrcodeModal from "../../components/QrcodeModal";
+import { i18n } from "../../translate/i18n";
+import { WhatsAppsContext } from "../../context/WhatsApp/WhatsAppsContext";
+import toastError from "../../errors/toastError";
+
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { Can } from "../../components/Can";
+
+const useStyles = makeStyles(theme => ({
+	mainPaper: {
+		flex: 1,
+		padding: theme.spacing(1),
+		overflowY: "scroll",
+		...theme.scrollbarStyles,
+	},
+	customTableCell: {
+		display: "flex",
+		alignItems: "center",
+		justifyContent: "center",
+	},
+	tooltip: {
+		backgroundColor: "#f5f5f9",
+		color: "rgba(0, 0, 0, 0.87)",
+		fontSize: theme.typography.pxToRem(14),
+		border: "1px solid #dadde9",
+		maxWidth: 450,
+	},
+	tooltipPopper: {
+		textAlign: "center",
+	},
+	buttonProgress: {
+		color: green[500],
+	},
+}));
+
+const CustomToolTip = ({ title, content, children }) => {
+	const classes = useStyles();
+
+	return (
+		<Tooltip
+			arrow
+			classes={{
+				tooltip: classes.tooltip,
+				popper: classes.tooltipPopper,
+			}}
+			title={
+				<React.Fragment>
+					<Typography gutterBottom color="inherit">
+						{title}
+					</Typography>
+					{content && <Typography>{content}</Typography>}
+				</React.Fragment>
+			}
+		>
+			{children}
+		</Tooltip>
+	);
+};
+
+const Connections = () => {
+	const classes = useStyles();
+
+	const { user } = useContext(AuthContext);
+	const { whatsApps, loading } = useContext(WhatsAppsContext);
+	const [whatsAppModalOpen, setWhatsAppModalOpen] = useState(false);
+	const [qrModalOpen, setQrModalOpen] = useState(false);
+	const [selectedWhatsApp, setSelectedWhatsApp] = useState(null);
+	const [confirmModalOpen, setConfirmModalOpen] = useState(false);
+	const confirmationModalInitialState = {
+		action: "",
+		title: "",
+		message: "",
+		whatsAppId: "",
+		open: false,
+	};
+	const [confirmModalInfo, setConfirmModalInfo] = useState(
+		confirmationModalInitialState
+	);
+
+	const handleStartWhatsAppSession = async whatsAppId => {
+		try {
+			await api.post(`/whatsappsession/${whatsAppId}`);
+		} catch (err) {
+			toastError(err);
+		}
+	};
+
+	const handleRequestNewQrCode = async whatsAppId => {
+		try {
+			await api.put(`/whatsappsession/${whatsAppId}`);
+		} catch (err) {
+			toastError(err);
+		}
+	};
+
+	const handleOpenWhatsAppModal = () => {
+		setSelectedWhatsApp(null);
+		setWhatsAppModalOpen(true);
+	};
+
+	const handleCloseWhatsAppModal = useCallback(() => {
+		setWhatsAppModalOpen(false);
+		setSelectedWhatsApp(null);
+	}, [setSelectedWhatsApp, setWhatsAppModalOpen]);
+
+	const handleOpenQrModal = whatsApp => {
+		setSelectedWhatsApp(whatsApp);
+		setQrModalOpen(true);
+	};
+
+	const handleCloseQrModal = useCallback(() => {
+		setSelectedWhatsApp(null);
+		setQrModalOpen(false);
+	}, [setQrModalOpen, setSelectedWhatsApp]);
+
+	const handleEditWhatsApp = whatsApp => {
+		setSelectedWhatsApp(whatsApp);
+		setWhatsAppModalOpen(true);
+	};
+
+	const handleOpenConfirmationModal = (action, whatsAppId) => {
+		if (action === "disconnect") {
+			setConfirmModalInfo({
+				action: action,
+				title: i18n.t("connections.confirmationModal.disconnectTitle"),
+				message: i18n.t("connections.confirmationModal.disconnectMessage"),
+				whatsAppId: whatsAppId,
+			});
+		}
+
+		if (action === "delete") {
+			setConfirmModalInfo({
+				action: action,
+				title: i18n.t("connections.confirmationModal.deleteTitle"),
+				message: i18n.t("connections.confirmationModal.deleteMessage"),
+				whatsAppId: whatsAppId,
+			});
+		}
+		setConfirmModalOpen(true);
+	};
+
+	const handleSubmitConfirmationModal = async () => {
+		if (confirmModalInfo.action === "disconnect") {
+			try {
+				await api.delete(`/whatsappsession/${confirmModalInfo.whatsAppId}`);
+			} catch (err) {
+				toastError(err);
+			}
+		}
+
+		if (confirmModalInfo.action === "delete") {
+			try {
+				await api.delete(`/whatsapp/${confirmModalInfo.whatsAppId}`);
+				toast.success(i18n.t("connections.toasts.deleted"));
+			} catch (err) {
+				toastError(err);
+			}
+		}
+
+		setConfirmModalInfo(confirmationModalInitialState);
+	};
+
+	const renderActionButtons = whatsApp => {
+		return (
+			<>
+				{whatsApp.status === "qrcode" && (
+					<Button
+						size="small"
+						variant="contained"
+						color="primary"
+						onClick={() => handleOpenQrModal(whatsApp)}
+					>
+						{i18n.t("connections.buttons.qrcode")}
+					</Button>
+				)}
+				{whatsApp.status === "DISCONNECTED" && (
+					<>
+						<Button
+							size="small"
+							variant="outlined"
+							color="primary"
+							onClick={() => handleStartWhatsAppSession(whatsApp.id)}
+						>
+							{i18n.t("connections.buttons.tryAgain")}
+						</Button>{" "}
+						<Button
+							size="small"
+							variant="outlined"
+							color="secondary"
+							onClick={() => handleRequestNewQrCode(whatsApp.id)}
+						>
+							{i18n.t("connections.buttons.newQr")}
+						</Button>
+					</>
+				)}
+				{(whatsApp.status === "CONNECTED" ||
+					whatsApp.status === "PAIRING" ||
+					whatsApp.status === "TIMEOUT") && (
+					<Button
+						size="small"
+						variant="outlined"
+						color="secondary"
+						onClick={() => {
+							handleOpenConfirmationModal("disconnect", whatsApp.id);
+						}}
+					>
+						{i18n.t("connections.buttons.disconnect")}
+					</Button>
+				)}
+				{whatsApp.status === "OPENING" && (
+					<Button size="small" variant="outlined" disabled color="default">
+						{i18n.t("connections.buttons.connecting")}
+					</Button>
+				)}
+			</>
+		);
+	};
+
+	const renderStatusToolTips = whatsApp => {
+		return (
+			<div className={classes.customTableCell}>
+				{whatsApp.status === "DISCONNECTED" && (
+					<CustomToolTip
+						title={i18n.t("connections.toolTips.disconnected.title")}
+						content={i18n.t("connections.toolTips.disconnected.content")}
+					>
+						<SignalCellularConnectedNoInternet0Bar color="secondary" />
+					</CustomToolTip>
+				)}
+				{whatsApp.status === "OPENING" && (
+					<CircularProgress size={24} className={classes.buttonProgress} />
+				)}
+				{whatsApp.status === "qrcode" && (
+					<CustomToolTip
+						title={i18n.t("connections.toolTips.qrcode.title")}
+						content={i18n.t("connections.toolTips.qrcode.content")}
+					>
+						<CropFree />
+					</CustomToolTip>
+				)}
+				{whatsApp.status === "CONNECTED" && (
+					<CustomToolTip title={i18n.t("connections.toolTips.connected.title")}>
+						<SignalCellular4Bar style={{ color: green[500] }} />
+					</CustomToolTip>
+				)}
+				{(whatsApp.status === "TIMEOUT" || whatsApp.status === "PAIRING") && (
+					<CustomToolTip
+						title={i18n.t("connections.toolTips.timeout.title")}
+						content={i18n.t("connections.toolTips.timeout.content")}
+					>
+						<SignalCellularConnectedNoInternet2Bar color="secondary" />
+					</CustomToolTip>
+				)}
+			</div>
+		);
+	};
+
+	return (
+		<MainContainer>
+			<ConfirmationModal
+				title={confirmModalInfo.title}
+				open={confirmModalOpen}
+				onClose={setConfirmModalOpen}
+				onConfirm={handleSubmitConfirmationModal}
+			>
+				{confirmModalInfo.message}
+			</ConfirmationModal>
+			<QrcodeModal
+				open={qrModalOpen}
+				onClose={handleCloseQrModal}
+				whatsAppId={!whatsAppModalOpen && selectedWhatsApp?.id}
+			/>
+			<WhatsAppModal
+				open={whatsAppModalOpen}
+				onClose={handleCloseWhatsAppModal}
+				whatsAppId={!qrModalOpen && selectedWhatsApp?.id}
+			/>
+			<MainHeader>
+				<Title>{i18n.t("connections.title")}</Title>
+				<MainHeaderButtonsWrapper>
+					<Can
+						role={user.profile}
+						perform="connections-page:addConnection"
+						yes={() => (
+							<Button
+								variant="contained"
+								color="primary"
+								onClick={handleOpenWhatsAppModal}
+							>
+								{i18n.t("connections.buttons.add")}
+							</Button>
+						)}
+					/>
+				</MainHeaderButtonsWrapper>
+			</MainHeader>
+			<Paper className={classes.mainPaper} variant="outlined">
+				<Table size="small">
+					<TableHead>
+						<TableRow>
+							<TableCell align="center">
+								{i18n.t("connections.table.name")}
+							</TableCell>
+							<TableCell align="center">
+								{i18n.t("connections.table.status")}
+							</TableCell>
+							<Can
+								role={user.profile}
+								perform="connections-page:actionButtons"
+								yes={() => (
+									<TableCell align="center">
+										{i18n.t("connections.table.session")}
+									</TableCell>
+								)}
+							/>
+							<TableCell align="center">
+								{i18n.t("connections.table.lastUpdate")}
+							</TableCell>
+							<TableCell align="center">
+								{i18n.t("connections.table.default")}
+							</TableCell>
+							<Can
+								role={user.profile}
+								perform="connections-page:editOrDeleteConnection"
+								yes={() => (
+									<TableCell align="center">
+										{i18n.t("connections.table.actions")}
+									</TableCell>
+								)}
+							/>
+						</TableRow>
+					</TableHead>
+					<TableBody>
+						{loading ? (
+							<TableRowSkeleton />
+						) : (
+							<>
+								{whatsApps?.length > 0 &&
+									whatsApps.map(whatsApp => (
+										<TableRow key={whatsApp.id}>
+											<TableCell align="center">{whatsApp.name}</TableCell>
+											<TableCell align="center">
+												{renderStatusToolTips(whatsApp)}
+											</TableCell>
+											<Can
+												role={user.profile}
+												perform="connections-page:actionButtons"
+												yes={() => (
+													<TableCell align="center">
+														{renderActionButtons(whatsApp)}
+													</TableCell>
+												)}
+											/>
+											<TableCell align="center">
+												{format(parseISO(whatsApp.updatedAt), "dd/MM/yy HH:mm")}
+											</TableCell>
+											<TableCell align="center">
+												{whatsApp.isDefault && (
+													<div className={classes.customTableCell}>
+														<CheckCircle style={{ color: green[500] }} />
+													</div>
+												)}
+											</TableCell>
+											<Can
+												role={user.profile}
+												perform="connections-page:editOrDeleteConnection"
+												yes={() => (
+													<TableCell align="center">
+														<IconButton
+															size="small"
+															onClick={() => handleEditWhatsApp(whatsApp)}
+														>
+															<Edit />
+														</IconButton>
+
+														<IconButton
+															size="small"
+															onClick={e => {
+																handleOpenConfirmationModal("delete", whatsApp.id);
+															}}
+														>
+															<DeleteOutline />
+														</IconButton>
+													</TableCell>
+												)}
+											/>
+										</TableRow>
+									))}
+							</>
+						)}
+					</TableBody>
+				</Table>
+			</Paper>
+		</MainContainer>
+	);
+};
+
+export default Connections;

+ 437 - 0
frontend/src/pages/ContactListItems/index.js

@@ -0,0 +1,437 @@
+import React, {
+  useState,
+  useEffect,
+  useReducer,
+  useContext,
+  useRef,
+} from "react";
+
+import { toast } from "react-toastify";
+import { useParams, useHistory } from "react-router-dom";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableHead from "@material-ui/core/TableHead";
+import TableRow from "@material-ui/core/TableRow";
+import Paper from "@material-ui/core/Paper";
+import Button from "@material-ui/core/Button";
+import SearchIcon from "@material-ui/icons/Search";
+import TextField from "@material-ui/core/TextField";
+import InputAdornment from "@material-ui/core/InputAdornment";
+
+import IconButton from "@material-ui/core/IconButton";
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import EditIcon from "@material-ui/icons/Edit";
+import CheckCircleIcon from "@material-ui/icons/CheckCircle";
+import BlockIcon from "@material-ui/icons/Block";
+
+import api from "../../services/api";
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+import ContactListItemModal from "../../components/ContactListItemModal";
+import ConfirmationModal from "../../components/ConfirmationModal/";
+
+import { i18n } from "../../translate/i18n";
+import MainHeader from "../../components/MainHeader";
+import Title from "../../components/Title";
+import MainContainer from "../../components/MainContainer";
+import toastError from "../../errors/toastError";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { Can } from "../../components/Can";
+import useContactLists from "../../hooks/useContactLists";
+import { Grid } from "@material-ui/core";
+
+import planilhaExemplo from "../../assets/planilha.xlsx";
+import { SocketContext } from "../../context/Socket/SocketContext";
+
+const reducer = (state, action) => {
+  if (action.type === "LOAD_CONTACTS") {
+    const contacts = action.payload;
+    const newContacts = [];
+
+    contacts.forEach((contact) => {
+      const contactIndex = state.findIndex((c) => c.id === contact.id);
+      if (contactIndex !== -1) {
+        state[contactIndex] = contact;
+      } else {
+        newContacts.push(contact);
+      }
+    });
+
+    return [...state, ...newContacts];
+  }
+
+  if (action.type === "UPDATE_CONTACTS") {
+    const contact = action.payload;
+    const contactIndex = state.findIndex((c) => c.id === contact.id);
+
+    if (contactIndex !== -1) {
+      state[contactIndex] = contact;
+      return [...state];
+    } else {
+      return [contact, ...state];
+    }
+  }
+
+  if (action.type === "DELETE_CONTACT") {
+    const contactId = action.payload;
+
+    const contactIndex = state.findIndex((c) => c.id === contactId);
+    if (contactIndex !== -1) {
+      state.splice(contactIndex, 1);
+    }
+    return [...state];
+  }
+
+  if (action.type === "RESET") {
+    return [];
+  }
+};
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+}));
+
+const ContactListItems = () => {
+  const classes = useStyles();
+
+  const { user } = useContext(AuthContext);
+  const { contactListId } = useParams();
+  const history = useHistory();
+
+  const [loading, setLoading] = useState(false);
+  const [pageNumber, setPageNumber] = useState(1);
+  const [searchParam, setSearchParam] = useState("");
+  const [contacts, dispatch] = useReducer(reducer, []);
+  const [selectedContactId, setSelectedContactId] = useState(null);
+  const [contactListItemModalOpen, setContactListItemModalOpen] =
+    useState(false);
+  const [deletingContact, setDeletingContact] = useState(null);
+  const [confirmOpen, setConfirmOpen] = useState(false);
+  const [hasMore, setHasMore] = useState(false);
+  const [contactList, setContactList] = useState({});
+  const fileUploadRef = useRef(null);
+
+  const { findById: findContactList } = useContactLists();
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    findContactList(contactListId).then((data) => {
+      setContactList(data);
+    });
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [contactListId]);
+
+  useEffect(() => {
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+  }, [searchParam]);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      const fetchContacts = async () => {
+        try {
+          const { data } = await api.get(`contact-list-items`, {
+            params: { searchParam, pageNumber, contactListId },
+          });
+          dispatch({ type: "LOAD_CONTACTS", payload: data.contacts });
+          setHasMore(data.hasMore);
+          setLoading(false);
+        } catch (err) {
+          toastError(err);
+        }
+      };
+      fetchContacts();
+    }, 500);
+    return () => clearTimeout(delayDebounceFn);
+  }, [searchParam, pageNumber, contactListId]);
+
+  useEffect(() => {
+    const companyId = localStorage.getItem("companyId");
+    const socket = socketManager.getSocket(companyId);
+
+    socket.on(`company-${companyId}-ContactListItem`, (data) => {
+      if (data.action === "update" || data.action === "create") {
+        dispatch({ type: "UPDATE_CONTACTS", payload: data.record });
+      }
+
+      if (data.action === "delete") {
+        dispatch({ type: "DELETE_CONTACT", payload: +data.id });
+      }
+
+      if (data.action === "reload") {
+        dispatch({ type: "LOAD_CONTACTS", payload: data.records });
+      }
+    });
+
+    socket.on(
+      `company-${companyId}-ContactListItem-${contactListId}`,
+      (data) => {
+        if (data.action === "reload") {
+          dispatch({ type: "LOAD_CONTACTS", payload: data.records });
+        }
+      }
+    );
+
+    return () => {
+      socket.disconnect();
+    };
+  }, [contactListId, socketManager]);
+
+  const handleSearch = (event) => {
+    setSearchParam(event.target.value.toLowerCase());
+  };
+
+  const handleOpenContactListItemModal = () => {
+    setSelectedContactId(null);
+    setContactListItemModalOpen(true);
+  };
+
+  const handleCloseContactListItemModal = () => {
+    setSelectedContactId(null);
+    setContactListItemModalOpen(false);
+  };
+
+  const hadleEditContact = (contactId) => {
+    setSelectedContactId(contactId);
+    setContactListItemModalOpen(true);
+  };
+
+  const handleDeleteContact = async (contactId) => {
+    try {
+      await api.delete(`/contact-list-items/${contactId}`);
+      toast.success(i18n.t("contacts.toasts.deleted"));
+    } catch (err) {
+      toastError(err);
+    }
+    setDeletingContact(null);
+    setSearchParam("");
+    setPageNumber(1);
+  };
+
+  const handleImportContacts = async () => {
+    try {
+      const formData = new FormData();
+      formData.append("file", fileUploadRef.current.files[0]);
+      await api.request({
+        url: `contact-lists/${contactListId}/upload`,
+        method: "POST",
+        data: formData,
+      });
+    } 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 goToContactLists = () => {
+    history.push("/contact-lists");
+  };
+
+  return (
+    <MainContainer className={classes.mainContainer}>
+      <ContactListItemModal
+        open={contactListItemModalOpen}
+        onClose={handleCloseContactListItemModal}
+        aria-labelledby="form-dialog-title"
+        contactId={selectedContactId}
+      ></ContactListItemModal>
+      <ConfirmationModal
+        title={
+          deletingContact
+            ? `${i18n.t("contactListItems.confirmationModal.deleteTitle")} ${
+                deletingContact.name
+              }?`
+            : `${i18n.t("contactListItems.confirmationModal.importTitlte")}`
+        }
+        open={confirmOpen}
+        onClose={setConfirmOpen}
+        onConfirm={() =>
+          deletingContact
+            ? handleDeleteContact(deletingContact.id)
+            : handleImportContacts()
+        }
+      >
+        {deletingContact ? (
+          `${i18n.t("contactListItems.confirmationModal.deleteMessage")}`
+        ) : (
+          <>
+            {i18n.t("contactListItems.confirmationModal.importMessage")}
+            <a href={planilhaExemplo} download="planilha.xlsx">
+              {i18n.t("contactListItems.download")}
+            </a>
+          </>
+        )}
+      </ConfirmationModal>
+      <MainHeader>
+        <Grid style={{ width: "99.6%" }} container>
+          <Grid xs={12} sm={5} item>
+            <Title>{contactList.name}</Title>
+          </Grid>
+          <Grid xs={12} sm={7} item>
+            <Grid spacing={2} container>
+              <Grid xs={12} sm={6} item>
+                <TextField
+                  fullWidth
+                  placeholder={i18n.t("contactListItems.searchPlaceholder")}
+                  type="search"
+                  value={searchParam}
+                  onChange={handleSearch}
+                  InputProps={{
+                    startAdornment: (
+                      <InputAdornment position="start">
+                        <SearchIcon style={{ color: "gray" }} />
+                      </InputAdornment>
+                    ),
+                  }}
+                />
+              </Grid>
+              <Grid xs={4} sm={2} item>
+                <Button
+                  fullWidth
+                  variant="contained"
+                  color="primary"
+                  onClick={goToContactLists}
+                >
+                  {i18n.t("contactListItems.buttons.lists")}
+                </Button>
+              </Grid>
+              <Grid xs={4} sm={2} item>
+                <Button
+                  fullWidth
+                  variant="contained"
+                  color="primary"
+                  onClick={() => {
+                    fileUploadRef.current.value = null;
+                    fileUploadRef.current.click();
+                  }}
+                >
+                  {i18n.t("contactListItems.buttons.import")}
+                </Button>
+              </Grid>
+              <Grid xs={4} sm={2} item>
+                <Button
+                  fullWidth
+                  variant="contained"
+                  color="primary"
+                  onClick={handleOpenContactListItemModal}
+                >
+                  {i18n.t("contactListItems.buttons.add")}
+                </Button>
+              </Grid>
+            </Grid>
+          </Grid>
+        </Grid>
+      </MainHeader>
+      <Paper
+        className={classes.mainPaper}
+        variant="outlined"
+        onScroll={handleScroll}
+      >
+        <>
+          <input
+            style={{ display: "none" }}
+            id="upload"
+            name="file"
+            type="file"
+            accept=".xls,.xlsx"
+            onChange={() => {
+              setConfirmOpen(true);
+            }}
+            ref={fileUploadRef}
+          />
+        </>
+        <Table size="small">
+          <TableHead>
+            <TableRow>
+              <TableCell align="center" style={{ width: "0%" }}>
+                #
+              </TableCell>
+              <TableCell>{i18n.t("contactListItems.table.name")}</TableCell>
+              <TableCell align="center">
+                {i18n.t("contactListItems.table.number")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("contactListItems.table.email")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("contactListItems.table.actions")}
+              </TableCell>
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            <>
+              {contacts.map((contact) => (
+                <TableRow key={contact.id}>
+                  <TableCell align="center" style={{ width: "0%" }}>
+                    <IconButton>
+                      {contact.isWhatsappValid ? (
+                        <CheckCircleIcon
+                          titleAccess="Whatsapp Válido"
+                          htmlColor="green"
+                        />
+                      ) : (
+                        <BlockIcon
+                          titleAccess="Whatsapp Inválido"
+                          htmlColor="grey"
+                        />
+                      )}
+                    </IconButton>
+                  </TableCell>
+                  <TableCell>{contact.name}</TableCell>
+                  <TableCell align="center">{contact.number}</TableCell>
+                  <TableCell align="center">{contact.email}</TableCell>
+                  <TableCell align="center">
+                    <IconButton
+                      size="small"
+                      onClick={() => hadleEditContact(contact.id)}
+                    >
+                      <EditIcon />
+                    </IconButton>
+                    <Can
+                      role={user.profile}
+                      perform="contacts-page:deleteContact"
+                      yes={() => (
+                        <IconButton
+                          size="small"
+                          onClick={() => {
+                            setConfirmOpen(true);
+                            setDeletingContact(contact);
+                          }}
+                        >
+                          <DeleteOutlineIcon />
+                        </IconButton>
+                      )}
+                    />
+                  </TableCell>
+                </TableRow>
+              ))}
+              {loading && <TableRowSkeleton columns={4} />}
+            </>
+          </TableBody>
+        </Table>
+      </Paper>
+    </MainContainer>
+  );
+};
+
+export default ContactListItems;

+ 326 - 0
frontend/src/pages/ContactLists/index.js

@@ -0,0 +1,326 @@
+import React, { useState, useEffect, useReducer, useContext } from "react";
+import { toast } from "react-toastify";
+
+import { useHistory } from "react-router-dom";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+import Button from "@material-ui/core/Button";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableHead from "@material-ui/core/TableHead";
+import TableRow from "@material-ui/core/TableRow";
+import IconButton from "@material-ui/core/IconButton";
+import SearchIcon from "@material-ui/icons/Search";
+import TextField from "@material-ui/core/TextField";
+import InputAdornment from "@material-ui/core/InputAdornment";
+
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import EditIcon from "@material-ui/icons/Edit";
+import PeopleIcon from "@material-ui/icons/People";
+import DownloadIcon from "@material-ui/icons/GetApp";
+
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import Title from "../../components/Title";
+
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+import ContactListDialog from "../../components/ContactListDialog";
+import ConfirmationModal from "../../components/ConfirmationModal";
+import toastError from "../../errors/toastError";
+import { Grid } from "@material-ui/core";
+
+import planilhaExemplo from "../../assets/planilha.xlsx";
+import { SocketContext } from "../../context/Socket/SocketContext";
+
+const reducer = (state, action) => {
+  if (action.type === "LOAD_CONTACTLISTS") {
+    const contactLists = action.payload;
+    const newContactLists = [];
+
+    contactLists.forEach((contactList) => {
+      const contactListIndex = state.findIndex((u) => u.id === contactList.id);
+      if (contactListIndex !== -1) {
+        state[contactListIndex] = contactList;
+      } else {
+        newContactLists.push(contactList);
+      }
+    });
+
+    return [...state, ...newContactLists];
+  }
+
+  if (action.type === "UPDATE_CONTACTLIST") {
+    const contactList = action.payload;
+    const contactListIndex = state.findIndex((u) => u.id === contactList.id);
+
+    if (contactListIndex !== -1) {
+      state[contactListIndex] = contactList;
+      return [...state];
+    } else {
+      return [contactList, ...state];
+    }
+  }
+
+  if (action.type === "DELETE_CONTACTLIST") {
+    const contactListId = action.payload;
+
+    const contactListIndex = state.findIndex((u) => u.id === contactListId);
+    if (contactListIndex !== -1) {
+      state.splice(contactListIndex, 1);
+    }
+    return [...state];
+  }
+
+  if (action.type === "RESET") {
+    return [];
+  }
+};
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+}));
+
+const ContactLists = () => {
+  const classes = useStyles();
+  const history = useHistory();
+
+  const [loading, setLoading] = useState(false);
+  const [pageNumber, setPageNumber] = useState(1);
+  const [hasMore, setHasMore] = useState(false);
+  const [selectedContactList, setSelectedContactList] = useState(null);
+  const [deletingContactList, setDeletingContactList] = useState(null);
+  const [contactListModalOpen, setContactListModalOpen] = useState(false);
+  const [confirmModalOpen, setConfirmModalOpen] = useState(false);
+  const [searchParam, setSearchParam] = useState("");
+  const [contactLists, dispatch] = useReducer(reducer, []);
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+  }, [searchParam]);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      const fetchContactLists = async () => {
+        try {
+          const { data } = await api.get("/contact-lists/", {
+            params: { searchParam, pageNumber },
+          });
+          dispatch({ type: "LOAD_CONTACTLISTS", payload: data.records });
+          setHasMore(data.hasMore);
+          setLoading(false);
+        } catch (err) {
+          toastError(err);
+        }
+      };
+      fetchContactLists();
+    }, 500);
+    return () => clearTimeout(delayDebounceFn);
+  }, [searchParam, pageNumber]);
+
+  useEffect(() => {
+    const companyId = localStorage.getItem("companyId");
+    const socket = socketManager.getSocket(companyId);
+
+    socket.on(`company-${companyId}-ContactList`, (data) => {
+      if (data.action === "update" || data.action === "create") {
+        dispatch({ type: "UPDATE_CONTACTLIST", payload: data.record });
+      }
+
+      if (data.action === "delete") {
+        dispatch({ type: "DELETE_CONTACTLIST", payload: +data.id });
+      }
+    });
+
+    return () => {
+      socket.disconnect();
+    };
+  }, [socketManager]);
+
+  const handleOpenContactListModal = () => {
+    setSelectedContactList(null);
+    setContactListModalOpen(true);
+  };
+
+  const handleCloseContactListModal = () => {
+    setSelectedContactList(null);
+    setContactListModalOpen(false);
+  };
+
+  const handleSearch = (event) => {
+    setSearchParam(event.target.value.toLowerCase());
+  };
+
+  const handleEditContactList = (contactList) => {
+    setSelectedContactList(contactList);
+    setContactListModalOpen(true);
+  };
+
+  const handleDeleteContactList = async (contactListId) => {
+    try {
+      await api.delete(`/contact-lists/${contactListId}`);
+      toast.success(i18n.t("contactLists.toasts.deleted"));
+    } catch (err) {
+      toastError(err);
+    }
+    setDeletingContactList(null);
+    setSearchParam("");
+    setPageNumber(1);
+  };
+
+  const loadMore = () => {
+    setPageNumber((prevState) => prevState + 1);
+  };
+
+  const handleScroll = (e) => {
+    if (!hasMore || loading) return;
+    const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
+    if (scrollHeight - (scrollTop + 100) < clientHeight) {
+      loadMore();
+    }
+  };
+
+  const goToContacts = (id) => {
+    history.push(`/contact-lists/${id}/contacts`);
+  };
+
+  return (
+    <MainContainer>
+      <ConfirmationModal
+        title={
+          deletingContactList &&
+          `${i18n.t("contactLists.confirmationModal.deleteTitle")} ${
+            deletingContactList.name
+          }?`
+        }
+        open={confirmModalOpen}
+        onClose={setConfirmModalOpen}
+        onConfirm={() => handleDeleteContactList(deletingContactList.id)}
+      >
+        {i18n.t("contactLists.confirmationModal.deleteMessage")}
+      </ConfirmationModal>
+      <ContactListDialog
+        open={contactListModalOpen}
+        onClose={handleCloseContactListModal}
+        aria-labelledby="form-dialog-title"
+        contactListId={selectedContactList && selectedContactList.id}
+      />
+      <MainHeader>
+        <Grid style={{ width: "99.6%" }} container>
+          <Grid xs={12} sm={8} item>
+            <Title>{i18n.t("contactLists.title")}</Title>
+          </Grid>
+          <Grid xs={12} sm={4} item>
+            <Grid spacing={2} container>
+              <Grid xs={7} sm={6} item>
+                <TextField
+                  fullWidth
+                  placeholder={i18n.t("contacts.searchPlaceholder")}
+                  type="search"
+                  value={searchParam}
+                  onChange={handleSearch}
+                  InputProps={{
+                    startAdornment: (
+                      <InputAdornment position="start">
+                        <SearchIcon style={{ color: "gray" }} />
+                      </InputAdornment>
+                    ),
+                  }}
+                />
+              </Grid>
+              <Grid xs={5} sm={6} item>
+                <Button
+                  fullWidth
+                  variant="contained"
+                  color="primary"
+                  onClick={handleOpenContactListModal}
+                >
+                  {i18n.t("contactLists.buttons.add")}
+                </Button>
+              </Grid>
+            </Grid>
+          </Grid>
+        </Grid>
+      </MainHeader>
+      <Paper
+        className={classes.mainPaper}
+        variant="outlined"
+        onScroll={handleScroll}
+      >
+        <Table size="small">
+          <TableHead>
+            <TableRow>
+              <TableCell align="center">
+                {i18n.t("contactLists.table.name")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("contactLists.table.contacts")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("contactLists.table.actions")}
+              </TableCell>
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            <>
+              {contactLists.map((contactList) => (
+                <TableRow key={contactList.id}>
+                  <TableCell align="center">{contactList.name}</TableCell>
+                  <TableCell align="center">
+                    {contactList.contactsCount || 0}
+                  </TableCell>
+                  <TableCell align="center">
+                    <a href={planilhaExemplo} download="planilha.xlsx">
+                      <IconButton size="small" title="Baixar Planilha Exemplo">
+                        <DownloadIcon />
+                      </IconButton>
+                    </a>
+
+                    <IconButton
+                      size="small"
+                      onClick={() => goToContacts(contactList.id)}
+                    >
+                      <PeopleIcon />
+                    </IconButton>
+
+                    <IconButton
+                      size="small"
+                      onClick={() => handleEditContactList(contactList)}
+                    >
+                      <EditIcon />
+                    </IconButton>
+
+                    <IconButton
+                      size="small"
+                      onClick={(e) => {
+                        setConfirmModalOpen(true);
+                        setDeletingContactList(contactList);
+                      }}
+                    >
+                      <DeleteOutlineIcon />
+                    </IconButton>
+                  </TableCell>
+                </TableRow>
+              ))}
+              {loading && <TableRowSkeleton columns={3} />}
+            </>
+          </TableBody>
+        </Table>
+      </Paper>
+    </MainContainer>
+  );
+};
+
+export default ContactLists;

+ 401 - 0
frontend/src/pages/Contacts/index.js

@@ -0,0 +1,401 @@
+import React, { useState, useEffect, useReducer, useContext } from "react";
+
+import { toast } from "react-toastify";
+import { useHistory } from "react-router-dom";
+import { Tooltip } from "@material-ui/core";
+import { makeStyles } from "@material-ui/core/styles";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableHead from "@material-ui/core/TableHead";
+import TableRow from "@material-ui/core/TableRow";
+import Paper from "@material-ui/core/Paper";
+import Button from "@material-ui/core/Button";
+import Avatar from "@material-ui/core/Avatar";
+import WhatsAppIcon from "@material-ui/icons/WhatsApp";
+import SearchIcon from "@material-ui/icons/Search";
+import TextField from "@material-ui/core/TextField";
+import InputAdornment from "@material-ui/core/InputAdornment";
+
+import IconButton from "@material-ui/core/IconButton";
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import EditIcon from "@material-ui/icons/Edit";
+import api from "../../services/api";
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+import ContactModal from "../../components/ContactModal";
+import ConfirmationModal from "../../components/ConfirmationModal/";
+
+import { i18n } from "../../translate/i18n";
+import MainHeader from "../../components/MainHeader";
+import Title from "../../components/Title";
+import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
+import MainContainer from "../../components/MainContainer";
+import toastError from "../../errors/toastError";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { Can } from "../../components/Can";
+import NewTicketModal from "../../components/NewTicketModal";
+import { SocketContext } from "../../context/Socket/SocketContext";
+
+import {CSVLink} from "react-csv";
+import ImportContactsModal from "../../components/ImportContactsModal";
+
+const reducer = (state, action) => {
+  if (action.type === "LOAD_CONTACTS") {
+    const contacts = action.payload;
+    const newContacts = [];
+
+    contacts.forEach((contact) => {
+      const contactIndex = state.findIndex((c) => c.id === contact.id);
+      if (contactIndex !== -1) {
+        state[contactIndex] = contact;
+      } else {
+        newContacts.push(contact);
+      }
+    });
+
+    return [...state, ...newContacts];
+  }
+
+  if (action.type === "UPDATE_CONTACTS") {
+    const contact = action.payload;
+    const contactIndex = state.findIndex((c) => c.id === contact.id);
+
+    if (contactIndex !== -1) {
+      state[contactIndex] = contact;
+      return [...state];
+    } else {
+      return [contact, ...state];
+    }
+  }
+
+  if (action.type === "DELETE_CONTACT") {
+    const contactId = action.payload;
+
+    const contactIndex = state.findIndex((c) => c.id === contactId);
+    if (contactIndex !== -1) {
+      state.splice(contactIndex, 1);
+    }
+    return [...state];
+  }
+
+  if (action.type === "RESET") {
+    return [];
+  }
+};
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+}));
+
+const Contacts = () => {
+  const classes = useStyles();
+  const history = useHistory();
+
+  const { user } = useContext(AuthContext);
+
+  const [loading, setLoading] = useState(false);
+  const [pageNumber, setPageNumber] = useState(1);
+  const [searchParam, setSearchParam] = useState("");
+  const [contacts, dispatch] = useReducer(reducer, []);
+  const [selectedContactId, setSelectedContactId] = useState(null);
+  const [contactModalOpen, setContactModalOpen] = useState(false);
+  const [newTicketModalOpen, setNewTicketModalOpen] = useState(false);
+  const [contactTicket, setContactTicket] = useState({});
+  const [deletingContact, setDeletingContact] = useState(null);
+  const [confirmOpen, setConfirmOpen] = useState(false);
+  const [hasMore, setHasMore] = useState(false);
+  const [openModalImport, setOpenModalImport] = useState(false);
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+  }, [searchParam]);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      const fetchContacts = async () => {
+        try {
+          const { data } = await api.get("/contacts/", {
+            params: { searchParam, pageNumber },
+          });
+          dispatch({ type: "LOAD_CONTACTS", payload: data.contacts });
+          setHasMore(data.hasMore);
+          setLoading(false);
+        } catch (err) {
+          toastError(err);
+        }
+      };
+      fetchContacts();
+    }, 500);
+    return () => clearTimeout(delayDebounceFn);
+  }, [searchParam, pageNumber]);
+
+  useEffect(() => {
+    const companyId = localStorage.getItem("companyId");
+    const socket = socketManager.getSocket(companyId);
+
+    socket.on(`company-${companyId}-contact`, (data) => {
+      if (data.action === "update" || data.action === "create") {
+        dispatch({ type: "UPDATE_CONTACTS", payload: data.contact });
+      }
+
+      if (data.action === "delete") {
+        dispatch({ type: "DELETE_CONTACT", payload: +data.contactId });
+      }
+    });
+
+    return () => {
+      socket.disconnect();
+    };
+  }, [ socketManager]);
+
+  const handleSearch = (event) => {
+    setSearchParam(event.target.value.toLowerCase());
+  };
+
+  const handleOpenContactModal = () => {
+    setSelectedContactId(null);
+    setContactModalOpen(true);
+  };
+
+  const handleCloseContactModal = () => {
+    setSelectedContactId(null);
+    setContactModalOpen(false);
+  };
+
+  // const handleSaveTicket = async contactId => {
+  // 	if (!contactId) return;
+  // 	setLoading(true);
+  // 	try {
+  // 		const { data: ticket } = await api.post("/tickets", {
+  // 			contactId: contactId,
+  // 			userId: user?.id,
+  // 			status: "open",
+  // 		});
+  // 		history.push(`/tickets/${ticket.id}`);
+  // 	} catch (err) {
+  // 		toastError(err);
+  // 	}
+  // 	setLoading(false);
+  // };
+
+  const handleCloseOrOpenTicket = (ticket) => {
+    setNewTicketModalOpen(false);
+    if (ticket !== undefined && ticket.uuid !== undefined) {
+      history.push(`/tickets/${ticket.uuid}`);
+    }
+  };
+
+  const hadleEditContact = (contactId) => {
+    setSelectedContactId(contactId);
+    setContactModalOpen(true);
+  };
+
+  const handleDeleteContact = async (contactId) => {
+    try {
+      await api.delete(`/contacts/${contactId}`);
+      toast.success(i18n.t("contacts.toasts.deleted"));
+    } catch (err) {
+      toastError(err);
+    }
+    setDeletingContact(null);
+    setSearchParam("");
+    setPageNumber(1);
+  };
+
+  const handleimportContact = async () => {
+    try {
+      await api.post("/contacts/import");
+      history.go(0);
+    } catch (err) {
+      toastError(err);
+    }
+  };
+
+  const handleOpenImportModal = (  ) => {
+    setOpenModalImport(true);
+  }
+
+  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 handleCloseModalImport = (  ) => {
+    setOpenModalImport(false);
+  }
+
+  return (
+    <MainContainer className={classes.mainContainer}>
+      <ImportContactsModal
+        open={openModalImport}
+        onClose={handleCloseModalImport}
+      />
+      <NewTicketModal
+        modalOpen={newTicketModalOpen}
+        initialContact={contactTicket}
+        onClose={(ticket) => {
+          handleCloseOrOpenTicket(ticket);
+        }}
+      />
+      <ContactModal
+        open={contactModalOpen}
+        onClose={handleCloseContactModal}
+        aria-labelledby="form-dialog-title"
+        contactId={selectedContactId}
+      ></ContactModal>
+      <ConfirmationModal
+        title={
+          deletingContact
+            ? `${i18n.t("contacts.confirmationModal.deleteTitle")} ${
+                deletingContact.name
+              }?`
+            : `${i18n.t("contacts.confirmationModal.importTitlte")}`
+        }
+        open={confirmOpen}
+        onClose={setConfirmOpen}
+        onConfirm={(e) =>
+          deletingContact
+            ? handleDeleteContact(deletingContact.id)
+            : handleimportContact()
+        }
+      >
+        {deletingContact
+          ? `${i18n.t("contacts.confirmationModal.deleteMessage")}`
+          : `${i18n.t("contacts.confirmationModal.importMessage")}`}
+      </ConfirmationModal>
+      <MainHeader>
+        <Title>{i18n.t("contacts.title")}</Title>
+        <MainHeaderButtonsWrapper>
+          <TextField
+            placeholder={i18n.t("contacts.searchPlaceholder")}
+            type="search"
+            value={searchParam}
+            onChange={handleSearch}
+            InputProps={{
+              startAdornment: (
+                <InputAdornment position="start">
+                  <SearchIcon style={{ color: "gray" }} />
+                </InputAdornment>
+              ),
+            }}
+          />
+          {/*<Button
+            variant="contained"
+            color="primary"
+            onClick={(e) => setConfirmOpen(true)}
+          >
+            {i18n.t("contacts.buttons.import")}
+          </Button>*/}
+          <Button
+            variant="contained"
+            color="primary"
+            onClick={handleOpenImportModal}
+          >
+            {i18n.t("contacts.buttons.import")}
+          </Button>
+          <Button
+            variant="contained"
+            color="primary"
+            onClick={handleOpenContactModal}
+          >
+            {i18n.t("contacts.buttons.add")}
+          </Button>
+         <CSVLink style={{ textDecoration:'none'}} separator=";" filename={'contatos.csv'} data={contacts.map((contact) => ({ name: contact.name, number: contact.number, email: contact.email }))}>
+          <Button	variant="contained" color="primary"> 
+            {i18n.t("contacts.buttons.export")}
+          </Button>
+          </CSVLink>		  
+
+        </MainHeaderButtonsWrapper>
+      </MainHeader>
+      <Paper
+        className={classes.mainPaper}
+        variant="outlined"
+        onScroll={handleScroll}
+      >
+        <Table size="small">
+          <TableHead>
+            <TableRow>
+              <TableCell padding="checkbox" />
+              <TableCell>{i18n.t("contacts.table.name")}</TableCell>
+              <TableCell align="center">
+                {i18n.t("contacts.table.whatsapp")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("contacts.table.email")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("contacts.table.actions")}
+              </TableCell>
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            <>
+              {contacts.map((contact) => (
+                <TableRow key={contact.id}>
+                  <TableCell style={{ paddingRight: 0 }}>
+                    {<Avatar src={contact.profilePicUrl} />}
+                  </TableCell>
+                  <TableCell>{contact.name}</TableCell>
+                  <TableCell align="center">{contact.number}</TableCell>
+                  <TableCell align="center">{contact.email}</TableCell>
+                  <TableCell align="center">
+                    <IconButton
+                      size="small"
+                      onClick={() => {
+                        setContactTicket(contact);
+                        setNewTicketModalOpen(true);
+                      }}
+                    >
+                      <WhatsAppIcon />
+                    </IconButton>
+                    <IconButton
+                      size="small"
+                      onClick={() => hadleEditContact(contact.id)}
+                    >
+                      <EditIcon />
+                    </IconButton>
+                    <Can
+                      role={user.profile}
+                      perform="contacts-page:deleteContact"
+                      yes={() => (
+                        <IconButton
+                          size="small"
+                          onClick={(e) => {
+                            setConfirmOpen(true);
+                            setDeletingContact(contact);
+                          }}
+                        >
+                          <DeleteOutlineIcon />
+                        </IconButton>
+                      )}
+                    />
+                  </TableCell>
+                </TableRow>
+              ))}
+              {loading && <TableRowSkeleton avatar columns={3} />}
+            </>
+          </TableBody>
+        </Table>
+      </Paper>
+    </MainContainer>
+  );
+};
+
+export default Contacts;

+ 119 - 0
frontend/src/pages/Dashboard/Chart.js

@@ -0,0 +1,119 @@
+import React, { useState, useEffect } from "react";
+import { useTheme } from "@material-ui/core/styles";
+import {
+	CartesianGrid,
+	XAxis,
+	YAxis,
+	Label,
+	ResponsiveContainer,
+	LineChart,
+	Line,
+	Tooltip,
+	Legend,
+} from "recharts";
+import { startOfHour, parseISO, format } from "date-fns";
+
+import Title from "./Title";
+import useTickets from "../../hooks/useTickets";
+
+const Chart = ({ queueTicket }) => {
+	const theme = useTheme();
+
+	const { tickets, count } = useTickets({
+		queueIds: queueTicket ? `[${queueTicket}]` : "[]",
+	});
+
+	const [chartData, setChartData] = useState([
+		{ time: "00:00", amount: 0 },
+		{ time: "01:00", amount: 0 },
+		{ time: "02:00", amount: 0 },
+		{ time: "03:00", amount: 0 },
+		{ time: "04:00", amount: 0 },
+		{ time: "05:00", amount: 0 },
+		{ time: "06:00", amount: 0 },
+		{ time: "07:00", amount: 0 },
+		{ time: "08:00", amount: 0 },
+		{ time: "09:00", amount: 0 },
+		{ time: "10:00", amount: 0 },
+		{ time: "11:00", amount: 0 },
+		{ time: "12:00", amount: 0 },
+		{ time: "13:00", amount: 0 },
+		{ time: "14:00", amount: 0 },
+		{ time: "15:00", amount: 0 },
+		{ time: "16:00", amount: 0 },
+		{ time: "17:00", amount: 0 },
+		{ time: "18:00", amount: 0 },
+		{ time: "19:00", amount: 0 },
+		{ time: "20:00", amount: 0 },
+		{ time: "21:00", amount: 0 },
+		{ time: "22:00", amount: 0 },
+		{ time: "23:00", amount: 0 },
+	]);
+
+	useEffect(() => {
+		setChartData((prevState) => {
+			let aux = [...prevState];
+
+			aux.forEach((a) => {
+				tickets.forEach((ticket) => {
+					format(startOfHour(parseISO(ticket.createdAt)), "HH:mm") ===
+						a.time && a.amount++;
+				});
+			});
+
+			return aux;
+		});
+	}, [tickets]);
+
+	return (
+		<React.Fragment>
+			<Title>{`${"Atendimentos Criados: "}${count}`}</Title>
+			<ResponsiveContainer>
+				<LineChart
+					data={chartData}
+					width={730}
+					height={250}
+					margin={{
+						top: 5,
+						right: 30,
+						left: 20,
+						bottom: 5,
+					}}
+				>
+					<CartesianGrid strokeDasharray="3 3" />
+					<XAxis
+						dataKey="time"
+						stroke={theme.palette.text.secondary}
+					/>
+					<YAxis
+						type="number"
+						allowDecimals={false}
+						stroke={theme.palette.text.secondary}
+					>
+						<Tooltip />
+						<Legend />
+						<Label
+							angle={270}
+							position="left"
+							style={{
+								textAnchor: "middle",
+								fill: theme.palette.text.primary,
+							}}
+						>
+							Tickets
+						</Label>
+					</YAxis>
+					<Line
+						type="monotone"
+						dataKey="amount"
+						stroke="#8884d8"
+						strokeWidth={2}
+					// fill={theme.palette.primary.main}
+					/>
+				</LineChart>
+			</ResponsiveContainer>
+		</React.Fragment>
+	);
+};
+
+export default Chart;

+ 131 - 0
frontend/src/pages/Dashboard/ChartsDate.js

@@ -0,0 +1,131 @@
+import React, { useEffect, useState } from 'react';
+import {
+    Chart as ChartJS,
+    CategoryScale,
+    LinearScale,
+    BarElement,
+    Title,
+    Tooltip,
+    Legend,
+} from 'chart.js';
+import { Bar } from 'react-chartjs-2';
+import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
+import brLocale from 'date-fns/locale/pt-BR';
+import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers';
+import { Button, Stack, TextField } from '@mui/material';
+import Typography from "@material-ui/core/Typography";
+import api from '../../services/api';
+import { format } from 'date-fns';
+import { toast } from 'react-toastify';
+import './button.css';
+import { i18n } from '../../translate/i18n';
+
+ChartJS.register(
+    CategoryScale,
+    LinearScale,
+    BarElement,
+    Title,
+    Tooltip,
+    Legend
+);
+
+export const options = {
+    responsive: true,
+    plugins: {
+        legend: {
+            position: 'top',
+            display: false,
+        },
+        title: {
+            display: true,
+            text: i18n.t("dashboard.charts.date.label"),
+            position: 'left',
+        },
+        datalabels: {
+            display: true,
+            anchor: 'start',
+            offset: -30,
+            align: "start",
+            color: "#fff",
+            textStrokeColor: "#000",
+            textStrokeWidth: 2,
+            font: {
+                size: 20,
+                weight: "bold"
+
+            },
+        }
+    },
+};
+
+export const ChartsDate = () => {
+
+    const [initialDate, setInitialDate] = useState(new Date());
+    const [finalDate, setFinalDate] = useState(new Date());
+    const [ticketsData, setTicketsData] = useState({ data: [], count: 0 });
+
+    const companyId = localStorage.getItem("companyId");
+
+    useEffect(() => {
+        handleGetTicketsInformation();
+    }, []);
+
+    const dataCharts = {
+
+        labels: ticketsData && ticketsData?.data.length > 0 && ticketsData?.data.map((item) => (item.hasOwnProperty('horario') ? `Das ${item.horario}:00 as ${item.horario}:59` : item.data)),
+        datasets: [
+            {
+                // label: 'Dataset 1',
+                data: ticketsData?.data.length > 0 && ticketsData?.data.map((item, index) => {
+                    return item.total
+                }),
+                backgroundColor: '#2DDD7F',
+            },
+        ],
+    };
+
+    const handleGetTicketsInformation = async () => {
+        try {
+            const { data } = await api.get(`/dashboard/ticketsDay?initialDate=${format(initialDate, 'yyyy-MM-dd')}&finalDate=${format(finalDate, 'yyyy-MM-dd')}&companyId=${companyId}`);
+            setTicketsData(data);
+        } catch (error) {
+            toast.error(i18n.t("dashboard.toasts.dateChartError"));
+        }
+    }
+
+    return (
+        <>
+            <Typography component="h2" variant="h6" color="primary" gutterBottom>
+                {i18n.t("dashboard.charts.date.title")} ({ticketsData?.count})
+            </Typography>
+
+            <Stack direction={'row'} spacing={2} alignItems={'center'} sx={{ my: 2, }} >
+
+                <LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={brLocale}>
+                    <DatePicker
+                        value={initialDate}
+                        onChange={(newValue) => { setInitialDate(newValue) }}
+                        label={i18n.t("dashboard.charts.date.start")}
+                        renderInput={(params) => <TextField fullWidth {...params} sx={{ width: '20ch' }} />}
+
+                    />
+                </LocalizationProvider>
+
+                <LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={brLocale}>
+                    <DatePicker
+                        value={finalDate}
+                        onChange={(newValue) => { setFinalDate(newValue) }}
+                        label={i18n.t("dashboard.charts.date.end")}
+                        renderInput={(params) => <TextField fullWidth {...params} sx={{ width: '20ch' }} />}
+                    />
+                </LocalizationProvider>
+
+                <Button className="buttonHover" onClick={handleGetTicketsInformation} variant='contained' >
+                    {i18n.t("dashboard.charts.date.filter")}
+                </Button>
+
+            </Stack>
+            <Bar options={options} data={dataCharts} style={{ maxWidth: '100%', maxHeight: '280px', }} />
+        </>
+    );
+}

+ 144 - 0
frontend/src/pages/Dashboard/ChartsUser.js

@@ -0,0 +1,144 @@
+import React, { useEffect, useState } from 'react';
+import {
+    Chart as ChartJS,
+    CategoryScale,
+    LinearScale,
+    BarElement,
+    Title,
+    Tooltip,
+    Legend,
+} from 'chart.js';
+import { Bar } from 'react-chartjs-2';
+import ChartDataLabels from 'chartjs-plugin-datalabels';
+import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
+import brLocale from 'date-fns/locale/pt-BR';
+import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers';
+import { Button, Stack, TextField } from '@mui/material';
+import Typography from "@material-ui/core/Typography";
+import api from '../../services/api';
+import { format } from 'date-fns';
+import { toast } from 'react-toastify';
+import { makeStyles } from "@material-ui/core/styles";
+import './button.css';
+import { i18n } from '../../translate/i18n';
+
+const useStyles = makeStyles((theme) => ({
+    container: {
+        paddingTop: theme.spacing(1),
+        paddingBottom: theme.padding,
+        paddingLeft: theme.spacing(1),
+        paddingRight: theme.spacing(2),
+    }
+}));
+
+ChartJS.register(
+    CategoryScale,
+    LinearScale,
+    BarElement,
+    Title,
+    Tooltip,
+    Legend,
+    ChartDataLabels
+);
+
+export const options = {
+    responsive: true,
+    plugins: {
+        legend: {
+            position: 'top',
+            display: false,
+        },
+        title: {
+            display: true,
+            text: i18n.t("dashboard.charts.user.label"),
+            position: 'left',
+        },
+        datalabels: {
+            display: true,
+            anchor: 'start',
+            offset: -30,
+            align: "start",
+            color: "#fff",
+            textStrokeColor: "#000",
+            textStrokeWidth: 2,
+            font: {
+                size: 20,
+                weight: "bold"
+
+            },
+        }
+    },
+};
+
+export const ChatsUser = () => {
+    // const classes = useStyles();
+    const [initialDate, setInitialDate] = useState(new Date());
+    const [finalDate, setFinalDate] = useState(new Date());
+    const [ticketsData, setTicketsData] = useState({ data: [] });
+
+    const companyId = localStorage.getItem("companyId");
+
+    useEffect(() => {
+        handleGetTicketsInformation();
+    }, []);
+
+    const dataCharts = {
+
+        labels: ticketsData && ticketsData?.data.length > 0 && ticketsData?.data.map((item) => item.nome),
+        datasets: [
+            {
+                data: ticketsData?.data.length > 0 && ticketsData?.data.map((item, index) => {
+                    return item.quantidade
+                }),
+                backgroundColor: '#2DDD7F',
+            },
+
+        ],
+    };
+
+    const handleGetTicketsInformation = async () => {
+        try {
+
+            const { data } = await api.get(`/dashboard/ticketsUsers?initialDate=${format(initialDate, 'yyyy-MM-dd')}&finalDate=${format(finalDate, 'yyyy-MM-dd')}&companyId=${companyId}`);
+            setTicketsData(data);
+        } catch (error) {
+            toast.error(i18n.t("dashboard.toasts.userChartError"));
+        }
+    }
+
+    return (
+        <>
+            <Typography component="h2" variant="h6" color="primary" gutterBottom>
+                {i18n.t("dashboard.charts.user.title")}
+            </Typography>
+
+            <Stack direction={'row'} spacing={2} alignItems={'center'} sx={{ my: 2, }} >
+
+                <LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={brLocale}>
+                    <DatePicker
+                        value={initialDate}
+                        onChange={(newValue) => { setInitialDate(newValue) }}
+                        label={i18n.t("dashboard.charts.user.start")}
+                        renderInput={(params) => <TextField fullWidth {...params} sx={{ width: '20ch' }} />}
+
+                    />
+                </LocalizationProvider>
+
+                <LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={brLocale}>
+                    <DatePicker
+                        value={finalDate}
+                        onChange={(newValue) => { setFinalDate(newValue) }}
+                        label={i18n.t("dashboard.charts.user.end")}
+                        renderInput={(params) => <TextField fullWidth {...params} sx={{ width: '20ch' }} />}
+                    />
+                </LocalizationProvider>
+
+                <Button className="buttonHover" onClick={handleGetTicketsInformation} variant='contained'>
+                    {i18n.t("dashboard.charts.user.filter")}
+                </Button>
+
+            </Stack>
+            <Bar options={options} data={dataCharts} style={{ maxWidth: '100%', maxHeight: '280px', }} />
+        </>
+    );
+}

+ 110 - 0
frontend/src/pages/Dashboard/Filters.js

@@ -0,0 +1,110 @@
+import React from "react"; // { useContext }
+// import { AuthContext } from "../../context/Auth/AuthContext";
+
+import {
+    Button,
+    // FormControl,
+    Grid,
+    // InputLabel,
+    // MenuItem,
+    Paper,
+    // Select,
+    TextField,
+} from "@material-ui/core";
+
+import Title from "./Title";
+
+const Filters = ({
+    classes,
+    setDateStartTicket,
+    setDateEndTicket,
+    dateStartTicket,
+    dateEndTicket,
+    setQueueTicket,
+    queueTicket,
+}) => {
+    // const { user } = useContext(AuthContext);
+
+    const [
+        queues,
+        // setQueues
+    ] = React.useState(queueTicket);
+    const [dateStart, setDateStart] = React.useState(dateStartTicket);
+    const [dateEnd, setDateEnd] = React.useState(dateEndTicket);
+
+    return (
+        <Grid item xs={12}>
+            <Paper className={classes.customFixedHeightPaperLg} elevation={6}>
+                <Title>Filtros</Title>
+                <Grid container spacing={3}>
+                    {/* <Grid item xs={12} sm={6} md={3}>
+                        <FormControl fullWidth>
+                            <InputLabel id="queue-label">
+                                Departamentos
+                            </InputLabel>
+                            <Select
+                                labelId="queue-label"
+                                id="queue-select"
+                                defaultValue={queueTicket}
+                                label="Departamentos"
+                                onChange={(e) => setQueues(e.target.value)}
+                            >
+                                <MenuItem value={false}>
+                                    Todos os Departamentos
+                                </MenuItem>
+                                {user.queues.map((queue) => (
+                                    <MenuItem key={queue.id} value={queue.id}>
+                                        {queue.name}
+                                    </MenuItem>
+                                ))}
+                            </Select>
+                        </FormControl>
+                    </Grid> */}
+
+                    <Grid item xs={12} sm={6} md={5}>
+                        <TextField
+                            fullWidth
+                            name="dateStart"
+                            label="De"
+                            InputLabelProps={{
+                                shrink: true,
+                            }}
+                            type="date"
+                            defaultValue={dateStart}
+                            onChange={(e) => setDateStart(e.target.value)}
+                        />
+                    </Grid>
+                    <Grid item xs={12} sm={6} md={5}>
+                        <TextField
+                            fullWidth
+                            name="dateEnd"
+                            label="Até"
+                            InputLabelProps={{
+                                shrink: true,
+                            }}
+                            type="date"
+                            defaultValue={dateEnd}
+                            onChange={(e) => setDateEnd(e.target.value)}
+                        />
+                    </Grid>
+                    <Grid item xs={12} sm={6} md={2}>
+                        <Button
+                            fullWidth
+                            variant="contained"
+                            color="primary"
+                            onClick={() => {
+                                setQueueTicket(queues);
+                                setDateStartTicket(dateStart);
+                                setDateEndTicket(dateEnd);
+                            }}
+                        >
+                            Filtrar
+                        </Button>
+                    </Grid>
+                </Grid>
+            </Paper>
+        </Grid>
+    );
+};
+
+export default Filters;

+ 12 - 0
frontend/src/pages/Dashboard/Title.js

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

+ 9 - 0
frontend/src/pages/Dashboard/button.css

@@ -0,0 +1,9 @@
+/* cor quando passa o mouse por cima */
+.MuiButtonBase-root.buttonHover:hover {
+    background: rgb(36, 36, 36)
+  }
+  
+  /* cor enquanto não passa o mouse */
+  .MuiButtonBase-root.buttonHover {
+    background: rgb(71, 71, 71)
+  }

+ 694 - 0
frontend/src/pages/Dashboard/index.js

@@ -0,0 +1,694 @@
+import React, { useContext, useState, useEffect } from "react";
+
+import Paper from "@material-ui/core/Paper";
+import Container from "@material-ui/core/Container";
+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 TextField from "@material-ui/core/TextField";
+import FormHelperText from "@material-ui/core/FormHelperText";
+import Typography from "@material-ui/core/Typography";
+
+import CallIcon from "@material-ui/icons/Call";
+import GroupAddIcon from "@material-ui/icons/GroupAdd";
+import HourglassEmptyIcon from "@material-ui/icons/HourglassEmpty";
+import CheckCircleIcon from "@material-ui/icons/CheckCircle";
+import AccessAlarmIcon from '@material-ui/icons/AccessAlarm';
+import TimerIcon from '@material-ui/icons/Timer';
+
+import { makeStyles } from "@material-ui/core/styles";
+import { grey, blue } from "@material-ui/core/colors";
+import { toast } from "react-toastify";
+
+import ButtonWithSpinner from "../../components/ButtonWithSpinner";
+
+import TableAttendantsStatus from "../../components/Dashboard/TableAttendantsStatus";
+import { isArray } from "lodash";
+
+import useDashboard from "../../hooks/useDashboard";
+import useContacts from "../../hooks/useContacts";
+import { ChatsUser } from "./ChartsUser"
+
+import { isEmpty } from "lodash";
+import moment from "moment";
+import { ChartsDate } from "./ChartsDate";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles((theme) => ({
+  container: {
+    paddingTop: theme.spacing(1),
+    paddingBottom: theme.padding,
+    paddingLeft: theme.spacing(1),
+    paddingRight: theme.spacing(2),
+  },
+  fixedHeightPaper: {
+    padding: theme.spacing(2),
+    display: "flex",
+    flexDirection: "column",
+    height: 240,
+    overflowY: "auto",
+    ...theme.scrollbarStyles,
+  },
+  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",
+  },
+  iframeDashboard: {
+    width: "100%",
+    height: "calc(100vh - 64px)",
+    border: "none",
+  },
+  container: {
+    paddingTop: theme.spacing(4),
+    paddingBottom: theme.spacing(4),
+  },
+  fixedHeightPaper: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+    height: 240,
+  },
+  customFixedHeightPaper: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+    height: 120,
+  },
+  customFixedHeightPaperLg: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+    height: "100%",
+  },
+  card1: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+    height: "100%",
+    //backgroundColor: "palette",
+    //backgroundColor: theme.palette.primary.main,
+    backgroundColor: theme.palette.type === 'dark' ? theme.palette.boxticket.main : theme.palette.primary.main,
+    color: "#eee",
+  },
+  card2: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+    height: "100%",
+    //backgroundColor: "palette",
+    //backgroundColor: theme.palette.primary.main,
+    backgroundColor: theme.palette.type === 'dark' ? theme.palette.boxticket.main : theme.palette.primary.main,
+    color: "#eee",
+  },
+  card3: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+    height: "100%",
+  //backgroundColor: theme.palette.primary.main,
+    backgroundColor: theme.palette.type === 'dark' ? theme.palette.boxticket.main : theme.palette.primary.main,
+    color: "#eee",
+  },
+  card4: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+    height: "100%",
+    //backgroundColor: theme.palette.primary.main,
+    backgroundColor: theme.palette.type === 'dark' ? theme.palette.boxticket.main : theme.palette.primary.main,
+    color: "#eee",
+  },
+  card5: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+    height: "100%",
+    //backgroundColor: theme.palette.primary.main,
+    backgroundColor: theme.palette.type === 'dark' ? theme.palette.boxticket.main : theme.palette.primary.main,
+    color: "#eee",
+  },
+  card6: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+    height: "100%",
+    //backgroundColor: theme.palette.primary.main,
+    backgroundColor: theme.palette.type === 'dark' ? theme.palette.boxticket.main : theme.palette.primary.main,
+    color: "#eee",
+  },
+  card7: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+    height: "100%",
+    //backgroundColor: theme.palette.primary.main,
+    backgroundColor: theme.palette.type === 'dark' ? theme.palette.boxticket.main : theme.palette.primary.main,
+    color: "#eee",
+  },
+  card8: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+    height: "100%",
+    //backgroundColor: theme.palette.primary.main,
+    backgroundColor: theme.palette.type === 'dark' ? theme.palette.boxticket.main : theme.palette.primary.main,
+    color: "#eee",
+  },
+  card9: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+    height: "100%",
+    //backgroundColor: theme.palette.primary.main,
+    backgroundColor: theme.palette.type === 'dark' ? theme.palette.boxticket.main : theme.palette.primary.main,
+    color: "#eee",
+  },
+  fixedHeightPaper2: {
+    padding: theme.spacing(2),
+    display: "flex",
+    overflow: "auto",
+    flexDirection: "column",
+  },
+}));
+
+const Dashboard = () => {
+  const classes = useStyles();
+  const [counters, setCounters] = useState({});
+  const [attendants, setAttendants] = useState([]);
+  const [period, setPeriod] = useState(0);
+  const [filterType, setFilterType] = useState(1);
+  const [dateFrom, setDateFrom] = useState(moment("1", "D").format("YYYY-MM-DD"));
+  const [dateTo, setDateTo] = useState(moment().format("YYYY-MM-DD"));
+  const [loading, setLoading] = useState(false);
+  const { find } = useDashboard();
+
+  useEffect(() => {
+    async function firstLoad() {
+      await fetchData();
+    }
+    setTimeout(() => {
+      firstLoad();
+    }, 1000);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+  
+    async function handleChangePeriod(value) {
+    setPeriod(value);
+  }
+
+  async function handleChangeFilterType(value) {
+    setFilterType(value);
+    if (value === 1) {
+      setPeriod(0);
+    } else {
+      setDateFrom("");
+      setDateTo("");
+    }
+  }
+
+  async function fetchData() {
+    setLoading(true);
+
+    let params = {};
+
+    if (period > 0) {
+      params = {
+        days: period,
+      };
+    }
+
+    if (!isEmpty(dateFrom) && moment(dateFrom).isValid()) {
+      params = {
+        ...params,
+        date_from: moment(dateFrom).format("YYYY-MM-DD"),
+      };
+    }
+
+    if (!isEmpty(dateTo) && moment(dateTo).isValid()) {
+      params = {
+        ...params,
+        date_to: moment(dateTo).format("YYYY-MM-DD"),
+      };
+    }
+
+    if (Object.keys(params).length === 0) {
+      toast.error(i18n.t("dashboard.toasts.selectFilterError"));
+      setLoading(false);
+      return;
+    }
+
+    const data = await find(params);
+
+    setCounters(data.counters);
+    if (isArray(data.attendants)) {
+      setAttendants(data.attendants);
+    } else {
+      setAttendants([]);
+    }
+
+    setLoading(false);
+  }
+
+  function formatTime(minutes) {
+    return moment()
+      .startOf("day")
+      .add(minutes, "minutes")
+      .format("HH[h] mm[m]");
+  }
+
+    const GetContacts = (all) => {
+    let props = {};
+    if (all) {
+      props = {};
+    }
+    const { count } = useContacts(props);
+    return count;
+  };
+  
+    function renderFilters() {
+    if (filterType === 1) {
+      return (
+        <>
+          <Grid item xs={12} sm={6} md={4}>
+            <TextField
+              label={i18n.t("dashboard.filters.initialDate")}
+              type="date"
+              value={dateFrom}
+              onChange={(e) => setDateFrom(e.target.value)}
+              className={classes.fullWidth}
+              InputLabelProps={{
+                shrink: true,
+              }}
+            />
+          </Grid>
+          <Grid item xs={12} sm={6} md={4}>
+            <TextField
+              label={i18n.t("dashboard.filters.finalDate")}
+              type="date"
+              value={dateTo}
+              onChange={(e) => setDateTo(e.target.value)}
+              className={classes.fullWidth}
+              InputLabelProps={{
+                shrink: true,
+              }}
+            />
+          </Grid>
+        </>
+      );
+    } else {
+      return (
+        <Grid item xs={12} sm={6} md={4}>
+          <FormControl className={classes.selectContainer}>
+            <InputLabel id="period-selector-label">
+              {i18n.t("dashboard.periodSelect.title")}
+            </InputLabel>
+            <Select
+              labelId="period-selector-label"
+              id="period-selector"
+              value={period}
+              onChange={(e) => handleChangePeriod(e.target.value)}
+            >
+              <MenuItem value={0}>{i18n.t("dashboard.periodSelect.options.none")}</MenuItem>
+              <MenuItem value={3}>{i18n.t("dashboard.periodSelect.options.last3")}</MenuItem>
+              <MenuItem value={7}>{i18n.t("dashboard.periodSelect.options.last7")}</MenuItem>
+              <MenuItem value={15}>{i18n.t("dashboard.periodSelect.options.last15")}</MenuItem>
+              <MenuItem value={30}>{i18n.t("dashboard.periodSelect.options.last30")}</MenuItem>
+              <MenuItem value={60}>{i18n.t("dashboard.periodSelect.options.last60")}</MenuItem>
+              <MenuItem value={90}>{i18n.t("dashboard.periodSelect.options.last90")}</MenuItem>
+            </Select>
+            <FormHelperText>{i18n.t("dashboard.periodSelect.helper")}</FormHelperText>
+          </FormControl>
+        </Grid>
+      );
+    }
+  }
+
+  return (
+    <div>
+      <Container maxWidth="lg" className={classes.container}>
+        <Grid container spacing={3} justifyContent="flex-end">
+		
+
+          {/* EM ATENDIMENTO */}
+          <Grid item xs={12} sm={6} md={4}>
+            <Paper
+              className={classes.card1}
+              style={{ overflow: "hidden" }}
+              elevation={4}
+            >
+              <Grid container spacing={3}>
+                <Grid item xs={8}>
+                  <Typography
+                    component="h3"
+                    variant="h6"
+                    paragraph
+                  >
+                    {i18n.t("dashboard.counters.inTalk")}
+                  </Typography>
+                  <Grid item>
+                    <Typography
+                      component="h1"
+                      variant="h4"
+                    >
+                      {counters.supportHappening}
+                    </Typography>
+                  </Grid>
+                </Grid>
+                <Grid item xs={2}>
+                  <CallIcon
+                    style={{
+                      fontSize: 100,
+                      color: "#FFFFFF",
+                    }}
+                  />
+                </Grid>
+              </Grid>
+            </Paper>
+          </Grid>
+
+          {/* AGUARDANDO */}
+          <Grid item xs={12} sm={6} md={4}>
+            <Paper
+              className={classes.card2}
+              style={{ overflow: "hidden" }}
+              elevation={6}
+            >
+              <Grid container spacing={3}>
+                <Grid item xs={8}>
+                  <Typography
+                    component="h3"
+                    variant="h6"
+                    paragraph
+                  >
+                    {i18n.t("dashboard.counters.waiting")}
+                  </Typography>
+                  <Grid item>
+                    <Typography
+                      component="h1"
+                      variant="h4"
+                    >
+                      {counters.supportPending}
+                    </Typography>
+                  </Grid>
+                </Grid>
+                <Grid item xs={4}>
+                  <HourglassEmptyIcon
+                    style={{
+                      fontSize: 100,
+                      color: "#FFFFFF",
+                    }}
+                  />
+                </Grid>
+              </Grid>
+            </Paper>
+          </Grid>
+
+          {/* ATENDENTES ATIVOS */}
+			  {/*<Grid item xs={12} sm={6} md={4}>
+            <Paper
+              className={classes.card6}
+              style={{ overflow: "hidden" }}
+              elevation={6}
+            >
+              <Grid container spacing={3}>
+                <Grid item xs={8}>
+                  <Typography
+                    component="h3"
+                    variant="h6"
+                    paragraph
+                  >
+                    Conversas Ativas
+                  </Typography>
+                  <Grid item>
+                    <Typography
+                      component="h1"
+                      variant="h4"
+                    >
+                      {GetUsers()}
+                      <span
+                        style={{ color: "#805753" }}
+                      >
+                        /{attendants.length}
+                      </span>
+                    </Typography>
+                  </Grid>
+                </Grid>
+                <Grid item xs={4}>
+                  <RecordVoiceOverIcon
+                    style={{
+                      fontSize: 100,
+                      color: "#805753",
+                    }}
+                  />
+                </Grid>
+              </Grid>
+            </Paper>
+</Grid>*/}
+
+          {/* FINALIZADOS */}
+          <Grid item xs={12} sm={6} md={4}>
+            <Paper
+              className={classes.card3}
+              style={{ overflow: "hidden" }}
+              elevation={6}
+            >
+              <Grid container spacing={3}>
+                <Grid item xs={8}>
+                  <Typography
+                    component="h3"
+                    variant="h6"
+                    paragraph
+                  >
+                    {i18n.t("dashboard.counters.finished")}
+                  </Typography>
+                  <Grid item>
+                    <Typography
+                      component="h1"
+                      variant="h4"
+                    >
+                      {counters.supportFinished}
+                    </Typography>
+                  </Grid>
+                </Grid>
+                <Grid item xs={4}>
+                  <CheckCircleIcon
+                    style={{
+                      fontSize: 100,
+                      color: "#FFFFFF",
+                    }}
+                  />
+                </Grid>
+              </Grid>
+            </Paper>
+          </Grid>
+
+          {/* NOVOS CONTATOS */}
+          <Grid item xs={12} sm={6} md={4}>
+            <Paper
+              className={classes.card4}
+              style={{ overflow: "hidden" }}
+              elevation={6}
+            >
+              <Grid container spacing={3}>
+                <Grid item xs={8}>
+                  <Typography
+                    component="h3"
+                    variant="h6"
+                    paragraph
+                  >
+                    {i18n.t("dashboard.counters.newContacts")}
+                  </Typography>
+                  <Grid item>
+                    <Typography
+                      component="h1"
+                      variant="h4"
+                    >
+                      {GetContacts(true)}
+                    </Typography>
+                  </Grid>
+                </Grid>
+                <Grid item xs={4}>
+                  <GroupAddIcon
+                    style={{
+                      fontSize: 100,
+                      color: "#FFFFFF",
+                    }}
+                  />
+                </Grid>
+              </Grid>
+            </Paper>
+          </Grid>
+
+          
+          {/* T.M. DE ATENDIMENTO */}
+          <Grid item xs={12} sm={6} md={4}>
+            <Paper
+              className={classes.card8}
+              style={{ overflow: "hidden" }}
+              elevation={6}
+            >
+              <Grid container spacing={3}>
+                <Grid item xs={8}>
+                  <Typography
+                    component="h3"
+                    variant="h6"
+                    paragraph
+                  >
+                    {i18n.t("dashboard.counters.averageTalkTime")}
+                  </Typography>
+                  <Grid item>
+                    <Typography
+                      component="h1"
+                      variant="h4"
+                    >
+                      {formatTime(counters.avgSupportTime)}
+                    </Typography>
+                  </Grid>
+                </Grid>
+                <Grid item xs={4}>
+                  <AccessAlarmIcon
+                    style={{
+                      fontSize: 100,
+                      color: "#FFFFFF",
+                    }}
+                  />
+                </Grid>
+              </Grid>
+            </Paper>
+          </Grid>
+
+          {/* T.M. DE ESPERA */}
+          <Grid item xs={12} sm={6} md={4}>
+            <Paper
+              className={classes.card9}
+              style={{ overflow: "hidden" }}
+              elevation={6}
+            >
+              <Grid container spacing={3}>
+                <Grid item xs={8}>
+                  <Typography
+                    component="h3"
+                    variant="h6"
+                    paragraph
+                  >
+                    {i18n.t("dashboard.counters.averageWaitTime")}
+                  </Typography>
+                  <Grid item>
+                    <Typography
+                      component="h1"
+                      variant="h4"
+                    >
+                      {formatTime(counters.avgWaitTime)}
+                    </Typography>
+                  </Grid>
+                </Grid>
+                <Grid item xs={4}>
+                  <TimerIcon
+                    style={{
+                      fontSize: 100,
+                      color: "#FFFFFF",
+                    }}
+                  />
+                </Grid>
+              </Grid>
+            </Paper>
+          </Grid>
+		  
+		  {/* FILTROS */}
+          <Grid item xs={12} sm={6} md={4}>
+            <FormControl className={classes.selectContainer}>
+              <InputLabel id="period-selector-label">{i18n.t("dashboard.filters.filterType.title")}</InputLabel>
+              <Select
+                labelId="period-selector-label"
+                value={filterType}
+                onChange={(e) => handleChangeFilterType(e.target.value)}
+              >
+                <MenuItem value={1}>{i18n.t("dashboard.filters.filterType.options.perDate")}</MenuItem>
+                <MenuItem value={2}>{i18n.t("dashboard.filters.filterType.options.perPeriod")}</MenuItem>
+              </Select>
+              <FormHelperText>
+                {i18n.t("dashboard.filters.filterType.helper")}
+              </FormHelperText>
+            </FormControl>
+          </Grid>
+
+          {renderFilters()}
+
+          {/* BOTAO FILTRAR */}
+          <Grid item xs={12} className={classes.alignRight}>
+            <ButtonWithSpinner
+              loading={loading}
+              onClick={() => fetchData()}
+              variant="contained"
+              color="primary"
+            >
+              {i18n.t("dashboard.buttons.filter")}
+            </ButtonWithSpinner>
+          </Grid>
+
+          {/* USUARIOS ONLINE */}
+          <Grid item xs={12}>
+            {attendants.length ? (
+              <TableAttendantsStatus
+                attendants={attendants}
+                loading={loading}
+              />
+            ) : null}
+          </Grid>
+
+          {/* TOTAL DE ATENDIMENTOS POR USUARIO */}
+          <Grid item xs={12}>
+            <Paper className={classes.fixedHeightPaper2}>
+              <ChatsUser />
+            </Paper>
+          </Grid>
+
+          {/* TOTAL DE ATENDIMENTOS */}
+          <Grid item xs={12}>
+            <Paper className={classes.fixedHeightPaper2}>
+              <ChartsDate />
+            </Paper>
+          </Grid>
+
+        </Grid>
+      </Container >
+    </div >
+  );
+};
+
+export default Dashboard;

+ 351 - 0
frontend/src/pages/Dashboard/index_old.js

@@ -0,0 +1,351 @@
+import React, { useState, useEffect } from "react";
+
+import Paper from "@material-ui/core/Paper";
+import Container from "@material-ui/core/Container";
+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 TextField from "@material-ui/core/TextField";
+import FormHelperText from "@material-ui/core/FormHelperText";
+
+import SpeedIcon from "@material-ui/icons/Speed";
+import GroupIcon from "@material-ui/icons/Group";
+import AssignmentIcon from "@material-ui/icons/Assignment";
+import PersonIcon from "@material-ui/icons/Person";
+import TodayIcon from '@material-ui/icons/Today';
+import BlockIcon from '@material-ui/icons/Block';
+import DoneIcon from '@material-ui/icons/Done';
+
+import { makeStyles } from "@material-ui/core/styles";
+import { grey, blue } from "@material-ui/core/colors";
+import { toast } from "react-toastify";
+
+import Chart from "./Chart";
+import ButtonWithSpinner from "../../components/ButtonWithSpinner";
+
+import CardCounter from "../../components/Dashboard/CardCounter";
+import TableAttendantsStatus from "../../components/Dashboard/TableAttendantsStatus";
+import { isArray } from "lodash";
+
+import useDashboard from "../../hooks/useDashboard";
+import useCompanies from "../../hooks/useCompanies";
+
+import { isEmpty } from "lodash";
+import moment from "moment";
+
+const useStyles = makeStyles((theme) => ({
+  container: {
+    paddingTop: theme.spacing(4),
+    paddingBottom: theme.spacing(4),
+  },
+  fixedHeightPaper: {
+    padding: theme.spacing(2),
+    display: "flex",
+    flexDirection: "column",
+    height: 240,
+    overflowY: "auto",
+    ...theme.scrollbarStyles,
+  },
+  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",
+  },
+}));
+
+const Dashboard = () => {
+  const classes = useStyles();
+  const [counters, setCounters] = useState({});
+  const [attendants, setAttendants] = useState([]);
+  const [filterType, setFilterType] = useState(1);
+  const [period, setPeriod] = useState(0);
+  const [companyDueDate, setCompanyDueDate] = useState();
+  const [dateFrom, setDateFrom] = useState(
+    moment("1", "D").format("YYYY-MM-DD")
+  );
+  const [dateTo, setDateTo] = useState(moment().format("YYYY-MM-DD"));
+  const [loading, setLoading] = useState(false);
+  const { find } = useDashboard();
+  const { finding } = useCompanies();
+  useEffect(() => {
+    async function firstLoad() {
+      await fetchData();
+    }
+    setTimeout(() => {
+      firstLoad();
+    }, 1000);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  async function handleChangePeriod(value) {
+    setPeriod(value);
+  }
+
+  async function handleChangeFilterType(value) {
+    setFilterType(value);
+    if (value === 1) {
+      setPeriod(0);
+    } else {
+      setDateFrom("");
+      setDateTo("");
+    }
+  }
+
+  async function fetchData() {
+    setLoading(true);
+
+    let params = {};
+
+    if (period > 0) {
+      params = {
+        days: period,
+      };
+    }
+
+    if (!isEmpty(dateFrom) && moment(dateFrom).isValid()) {
+      params = {
+        ...params,
+        date_from: moment(dateFrom).format("YYYY-MM-DD"),
+      };
+    }
+
+    if (!isEmpty(dateTo) && moment(dateTo).isValid()) {
+      params = {
+        ...params,
+        date_to: moment(dateTo).format("YYYY-MM-DD"),
+      };
+    }
+
+    if (Object.keys(params).length === 0) {
+      toast.error("Parametrize o filtro");
+      setLoading(false);
+      return;
+    }
+
+    const data = await find(params);
+
+
+
+    setCounters(data.counters);
+    if (isArray(data.attendants)) {
+      setAttendants(data.attendants);
+    } else {
+      setAttendants([]);
+    }
+
+    setLoading(false);
+  }
+
+  useEffect(() => {
+    async function fetchData() {
+      await loadCompanies();
+    }
+    fetchData();
+  }, [])
+  //let companyDueDate = localStorage.getItem("companyDueDate");
+  //const companyDueDate = localStorage.getItem("companyDueDate").toString();
+  const companyId = localStorage.getItem("companyId");
+  const loadCompanies = async () => {
+    setLoading(true);
+    try {
+      const companiesList = await finding(companyId);
+      setCompanyDueDate(moment(companiesList.dueDate).format("DD/MM/yyyy"));
+    } catch (e) {
+      console.log("🚀 Console Log : e", e);
+      // toast.error("Não foi possível carregar a lista de registros");
+    }
+    setLoading(false);
+  };
+
+  function formatTime(minutes) {
+    return moment()
+      .startOf("day")
+      .add(minutes, "minutes")
+      .format("HH[h] mm[m]");
+  }
+
+  function renderFilters() {
+    if (filterType === 1) {
+      return (
+        <>
+          <Grid item xs={12} sm={6} md={4}>
+            <TextField
+              label="Data Inicial"
+              type="date"
+              value={dateFrom}
+              onChange={(e) => setDateFrom(e.target.value)}
+              className={classes.fullWidth}
+              InputLabelProps={{
+                shrink: true,
+              }}
+            />
+          </Grid>
+          <Grid item xs={12} sm={6} md={4}>
+            <TextField
+              label="Data Final"
+              type="date"
+              value={dateTo}
+              onChange={(e) => setDateTo(e.target.value)}
+              className={classes.fullWidth}
+              InputLabelProps={{
+                shrink: true,
+              }}
+            />
+          </Grid>
+        </>
+      );
+    } else {
+      return (
+        <Grid item xs={12} sm={6} md={4}>
+          <FormControl className={classes.selectContainer}>
+            <InputLabel id="period-selector-label">Período</InputLabel>
+            <Select
+              labelId="period-selector-label"
+              id="period-selector"
+              value={period}
+              onChange={(e) => handleChangePeriod(e.target.value)}
+            >
+              <MenuItem value={0}>Nenhum selecionado</MenuItem>
+              <MenuItem value={3}>Últimos 3 dias</MenuItem>
+              <MenuItem value={7}>Últimos 7 dias</MenuItem>
+              <MenuItem value={15}>Últimos 15 dias</MenuItem>
+              <MenuItem value={30}>Últimos 30 dias</MenuItem>
+              <MenuItem value={60}>Últimos 60 dias</MenuItem>
+              <MenuItem value={90}>Últimos 90 dias</MenuItem>
+            </Select>
+            <FormHelperText>Selecione o período desejado</FormHelperText>
+          </FormControl>
+        </Grid>
+      );
+    }
+  }
+
+  return (
+    <div>
+      <Container maxWidth="lg" className={classes.container}>
+        <Grid container spacing={3} justifyContent="flex-end">
+          <Grid item xs={12} sm={6} md={3}>
+            <CardCounter
+              icon={<TodayIcon fontSize="inherit" />}
+              title="Data Vencimento"
+              value={companyDueDate}
+              loading={loading}
+            />
+          </Grid>
+          <Grid item xs={12}>
+            <Paper className={classes.fixedHeightPaper}>
+              <Chart />
+            </Paper>
+          </Grid>
+          <Grid item xs={12} sm={6} md={4}>
+            <FormControl className={classes.selectContainer}>
+              <InputLabel id="period-selector-label">Tipo de Filtro</InputLabel>
+              <Select
+                labelId="period-selector-label"
+                value={filterType}
+                onChange={(e) => handleChangeFilterType(e.target.value)}
+              >
+                <MenuItem value={1}>Filtro por Data</MenuItem>
+                <MenuItem value={2}>Filtro por Período</MenuItem>
+              </Select>
+              <FormHelperText>Selecione o período desejado</FormHelperText>
+            </FormControl>
+          </Grid>
+
+          {renderFilters()}
+
+          <Grid item xs={12} className={classes.alignRight}>
+            <ButtonWithSpinner
+              loading={loading}
+              onClick={() => fetchData()}
+              variant="contained"
+              color="primary"
+            >
+              Filtrar
+            </ButtonWithSpinner>
+          </Grid>
+          <Grid item xs={12} sm={6} md={4}>
+            <CardCounter
+              icon={<GroupIcon fontSize="inherit" />}
+              title="Atd. Pendentes"
+              value={counters.supportPending}
+              loading={loading}
+            />
+          </Grid>
+          <Grid item xs={12} sm={6} md={4}>
+            <CardCounter
+              icon={<GroupIcon fontSize="inherit" />}
+              title="Atd. Acontecendo"
+              value={counters.supportHappening}
+              loading={loading}
+            />
+          </Grid>
+          <Grid item xs={12} sm={6} md={4}>
+            <CardCounter
+              icon={<AssignmentIcon fontSize="inherit" />}
+              title="Atd. Realizados"
+              value={counters.supportFinished}
+              loading={loading}
+            />
+          </Grid>
+          <Grid item xs={12} sm={6} md={4}>
+            <CardCounter
+              icon={<PersonIcon fontSize="inherit" />}
+              title="Leads"
+              value={counters.leads}
+              loading={loading}
+            />
+          </Grid>
+          <Grid item xs={12} sm={6} md={4}>
+            <CardCounter
+              icon={<SpeedIcon fontSize="inherit" />}
+              title="T.M. de Atendimento"
+              value={formatTime(counters.avgSupportTime)}
+              loading={loading}
+            />
+          </Grid>
+          <Grid item xs={12} sm={6} md={4}>
+            <CardCounter
+              icon={<SpeedIcon fontSize="inherit" />}
+              title="T.M. de Espera"
+              value={formatTime(counters.avgWaitTime)}
+              loading={loading}
+            />
+          </Grid>
+          <Grid item xs={12}>
+            {attendants.length ? (
+              <TableAttendantsStatus
+                attendants={attendants}
+                loading={loading}
+              />
+            ) : null}
+          </Grid>
+        </Grid>
+      </Container>
+    </div>
+  );
+};
+
+export default Dashboard;

+ 290 - 0
frontend/src/pages/Files/index.js

@@ -0,0 +1,290 @@
+import React, {
+    useState,
+    useEffect,
+    useReducer,
+    useCallback,
+    useContext,
+} from "react";
+import { toast } from "react-toastify";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+import Button from "@material-ui/core/Button";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableHead from "@material-ui/core/TableHead";
+import TableRow from "@material-ui/core/TableRow";
+import IconButton from "@material-ui/core/IconButton";
+import SearchIcon from "@material-ui/icons/Search";
+import TextField from "@material-ui/core/TextField";
+import InputAdornment from "@material-ui/core/InputAdornment";
+
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import EditIcon from "@material-ui/icons/Edit";
+
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
+import Title from "../../components/Title";
+
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+import FileModal from "../../components/FileModal";
+import ConfirmationModal from "../../components/ConfirmationModal";
+import toastError from "../../errors/toastError";
+import { SocketContext } from "../../context/Socket/SocketContext";
+import { AuthContext } from "../../context/Auth/AuthContext";
+
+const reducer = (state, action) => {
+    if (action.type === "LOAD_FILES") {
+        const files = action.payload;
+        const newFiles = [];
+
+        files.forEach((fileList) => {
+            const fileListIndex = state.findIndex((s) => s.id === fileList.id);
+            if (fileListIndex !== -1) {
+                state[fileListIndex] = fileList;
+            } else {
+                newFiles.push(fileList);
+            }
+        });
+
+        return [...state, ...newFiles];
+    }
+
+    if (action.type === "UPDATE_FILES") {
+        const fileList = action.payload;
+        const fileListIndex = state.findIndex((s) => s.id === fileList.id);
+
+        if (fileListIndex !== -1) {
+            state[fileListIndex] = fileList;
+            return [...state];
+        } else {
+            return [fileList, ...state];
+        }
+    }
+
+    if (action.type === "DELETE_TAG") {
+        const fileListId = action.payload;
+
+        const fileListIndex = state.findIndex((s) => s.id === fileListId);
+        if (fileListIndex !== -1) {
+            state.splice(fileListIndex, 1);
+        }
+        return [...state];
+    }
+
+    if (action.type === "RESET") {
+        return [];
+    }
+};
+
+const useStyles = makeStyles((theme) => ({
+    mainPaper: {
+        flex: 1,
+        padding: theme.spacing(1),
+        overflowY: "scroll",
+        ...theme.scrollbarStyles,
+    },
+}));
+
+const FileLists = () => {
+    const classes = useStyles();
+
+    const { user } = useContext(AuthContext);
+
+    const [loading, setLoading] = useState(false);
+    const [pageNumber, setPageNumber] = useState(1);
+    const [hasMore, setHasMore] = useState(false);
+    const [selectedFileList, setSelectedFileList] = useState(null);
+    const [deletingFileList, setDeletingFileList] = useState(null);
+    const [confirmModalOpen, setConfirmModalOpen] = useState(false);
+    const [searchParam, setSearchParam] = useState("");
+    const [files, dispatch] = useReducer(reducer, []);
+    const [fileListModalOpen, setFileListModalOpen] = useState(false);
+
+    const fetchFileLists = useCallback(async () => {
+        try {
+            const { data } = await api.get("/files/", {
+                params: { searchParam, pageNumber },
+            });
+            dispatch({ type: "LOAD_FILES", payload: data.files });
+            setHasMore(data.hasMore);
+            setLoading(false);
+        } catch (err) {
+            toastError(err);
+        }
+    }, [searchParam, pageNumber]);
+
+    const socketManager = useContext(SocketContext);
+
+    useEffect(() => {
+        dispatch({ type: "RESET" });
+        setPageNumber(1);
+    }, [searchParam]);
+
+    useEffect(() => {
+        setLoading(true);
+        const delayDebounceFn = setTimeout(() => {
+            fetchFileLists();
+        }, 500);
+        return () => clearTimeout(delayDebounceFn);
+    }, [searchParam, pageNumber, fetchFileLists]);
+
+    useEffect(() => {
+        const socket = socketManager.getSocket(user.companyId);
+
+        socket.on(`company-${user.companyId}-file`, (data) => {
+            if (data.action === "update" || data.action === "create") {
+                dispatch({ type: "UPDATE_FILES", payload: data.files });
+            }
+
+            if (data.action === "delete") {
+                dispatch({ type: "DELETE_USER", payload: +data.fileId });
+            }
+        });
+
+        return () => {
+            socket.disconnect();
+        };
+    }, [socketManager, user]);
+
+    const handleOpenFileListModal = () => {
+        setSelectedFileList(null);
+        setFileListModalOpen(true);
+    };
+
+    const handleCloseFileListModal = () => {
+        setSelectedFileList(null);
+        setFileListModalOpen(false);
+    };
+
+    const handleSearch = (event) => {
+        setSearchParam(event.target.value.toLowerCase());
+    };
+
+    const handleEditFileList = (fileList) => {
+        setSelectedFileList(fileList);
+        setFileListModalOpen(true);
+    };
+
+    const handleDeleteFileList = async (fileListId) => {
+        try {
+            await api.delete(`/files/${fileListId}`);
+            toast.success(i18n.t("files.toasts.deleted"));
+        } catch (err) {
+            toastError(err);
+        }
+        setDeletingFileList(null);
+        setSearchParam("");
+        setPageNumber(1);
+
+        dispatch({ type: "RESET" });
+        setPageNumber(1);
+        await fetchFileLists();
+    };
+
+    const loadMore = () => {
+        setPageNumber((prevState) => prevState + 1);
+    };
+
+    const handleScroll = (e) => {
+        if (!hasMore || loading) return;
+        const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
+        if (scrollHeight - (scrollTop + 100) < clientHeight) {
+            loadMore();
+        }
+    };
+
+    return (
+        <MainContainer>
+            <ConfirmationModal
+                title={deletingFileList && `${i18n.t("files.confirmationModal.deleteTitle")}`}
+                open={confirmModalOpen}
+                onClose={setConfirmModalOpen}
+                onConfirm={() => handleDeleteFileList(deletingFileList.id)}
+            >
+                {i18n.t("files.confirmationModal.deleteMessage")}
+            </ConfirmationModal>
+            <FileModal
+                open={fileListModalOpen}
+                onClose={handleCloseFileListModal}
+                reload={fetchFileLists}
+                aria-labelledby="form-dialog-title"
+                fileListId={selectedFileList && selectedFileList.id}
+            />
+            <MainHeader>
+                <Title>{i18n.t("files.title")} ({files.length})</Title>
+                <MainHeaderButtonsWrapper>
+                    <TextField
+                        placeholder={i18n.t("contacts.searchPlaceholder")}
+                        type="search"
+                        value={searchParam}
+                        onChange={handleSearch}
+                        InputProps={{
+                            startAdornment: (
+                                <InputAdornment position="start">
+                                    <SearchIcon style={{ color: "gray" }} />
+                                </InputAdornment>
+                            ),
+                        }}
+                    />
+                    <Button
+                        variant="contained"
+                        color="primary"
+                        onClick={handleOpenFileListModal}
+                    >
+                        {i18n.t("files.buttons.add")}
+                    </Button>
+                </MainHeaderButtonsWrapper>
+            </MainHeader>
+            <Paper
+                className={classes.mainPaper}
+                variant="outlined"
+                onScroll={handleScroll}
+            >
+                <Table size="small">
+                    <TableHead>
+                        <TableRow>
+                            <TableCell align="center">{i18n.t("files.table.name")}</TableCell>
+                            <TableCell align="center">
+                                {i18n.t("files.table.actions")}
+                            </TableCell>
+                        </TableRow>
+                    </TableHead>
+                    <TableBody>
+                        <>
+                            {files.map((fileList) => (
+                                <TableRow key={fileList.id}>
+                                    <TableCell align="center">
+                                        {fileList.name}
+                                    </TableCell>
+                                    <TableCell align="center">
+                                        <IconButton size="small" onClick={() => handleEditFileList(fileList)}>
+                                            <EditIcon />
+                                        </IconButton>
+
+                                        <IconButton
+                                            size="small"
+                                            onClick={(e) => {
+                                                setConfirmModalOpen(true);
+                                                setDeletingFileList(fileList);
+                                            }}
+                                        >
+                                            <DeleteOutlineIcon />
+                                        </IconButton>
+                                    </TableCell>
+                                </TableRow>
+                            ))}
+                            {loading && <TableRowSkeleton columns={4} />}
+                        </>
+                    </TableBody>
+                </Table>
+            </Paper>
+        </MainContainer>
+    );
+};
+
+export default FileLists;

+ 244 - 0
frontend/src/pages/Financeiro/index.js

@@ -0,0 +1,244 @@
+import React, { useState, useEffect, useReducer } from "react";
+import { toast } from "react-toastify";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+import Button from "@material-ui/core/Button";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableHead from "@material-ui/core/TableHead";
+import TableRow from "@material-ui/core/TableRow";
+import IconButton from "@material-ui/core/IconButton";
+import SearchIcon from "@material-ui/icons/Search";
+import TextField from "@material-ui/core/TextField";
+import InputAdornment from "@material-ui/core/InputAdornment";
+
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import EditIcon from "@material-ui/icons/Edit";
+
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
+import Title from "../../components/Title";
+import SubscriptionModal from "../../components/SubscriptionModal";
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+import UserModal from "../../components/UserModal";
+import ConfirmationModal from "../../components/ConfirmationModal";
+import toastError from "../../errors/toastError";
+
+import moment from "moment";
+
+const reducer = (state, action) => {
+  if (action.type === "LOAD_INVOICES") {
+    const invoices = action.payload;
+    const newUsers = [];
+
+    invoices.forEach((user) => {
+      const userIndex = state.findIndex((u) => u.id === user.id);
+      if (userIndex !== -1) {
+        state[userIndex] = user;
+      } else {
+        newUsers.push(user);
+      }
+    });
+
+    return [...state, ...newUsers];
+  }
+
+  if (action.type === "UPDATE_USERS") {
+    const user = action.payload;
+    const userIndex = state.findIndex((u) => u.id === user.id);
+
+    if (userIndex !== -1) {
+      state[userIndex] = user;
+      return [...state];
+    } else {
+      return [user, ...state];
+    }
+  }
+
+  if (action.type === "DELETE_USER") {
+    const userId = action.payload;
+
+    const userIndex = state.findIndex((u) => u.id === userId);
+    if (userIndex !== -1) {
+      state.splice(userIndex, 1);
+    }
+    return [...state];
+  }
+
+  if (action.type === "RESET") {
+    return [];
+  }
+};
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+}));
+
+const Invoices = () => {
+  const classes = useStyles();
+
+  const [loading, setLoading] = useState(false);
+  const [pageNumber, setPageNumber] = useState(1);
+  const [hasMore, setHasMore] = useState(false);
+  const [searchParam, setSearchParam] = useState("");
+  const [invoices, dispatch] = useReducer(reducer, []);
+  const [storagePlans, setStoragePlans] = React.useState([]);
+  const [selectedContactId, setSelectedContactId] = useState(null);
+  const [contactModalOpen, setContactModalOpen] = useState(false);
+
+
+  const handleOpenContactModal = (invoices) => {
+    setStoragePlans(invoices);
+    setSelectedContactId(null);
+    setContactModalOpen(true);
+  };
+
+
+  const handleCloseContactModal = () => {
+    setSelectedContactId(null);
+    setContactModalOpen(false);
+  };
+  useEffect(() => {
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+  }, [searchParam]);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      const fetchInvoices = async () => {
+        try {
+          const { data } = await api.get("/invoices/all", {
+            params: { searchParam, pageNumber },
+          });
+          dispatch({ type: "LOAD_INVOICES", payload: data });
+          setHasMore(data.hasMore);
+          setLoading(false);
+        } catch (err) {
+          toastError(err);
+        }
+      };
+      fetchInvoices();
+    }, 500);
+    return () => clearTimeout(delayDebounceFn);
+  }, [searchParam, pageNumber]);
+
+
+  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 rowStyle = (record) => {
+    const hoje = moment(moment()).format("DD/MM/yyyy");
+    const vencimento = moment(record.dueDate).format("DD/MM/yyyy");
+    var diff = moment(vencimento, "DD/MM/yyyy").diff(moment(hoje, "DD/MM/yyyy"));
+    var dias = moment.duration(diff).asDays();    
+    if (dias < 0 && record.status !== "paid") {
+      return { backgroundColor: "#ffbcbc9c" };
+    }
+  };
+
+  const rowStatus = (record) => {
+    const hoje = moment(moment()).format("DD/MM/yyyy");
+    const vencimento = moment(record.dueDate).format("DD/MM/yyyy");
+    var diff = moment(vencimento, "DD/MM/yyyy").diff(moment(hoje, "DD/MM/yyyy"));
+    var dias = moment.duration(diff).asDays();    
+    const status = record.status;
+    if (status === "paid") {
+      return i18n.t("invoices.paid");
+    }
+    if (dias < 0) {
+      return i18n.t("invoices.expired");
+    } else {
+      return i18n.t("invoices.open");
+    }
+
+  }
+
+  return (
+    <MainContainer>
+      <SubscriptionModal
+        open={contactModalOpen}
+        onClose={handleCloseContactModal}
+        aria-labelledby="form-dialog-title"
+        Invoice={storagePlans}
+        contactId={selectedContactId}
+
+      ></SubscriptionModal>
+      <MainHeader>
+        <Title>{i18n.t("invoices.title")}</Title>
+      </MainHeader>
+      <Paper
+        className={classes.mainPaper}
+        variant="outlined"
+        onScroll={handleScroll}
+      >
+        <Table size="small">
+          <TableHead>
+            <TableRow>
+              <TableCell align="center">Id</TableCell>
+              <TableCell align="center">{i18n.t("invoices.details")}</TableCell>
+              <TableCell align="center">{i18n.t("invoices.value")}</TableCell>
+              <TableCell align="center">{i18n.t("invoices.dueDate")}</TableCell>
+              <TableCell align="center">{i18n.t("invoices.status")}</TableCell>
+              <TableCell align="center">{i18n.t("invoices.action")}</TableCell>
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            <>
+              {invoices.map((invoices) => (
+                <TableRow style={rowStyle(invoices)} key={invoices.id}>
+                  <TableCell align="center">{invoices.id}</TableCell>
+                  <TableCell align="center">{invoices.detail}</TableCell>
+                  <TableCell style={{ fontWeight: 'bold' }} align="center">{invoices.value.toLocaleString('pt-br', { style: 'currency', currency: 'BRL' })}</TableCell>
+                  <TableCell align="center">{moment(invoices.dueDate).format("DD/MM/YYYY")}</TableCell>
+                  <TableCell style={{ fontWeight: 'bold' }} align="center">{rowStatus(invoices)}</TableCell>
+                  <TableCell align="center">
+                    {rowStatus(invoices) !== i18n.t("invoices.paid") ?
+                      <Button
+                        size="small"
+                        variant="outlined"
+                        color="secondary"
+                        onClick={() => handleOpenContactModal(invoices)}
+                      >
+                        {i18n.t("invoices.PAY")}
+                      </Button> :
+                      <Button
+                        size="small"
+                        variant="outlined" 
+                        /* color="secondary"
+                        disabled */
+                      >
+                        {i18n.t("invoices.PAID")}
+                      </Button>}
+
+                  </TableCell>
+                </TableRow>
+              ))}
+              {loading && <TableRowSkeleton columns={4} />}
+            </>
+          </TableBody>
+        </Table>
+      </Paper>
+    </MainContainer>
+  );
+};
+
+export default Invoices;

+ 356 - 0
frontend/src/pages/ForgetPassWord/index.js

@@ -0,0 +1,356 @@
+import React, { useState } from "react";
+import qs from "query-string";
+import IconButton from "@material-ui/core/IconButton";
+import VisibilityIcon from "@material-ui/icons/Visibility";
+import VisibilityOffIcon from "@material-ui/icons/VisibilityOff";
+import InputAdornment from "@material-ui/core/InputAdornment";
+import * as Yup from "yup";
+import { useHistory } from "react-router-dom";
+import { Link as RouterLink } from "react-router-dom";
+import { Formik, Form, Field } from "formik";
+import Button from "@material-ui/core/Button";
+import CssBaseline from "@material-ui/core/CssBaseline";
+import TextField from "@material-ui/core/TextField";
+import Link from "@material-ui/core/Link";
+import Grid from "@material-ui/core/Grid";
+import Box from "@material-ui/core/Box";
+import Typography from "@material-ui/core/Typography";
+import { makeStyles } from "@material-ui/core/styles";
+import Container from "@material-ui/core/Container";
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+import moment from "moment";
+import logo from "../../assets/logo.png";
+import { toast } from 'react-toastify'; 
+import toastError from '../../errors/toastError';
+import 'react-toastify/dist/ReactToastify.css';
+import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    width: "100vw",
+    height: "100vh",
+    background: "black", //Cor de fundo
+    backgroundRepeat: "no-repeat",
+    backgroundSize: "100% 100%",
+    backgroundPosition: "center",
+    display: "flex",
+    flexDirection: "column",
+    alignItems: "center",
+    justifyContent: "center",
+    textAlign: "center",
+  },
+  paper: {
+    backgroundColor: "white",
+    display: "flex",
+    flexDirection: "column",
+    alignItems: "center",
+    padding: "55px 30px",
+    borderRadius: "12.5px",
+  },
+  avatar: {
+    margin: theme.spacing(1),
+    backgroundColor: theme.palette.secondary.main,
+  },
+  form: {
+    width: "100%", // Fix IE 11 issue.
+    marginTop: theme.spacing(1),
+  },
+  submit: {
+    margin: theme.spacing(3, 0, 2),
+  },
+  powered: {
+    color: "white",
+  },
+}));
+
+const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/;
+
+const ForgetPassword = () => {
+  const classes = useStyles();
+  const history = useHistory();
+  let companyId = null;
+  const [showAdditionalFields, setShowAdditionalFields] = useState(false);
+  const [showResetPasswordButton, setShowResetPasswordButton] = useState(false);
+  const [showPassword, setShowPassword] = useState(false);
+  const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+  const [error, setError] = useState(""); // Estado para mensagens de erro
+
+  const togglePasswordVisibility = () => {
+    setShowPassword(!showPassword);
+  };
+
+  const toggleConfirmPasswordVisibility = () => {
+    setShowConfirmPassword(!showConfirmPassword);
+  };
+
+  const toggleAdditionalFields = () => {
+    setShowAdditionalFields(!showAdditionalFields);
+    if (showAdditionalFields) {
+      setShowResetPasswordButton(false);
+    } else {
+      setShowResetPasswordButton(true);
+    }
+  };
+
+  const params = qs.parse(window.location.search);
+  if (params.companyId !== undefined) {
+    companyId = params.companyId;
+  }
+
+  const initialState = { email: "" };
+
+  const [user] = useState(initialState);
+  const dueDate = moment().add(3, "day").format();
+
+const handleSendEmail = async (values) => {
+  const email = values.email;
+  try {
+    const response = await api.post(
+      `${process.env.REACT_APP_BACKEND_URL}/forgetpassword/${email}`
+    );
+    console.log("API Response:", response.data);
+
+    if (response.data.status === 404) {
+      toast.error(i18n.t("resetPassword.toasts.emailNotFound"));
+    } else {
+      toast.success(i18n.t("resetPassword.toasts.emailSent"));
+    }
+  } catch (err) {
+    console.log("API Error:", err);
+    toastError(err);
+  }
+};
+
+  const handleResetPassword = async (values) => {
+    const email = values.email;
+    const token = values.token;
+    const newPassword = values.newPassword;
+    const confirmPassword = values.confirmPassword;
+
+    if (newPassword === confirmPassword) {
+      try {
+        await api.post(
+          `${process.env.REACT_APP_BACKEND_URL}/resetpasswords/${email}/${token}/${newPassword}`
+        );
+        setError(""); // Limpe o erro se não houver erro
+        toast.success(i18n.t("resetPassword.toasts.passwordUpdated"));
+        history.push("/login");
+      } catch (err) {
+        console.log(err);
+      }
+    }
+  };
+
+  const isResetPasswordButtonClicked = showResetPasswordButton;
+  const UserSchema = Yup.object().shape({
+    email: Yup.string().email(i18n.t("resetPassword.formErrors.email.invalid")).required(i18n.t("resetPassword.formErrors.email.required")),
+    newPassword: isResetPasswordButtonClicked
+      ? Yup.string()
+          .required(i18n.t("resetPassword.formErrors.newPassword.required"))
+          .matches(
+            passwordRegex,
+            i18n.t("resetPassword.formErrors.newPassword.matches")
+          )
+      : Yup.string(), // Sem validação se não for redefinição de senha
+    confirmPassword: Yup.string().when("newPassword", {
+      is: (newPassword) => isResetPasswordButtonClicked && newPassword,
+      then: Yup.string()
+        .oneOf([Yup.ref("newPassword"), null], i18n.t("resetPassword.formErrors.confirmPassword.matches"))
+        .required(i18n.t("resetPassword.formErrors.confirmPassword.required")),
+      otherwise: Yup.string(), // Sem validação se não for redefinição de senha
+    }),
+  });
+
+  return (
+    <div className={classes.root}>
+      <Container component="main" maxWidth="xs">
+        <CssBaseline />
+        <div className={classes.paper}>
+          <div>
+            <img
+              style={{ margin: "0 auto", height: "80px", width: "100%" }}
+              src={logo}
+              alt="Whats"
+            />
+          </div>
+          <Typography component="h1" variant="h5">
+            {i18n.t("resetPassword.title")}
+          </Typography>
+          <Formik
+            initialValues={{
+              email: "",
+              token: "",
+              newPassword: "",
+              confirmPassword: "",
+            }}
+            enableReinitialize={true}
+            validationSchema={UserSchema}
+            onSubmit={(values, actions) => {
+              setTimeout(() => {
+                if (showResetPasswordButton) {
+                  handleResetPassword(values);
+                } else {
+                  handleSendEmail(values);
+                }
+                actions.setSubmitting(false);
+                toggleAdditionalFields();
+              }, 400);
+            }}
+          >
+            {({ touched, errors, isSubmitting }) => (
+              <Form className={classes.form}>
+                <Grid container spacing={2}>
+                  <Grid item xs={12}>
+                    <Field
+                      as={TextField}
+                      variant="outlined"
+                      fullWidth
+                      id="email"
+                      label={i18n.t("resetPassword.form.email")}
+                      name="email"
+                      error={touched.email && Boolean(errors.email)}
+                      helperText={touched.email && errors.email}
+                      autoComplete="email"
+                      required
+                    />
+                  </Grid>
+                  {showAdditionalFields && (
+                    <>
+                      <Grid item xs={12}>
+                        <Field
+                          as={TextField}
+                          variant="outlined"
+                          fullWidth
+                          id="token"
+                          label={i18n.t("resetPassword.form.verificationCode")}
+                          name="token"
+                          error={touched.token && Boolean(errors.token)}
+                          helperText={touched.token && errors.token}
+                          autoComplete="off"
+                          required
+                        />
+                      </Grid>
+                      <Grid item xs={12}>
+                        <Field
+                          as={TextField}
+                          variant="outlined"
+                          fullWidth
+                          type={showPassword ? "text" : "password"}
+                          id="newPassword"
+                          label={i18n.t("resetPassword.form.newPassword")}
+                          name="newPassword"
+                          error={
+                            touched.newPassword &&
+                            Boolean(errors.newPassword)
+                          }
+                          helperText={
+                            touched.newPassword && errors.newPassword
+                          }
+                          autoComplete="off"
+                          required
+                          InputProps={{
+                            endAdornment: (
+                              <InputAdornment position="end">
+                                <IconButton
+                                  onClick={togglePasswordVisibility}
+                                >
+                                  {showPassword ? (
+                                    <VisibilityIcon />
+                                  ) : (
+                                    <VisibilityOffIcon />
+                                  )}
+                                </IconButton>
+                              </InputAdornment>
+                            ),
+                          }}
+                        />
+                      </Grid>
+                      <Grid item xs={12}>
+                        <Field
+                          as={TextField}
+                          variant="outlined"
+                          fullWidth
+                          type={showConfirmPassword ? "text" : "password"}
+                          id="confirmPassword"
+                          label={i18n.t("resetPassword.form.confirmPassword")}
+                          name="confirmPassword"
+                          error={
+                            touched.confirmPassword &&
+                            Boolean(errors.confirmPassword)
+                          }
+                          helperText={
+                            touched.confirmPassword &&
+                            errors.confirmPassword
+                          }
+                          autoComplete="off"
+                          required
+                          InputProps={{
+                            endAdornment: (
+                              <InputAdornment position="end">
+                                <IconButton
+                                  onClick={toggleConfirmPasswordVisibility}
+                                >
+                                  {showConfirmPassword ? (
+                                    <VisibilityIcon />
+                                  ) : (
+                                    <VisibilityOffIcon />
+                                  )}
+                                </IconButton>
+                              </InputAdornment>
+                            ),
+                          }}
+                        />
+                      </Grid>
+                    </>
+                  )}
+                </Grid>
+                {showResetPasswordButton ? (
+                  <Button
+                    type="submit"
+                    fullWidth
+                    variant="contained"
+                    color="primary"
+                    className={classes.submit}
+                  >
+                    {i18n.t("resetPassword.buttons.submitPassword")}
+                  </Button>
+                ) : (
+                  <Button
+                    type="submit"
+                    fullWidth
+                    variant="contained"
+                    color="primary"
+                    className={classes.submit}
+                  >
+{                    i18n.t("resetPassword.buttons.submitEmail")}
+                  </Button>
+                )}
+                <Grid container justifyContent="flex-end">
+                  <Grid item>
+                    <Link
+                      href="#"
+                      variant="body2"
+                      component={RouterLink}
+                      to="/signup"
+                    >
+                      {i18n.t("resetPassword.buttons.back")}
+                    </Link>
+                  </Grid>
+                </Grid>
+                {error && (
+                  <Typography variant="body2" color="error">
+                    {error}
+                  </Typography>
+                )}
+              </Form>
+            )}
+          </Formik>
+        </div>
+        <Box mt={5} />
+      </Container>
+    </div>
+  );
+};
+
+export default ForgetPassword;

+ 172 - 0
frontend/src/pages/Helps/index.js

@@ -0,0 +1,172 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { makeStyles, Paper, Typography, Modal, IconButton } from "@material-ui/core";
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
+import Title from "../../components/Title";
+import { i18n } from "../../translate/i18n";
+import useHelps from "../../hooks/useHelps";
+
+const useStyles = makeStyles(theme => ({
+  mainPaperContainer: {
+    overflowY: 'auto',
+    maxHeight: 'calc(100vh - 200px)',
+  },
+  mainPaper: {
+    width: '100%',
+    display: 'grid',
+    gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
+    gap: theme.spacing(3),
+    padding: theme.spacing(2),
+    marginBottom: theme.spacing(3),
+  },
+  helpPaper: {
+    position: 'relative',
+    width: '100%',
+    minHeight: '340px',
+    padding: theme.spacing(2),
+    boxShadow: theme.shadows[3],
+    borderRadius: theme.spacing(1),
+    cursor: 'pointer',
+    display: 'flex',
+    flexDirection: 'column',
+    justifyContent: 'space-between',
+    maxWidth: '340px',
+  },
+  paperHover: {
+    transition: 'transform 0.3s, box-shadow 0.3s',
+    '&:hover': {
+      transform: 'scale(1.03)',
+      boxShadow: `0 0 8px`,
+      color: theme.palette.primary.main,
+    },
+  },
+  videoThumbnail: {
+    width: '100%',
+    height: 'calc(100% - 56px)',
+    objectFit: 'cover',
+    borderRadius: `${theme.spacing(1)}px ${theme.spacing(1)}px 0 0`,
+  },
+  videoTitle: {
+    marginTop: theme.spacing(1),
+    flex: 1,
+  },
+  videoDescription: {
+    maxHeight: '100px',
+    overflow: 'hidden',
+  },
+  videoModal: {
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  videoModalContent: {
+    outline: 'none',
+    width: '90%',
+    maxWidth: 1024,
+    aspectRatio: '16/9',
+    position: 'relative',
+    backgroundColor: 'white',
+    borderRadius: theme.spacing(1),
+    overflow: 'hidden',
+  },
+}));
+
+const Helps = () => {
+  const classes = useStyles();
+  const [records, setRecords] = useState([]);
+  const { list } = useHelps();
+  const [selectedVideo, setSelectedVideo] = useState(null);
+
+  useEffect(() => {
+    async function fetchData() {
+      const helps = await list();
+      setRecords(helps);
+    }
+    fetchData();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  const openVideoModal = (video) => {
+    setSelectedVideo(video);
+  };
+
+  const closeVideoModal = () => {
+    setSelectedVideo(null);
+  };
+
+  const handleModalClose = useCallback((event) => {
+    if (event.key === "Escape") {
+      closeVideoModal();
+    }
+  }, []);
+
+  useEffect(() => {
+    document.addEventListener("keydown", handleModalClose);
+    return () => {
+      document.removeEventListener("keydown", handleModalClose);
+    };
+  }, [handleModalClose]);
+
+  const renderVideoModal = () => {
+    return (
+      <Modal
+        open={Boolean(selectedVideo)}
+        onClose={closeVideoModal}
+        className={classes.videoModal}
+      >
+        <div className={classes.videoModalContent}>
+          {selectedVideo && (
+            <iframe
+              style={{ width: "100%", height: "100%", position: "absolute", top: 0, left: 0 }}
+              src={`https://www.youtube.com/embed/${selectedVideo}`}
+              title="YouTube video player"
+              frameBorder="0"
+              allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
+              allowFullScreen
+            />
+          )}
+        </div>
+      </Modal>
+    );
+  };
+
+  const renderHelps = () => {
+    return (
+      <>
+        <div className={`${classes.mainPaper} ${classes.mainPaperContainer}`}>
+          {records.length ? records.map((record, key) => (
+            <Paper key={key} className={`${classes.helpPaper} ${classes.paperHover}`} onClick={() => openVideoModal(record.video)}>
+              <img
+                src={`https://img.youtube.com/vi/${record.video}/mqdefault.jpg`}
+                alt="Thumbnail"
+                className={classes.videoThumbnail}
+              />
+              <Typography variant="button" className={classes.videoTitle}>
+                {record.title}
+              </Typography>
+              <Typography variant="caption" className={classes.videoDescription}>
+                {record.description}
+              </Typography>
+            </Paper>
+          )) : null}
+        </div>
+      </>
+    );
+  };
+
+  return (
+    <MainContainer>
+      <MainHeader>
+        <Title>{i18n.t("helps.title")} ({records.length})</Title>
+        <MainHeaderButtonsWrapper></MainHeaderButtonsWrapper>
+      </MainHeader>
+      <div className={classes.mainPaper}>
+        {renderHelps()}
+      </div>
+      {renderVideoModal()}
+    </MainContainer>
+  );
+};
+
+export default Helps;

+ 183 - 0
frontend/src/pages/Kanban/index.js

@@ -0,0 +1,183 @@
+import React, { useState, useEffect, useContext } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+import api from "../../services/api";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import Board from "react-trello";
+import { toast } from "react-toastify";
+import { i18n } from "../../translate/i18n";
+import { useHistory } from "react-router-dom";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    display: "flex",
+    alignItems: "center",
+    padding: theme.spacing(1),
+  },
+  button: {
+    background: "#10a110",
+    border: "none",
+    padding: "10px",
+    color: "white",
+    fontWeight: "bold",
+    borderRadius: "5px",
+  },
+}));
+
+const Kanban = () => {
+  const classes = useStyles();
+  const history = useHistory();
+
+  const [tags, setTags] = useState([]);
+
+  const fetchTags = async () => {
+    try {
+      const response = await api.get("/tags/kanban");
+      const fetchedTags = response.data.lista ?? [];
+
+      setTags(fetchedTags);
+
+      // Fetch tickets after fetching tags
+      await fetchTickets(jsonString);
+    } catch (error) {
+      console.log(error);
+    }
+  };
+
+  useEffect(() => {
+    fetchTags();
+  }, []);
+
+  const [file, setFile] = useState({
+    lanes: [],
+  });
+
+  const [tickets, setTickets] = useState([]);
+  const { user } = useContext(AuthContext);
+  const { profile, queues } = user;
+  const jsonString = user.queues.map((queue) => queue.UserQueue.queueId);
+
+  const fetchTickets = async (jsonString) => {
+    try {
+      const { data } = await api.get("/ticket/kanban", {
+        params: {
+          queueIds: JSON.stringify(jsonString),
+          showAll: profile === "admin",
+        },
+      });
+
+      setTickets(data.tickets);
+    } catch (err) {
+      console.log(err);
+      setTickets([]);
+    }
+  };
+
+  const popularCards = (jsonString) => {
+    const filteredTickets = tickets.filter(
+      (ticket) => ticket.tags.length === 0
+    );
+
+    const lanes = [
+      {
+        id: "lane0",
+        title: i18n.t("kanban.open"),
+        label: filteredTickets.length.toString(),
+        cards: filteredTickets.map((ticket) => ({
+          id: ticket.id.toString(),
+          label: "Ticket nº " + ticket.id.toString(),
+          description: (
+            <div>
+              <p>
+                {ticket.contact.number}
+                <br />
+                {ticket.lastMessage}
+              </p>
+              <button
+                className={classes.button}
+                onClick={() => {
+                  handleCardClick(ticket.uuid);
+                }}
+              >
+                {i18n.t("kanban.seeTicket")}
+              </button>
+            </div>
+          ),
+          title: ticket.contact.name,
+          draggable: true,
+          href: "/tickets/" + ticket.uuid,
+        })),
+      },
+      ...tags.map((tag) => {
+        const tagsTickets = tickets.filter((ticket) => {
+          const tagIds = ticket.tags.map((tag) => tag.id);
+          return tagIds.includes(tag.id);
+        });
+
+        return {
+          id: tag.id.toString(),
+          title: tag.name,
+          label: tagsTickets.length.toString(),
+          cards: tagsTickets.map((ticket) => ({
+            id: ticket.id.toString(),
+            label: "Ticket nº " + ticket.id.toString(),
+            description: (
+              <div>
+                <p>
+                  {ticket.contact.number}
+                  <br />
+                  {ticket.lastMessage}
+                </p>
+                <button
+                  className={classes.button}
+                  onClick={() => {
+                    handleCardClick(ticket.uuid);
+                  }}
+                >
+                  {i18n.t("kanban.seeTicket")}
+                </button>
+              </div>
+            ),
+            title: ticket.contact.name,
+            draggable: true,
+            href: "/tickets/" + ticket.uuid,
+          })),
+          style: { backgroundColor: tag.color, color: "white" },
+        };
+      }),
+    ];
+
+    setFile({ lanes });
+  };
+
+  const handleCardClick = (uuid) => {
+    //console.log("Clicked on card with UUID:", uuid);
+    history.push("/tickets/" + uuid);
+  };
+
+  useEffect(() => {
+    popularCards(jsonString);
+  }, [tags, tickets]);
+
+  const handleCardMove = async (cardId, sourceLaneId, targetLaneId) => {
+    try {
+      await api.delete(`/ticket-tags/${targetLaneId}`);
+      toast.success(i18n.t("kanban.toasts.removed"));
+      await api.put(`/ticket-tags/${targetLaneId}/${sourceLaneId}`);
+      toast.success(i18n.t("kanban.toasts.added"));
+    } catch (err) {
+      console.log(err);
+    }
+  };
+
+  return (
+    <div className={classes.root}>
+      <Board
+        data={file}
+        onCardMoveAcrossLanes={handleCardMove}
+        style={{ backgroundColor: "rgba(252, 252, 252, 0.03)" }}
+      />
+    </div>
+  );
+};
+
+export default Kanban;

+ 13 - 0
frontend/src/pages/Kanban/responsive.css

@@ -0,0 +1,13 @@
+@media (min-width: 390px) {
+  mobile {
+    width: 80%;
+    height: 50%;
+    border-radius: 5px;
+    top: 50%;
+    left: 50%;
+    right: auto;
+    bottom: auto;
+    margin-right: -50%;
+    transform: translate(-50%, -50%);
+  }
+}

+ 221 - 0
frontend/src/pages/Login/index.js

@@ -0,0 +1,221 @@
+import React, { useState, useContext } from "react";
+import { Link as RouterLink } from "react-router-dom";
+
+import Button from "@material-ui/core/Button";
+import CssBaseline from "@material-ui/core/CssBaseline";
+import TextField from "@material-ui/core/TextField";
+import Link from "@material-ui/core/Link";
+import Grid from "@material-ui/core/Grid"; 
+import Box from "@material-ui/core/Box";
+import Typography from "@material-ui/core/Typography";
+import { makeStyles } from "@material-ui/core/styles";
+import Container from "@material-ui/core/Container";
+import { versionSystem } from "../../../package.json";
+import { i18n } from "../../translate/i18n";
+import { nomeEmpresa } from "../../../package.json";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import logo from "../../assets/logo.png";
+import {LanguageOutlined} from "@material-ui/icons";
+import {IconButton, Menu, MenuItem} from "@material-ui/core";
+import LanguageControl from "../../components/LanguageControl";
+
+
+const Copyright = () => {
+	return (
+		<Typography variant="body2" color="primary" align="center">
+			{"Copyright "}
+ 			<Link color="primary" href="#">
+ 				{ nomeEmpresa } - v { versionSystem }
+ 			</Link>{" "}
+ 			{new Date().getFullYear()}
+ 			{"."}
+ 		</Typography>
+ 	);
+ };
+
+const useStyles = makeStyles(theme => ({
+	root: {
+		width: "100vw",
+		height: "100vh",
+		//background: "linear-gradient(to right, #682EE3 , #682EE3 , #682EE3)",
+		//backgroundImage: "url(https://i.imgur.com/CGby9tN.png)",
+		backgroundColor: theme.palette.primary.main,
+		backgroundRepeat: "no-repeat",
+		backgroundSize: "100% 100%",
+		backgroundPosition: "center",
+		display: "flex",
+		flexDirection: "column",
+		alignItems: "center",
+		justifyContent: "center",
+		textAlign: "center",
+		position: "relative"
+	},
+	paper: {
+		backgroundColor: theme.palette.login,
+		display: "flex",
+		flexDirection: "column",
+		alignItems: "center",
+		padding: "55px 30px",
+		borderRadius: "12.5px",
+	},
+	avatar: {
+		margin: theme.spacing(1),  
+		backgroundColor: theme.palette.secondary.main,
+	},
+	form: {
+		width: "100%", // Fix IE 11 issue.
+		marginTop: theme.spacing(1),
+	},
+	submit: {
+		margin: theme.spacing(3, 0, 2),
+	},
+	powered: {
+		color: "white"
+	},
+	languageControl: {
+		position: "absolute",
+		top: 0,
+		left: 0,
+		paddingLeft: 15
+	}
+}));
+
+const Login = () => {
+	const classes = useStyles();
+
+	const [user, setUser] = useState({ email: "", password: "" });
+
+	// Languages
+	const [anchorElLanguage, setAnchorElLanguage] = useState(null);
+	const [menuLanguageOpen, setMenuLanguageOpen] = useState(false);
+
+	const { handleLogin } = useContext(AuthContext);
+
+	const handleChangeInput = e => {
+		setUser({ ...user, [e.target.name]: e.target.value });
+	};
+
+	const handlSubmit = e => {
+		e.preventDefault();
+		handleLogin(user);
+	};
+
+	const handlemenuLanguage = ( event ) => {
+		setAnchorElLanguage(event.currentTarget);
+		setMenuLanguageOpen( true );
+	}
+
+	const handleCloseMenuLanguage = (  ) => {
+		setAnchorElLanguage(null);
+		setMenuLanguageOpen(false);
+	}
+	
+	return (
+		<div className={classes.root}>
+		<div className={classes.languageControl}>
+			<IconButton edge="start">
+				<LanguageOutlined
+					aria-label="account of current user"
+					aria-controls="menu-appbar"
+					aria-haspopup="true"
+					onClick={handlemenuLanguage}
+					variant="contained"
+					style={{ color: "white",marginRight:10 }}
+				/>
+			</IconButton>
+			<Menu
+				id="menu-appbar-language"
+				anchorEl={anchorElLanguage}
+				getContentAnchorEl={null}
+				anchorOrigin={{
+					vertical: "bottom",
+					horizontal: "right",
+				}}
+				transformOrigin={{
+					vertical: "top",
+					horizontal: "right",
+				}}
+				open={menuLanguageOpen}
+				onClose={handleCloseMenuLanguage}
+			>
+				<MenuItem>
+					<LanguageControl />
+				</MenuItem>
+			</Menu>
+		</div>
+		<Container component="main" maxWidth="xs">
+			<CssBaseline/>
+			<div className={classes.paper}>
+				<div>
+					<img style={{ margin: "0 auto", width: "70%" }} src={logo} alt="Whats" />
+				</div>
+				{/*<Typography component="h1" variant="h5">
+					{i18n.t("login.title")}
+				</Typography>*/}
+				<form className={classes.form} noValidate onSubmit={handlSubmit}>
+					<TextField
+						variant="outlined"
+						margin="normal"
+						required
+						fullWidth
+						id="email"
+						label={i18n.t("login.form.email")}
+						name="email"
+						value={user.email}
+						onChange={handleChangeInput}
+						autoComplete="email"
+						autoFocus
+					/>
+					<TextField
+						variant="outlined"
+						margin="normal"
+						required
+						fullWidth
+						name="password"
+						label={i18n.t("login.form.password")}
+						type="password"
+						id="password"
+						value={user.password}
+						onChange={handleChangeInput}
+						autoComplete="current-password"
+					/>
+					
+					{/* <Grid container justify="flex-end">
+					  <Grid item xs={6} style={{ textAlign: "right" }}>
+						<Link component={RouterLink} to="/forgetpsw" variant="body2">
+						  Esqueceu sua senha?
+						</Link>
+					  </Grid>
+					</Grid>*/}
+					
+					<Button
+						type="submit"
+						fullWidth
+						variant="contained"
+						color="primary"
+						className={classes.submit}
+					>
+						{i18n.t("login.buttons.submit")}
+					</Button>
+					{ <Grid container>
+						<Grid item>
+							<Link
+								href="#"
+								variant="body2"
+								component={RouterLink}
+								to="/signup"
+							>
+								{i18n.t("login.buttons.register")}
+							</Link>
+						</Grid>
+					</Grid> }
+				</form>
+			
+			</div>
+			<Box mt={8}><Copyright /></Box>
+		</Container>
+		</div>
+	);
+};
+
+export default Login;

+ 222 - 0
frontend/src/pages/Login/style.css

@@ -0,0 +1,222 @@
+*{
+	padding: 0;
+	margin: 0;
+	box-sizing: border-box;
+}
+
+body{
+    font-family: 'Poppins', sans-serif;
+    overflow: hidden;
+}
+
+.wave{
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	height: 100%;
+	z-index: -1;
+}
+
+.container{
+    width: 100vw;
+    height: 100vh;
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    grid-gap :7rem;
+    padding: 0 2rem;
+}
+
+.img{
+	display: flex;
+	justify-content: flex-end;
+	align-items: center;
+}
+
+.login-content{
+	display: flex;
+	justify-content: flex-start;
+	align-items: center;
+	text-align: center;
+}
+
+.img img{
+	width: 500px;
+}
+
+form{
+	width: 360px;
+}
+
+.login-content img{
+    height: 100px;
+}
+
+.login-content h2{
+	margin: 15px 0;
+	color: #333;
+	text-transform: uppercase;
+	font-size: 2.9rem;
+}
+
+.login-content .input-div{
+	position: relative;
+    display: grid;
+    grid-template-columns: 7% 93%;
+    margin: 25px 0;
+    padding: 5px 0;
+    border-bottom: 2px solid #d9d9d9;
+}
+
+.login-content .input-div.one{
+	margin-top: 0;
+}
+
+.i{
+	color: #d9d9d9;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+}
+
+.i i{
+	transition: .3s;
+}
+
+.input-div > div{
+    position: relative;
+	height: 45px;
+}
+
+.input-div > div > h5{
+	position: absolute;
+	left: 10px;
+	top: 50%;
+	transform: translateY(-50%);
+	color: #999;
+	font-size: 18px;
+	transition: .3s;
+}
+
+.input-div:before, .input-div:after{
+	content: '';
+	position: absolute;
+	bottom: -2px;
+	width: 0%;
+	height: 2px;
+	background-color: #38d39f;
+	transition: .4s;
+}
+
+.input-div:before{
+	right: 50%;
+}
+
+.input-div:after{
+	left: 50%;
+}
+
+.input-div.focus:before, .input-div.focus:after{
+	width: 50%;
+}
+
+.input-div.focus > div > h5{
+	top: -5px;
+	font-size: 15px;
+}
+
+.input-div.focus > .i > i{
+	color: #38d39f;
+}
+
+.input-div > div > input{
+	position: absolute;
+	left: 0;
+	top: 0;
+	width: 100%;
+	height: 100%;
+	border: none;
+	outline: none;
+	background: none;
+	padding: 0.5rem 0.7rem;
+	font-size: 1.2rem;
+	color: #555;
+	font-family: 'poppins', sans-serif;
+}
+
+.input-div.pass{
+	margin-bottom: 4px;
+}
+
+a{
+	display: block;
+	text-align: right;
+	text-decoration: none;
+	color: #999;
+	font-size: 0.9rem;
+	transition: .3s;
+}
+
+a:hover{
+	color: #38d39f;
+}
+
+.btn{
+	display: block;
+	width: 100%;
+	height: 50px;
+	border-radius: 25px;
+	outline: none;
+	border: none;
+	background-image: linear-gradient(to right, #32be8f, #38d39f, #32be8f);
+	background-size: 200%;
+	font-size: 1.2rem;
+	color: #fff;
+	font-family: 'Poppins', sans-serif;
+	text-transform: uppercase;
+	margin: 1rem 0;
+	cursor: pointer;
+	transition: .5s;
+}
+.btn:hover{
+	background-position: right;
+}
+
+
+@media screen and (max-width: 1050px){
+	.container{
+		grid-gap: 5rem;
+	}
+}
+
+@media screen and (max-width: 1000px){
+	form{
+		width: 290px;
+	}
+
+	.login-content h2{
+        font-size: 2.4rem;
+        margin: 8px 0;
+	}
+
+	.img img{
+		width: 400px;
+	}
+}
+
+@media screen and (max-width: 900px){
+	.container{
+		grid-template-columns: 1fr;
+	}
+
+	.img{
+		display: none;
+	}
+
+	.wave{
+		display: none;
+	}
+
+	.login-content{
+		justify-content: center;
+	}
+}

+ 345 - 0
frontend/src/pages/MessagesAPI/index.js

@@ -0,0 +1,345 @@
+import React, { useState, useEffect } from "react";
+import { useHistory } from "react-router-dom";
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+
+import { i18n } from "../../translate/i18n";
+import { Button, CircularProgress, Grid, TextField, Typography } from "@material-ui/core";
+import { Field, Form, Formik } from "formik";
+import toastError from "../../errors/toastError";
+import { toast } from "react-toastify";
+// import api from "../../services/api";
+import axios from "axios";
+import usePlans from "../../hooks/usePlans";
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(2),
+    paddingBottom: 100
+  },
+  mainHeader: {
+    marginTop: theme.spacing(1),
+  },
+  elementMargin: {
+    padding: theme.spacing(2),
+  },
+  formContainer: {
+    maxWidth: 500,
+  },
+  textRight: {
+    textAlign: "right"
+  }
+}));
+
+const MessagesAPI = () => {
+  const classes = useStyles();
+  const history = useHistory();
+
+  const [formMessageTextData,] = useState({ token: '', number: '', body: '' })
+  const [formMessageMediaData,] = useState({ token: '', number: '', medias: '' })
+  const [file, setFile] = useState({})
+
+  const { getPlanCompany } = usePlans();
+
+  useEffect(() => {
+    async function fetchData() {
+      const companyId = localStorage.getItem("companyId");
+      const planConfigs = await getPlanCompany(undefined, companyId);
+      if (!planConfigs.plan.useExternalApi) {
+        toast.error("messagesAPI.toasts.unauthorized");
+        setTimeout(() => {
+          history.push(`/`)
+        }, 1000);
+      }
+    }
+    fetchData();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  const getEndpoint = () => {
+    return process.env.REACT_APP_BACKEND_URL + '/api/messages/send'
+  }
+
+  const handleSendTextMessage = async (values) => {
+    const { number, body } = values;
+    const data = { number, body };
+    try {
+      await axios.request({
+        url: getEndpoint(),
+        method: 'POST',
+        data,
+        headers: {
+          'Content-type': 'application/json',
+          'Authorization': `Bearer ${values.token}`
+        }
+      })
+      toast.success(i18n.t('messagesAPI.toasts.success'));
+    } catch (err) {
+      toastError(err);
+    }
+  }
+
+  const handleSendMediaMessage = async (values) => {
+    try {
+      const firstFile = file[0];
+      const data = new FormData();
+      data.append('number', values.number);
+      data.append('body', firstFile.name);
+      data.append('medias', firstFile);
+      await axios.request({
+        url: getEndpoint(),
+        method: 'POST',
+        data,
+        headers: {
+          'Content-type': 'multipart/form-data',
+          'Authorization': `Bearer ${values.token}`
+        }
+      })
+      toast.success(i18n.t('messagesAPI.toasts.success'));
+    } catch (err) {
+      toastError(err);
+    }
+  }
+
+  const renderFormMessageText = () => {
+    return (
+      <Formik
+        initialValues={formMessageTextData}
+        enableReinitialize={true}
+        onSubmit={(values, actions) => {
+          setTimeout(async () => {
+            await handleSendTextMessage(values);
+            actions.setSubmitting(false);
+            actions.resetForm()
+          }, 400);
+        }}
+        className={classes.elementMargin}
+      >
+        {({ isSubmitting }) => (
+          <Form className={classes.formContainer}>
+            <Grid container spacing={2}>
+              <Grid item xs={12} md={6}>
+                <Field
+                  as={TextField}
+                  label={i18n.t("messagesAPI.textMessage.token")}
+                  name="token"
+                  autoFocus
+                  variant="outlined"
+                  margin="dense"
+                  fullWidth
+                  className={classes.textField}
+                  required
+                />
+              </Grid>
+              <Grid item xs={12} md={6}>
+                <Field
+                  as={TextField}
+                  label={i18n.t("messagesAPI.textMessage.number")}
+                  name="number"
+                  autoFocus
+                  variant="outlined"
+                  margin="dense"
+                  fullWidth
+                  className={classes.textField}
+                  required
+                />
+              </Grid>
+              <Grid item xs={12}>
+                <Field
+                  as={TextField}
+                  label={i18n.t("messagesAPI.textMessage.body")}
+                  name="body"
+                  autoFocus
+                  variant="outlined"
+                  margin="dense"
+                  fullWidth
+                  className={classes.textField}
+                  required
+                />
+              </Grid>
+              <Grid item xs={12} className={classes.textRight}>
+                <Button
+                  type="submit"
+                  color="primary"
+                  variant="contained"
+                  className={classes.btnWrapper}
+                >
+                  {isSubmitting ? (
+                    <CircularProgress
+                      size={24}
+                      className={classes.buttonProgress}
+                    />
+                  ) : i18n.t('messagesAPI.buttons.send')}
+                </Button>
+              </Grid>
+            </Grid>
+          </Form>
+        )}
+      </Formik>
+    )
+  }
+
+  const renderFormMessageMedia = () => {
+    return (
+      <Formik
+        initialValues={formMessageMediaData}
+        enableReinitialize={true}
+        onSubmit={(values, actions) => {
+          setTimeout(async () => {
+            await handleSendMediaMessage(values);
+            actions.setSubmitting(false);
+            actions.resetForm()
+            document.getElementById('medias').files = null
+            document.getElementById('medias').value = null
+          }, 400);
+        }}
+        className={classes.elementMargin}
+      >
+        {({ isSubmitting }) => (
+          <Form className={classes.formContainer}>
+            <Grid container spacing={2}>
+              <Grid item xs={12} md={6}>
+                <Field
+                  as={TextField}
+                  label={i18n.t("messagesAPI.mediaMessage.token")}
+                  name="token"
+                  autoFocus
+                  variant="outlined"
+                  margin="dense"
+                  fullWidth
+                  className={classes.textField}
+                  required
+                />
+              </Grid>
+              <Grid item xs={12} md={6}>
+                <Field
+                  as={TextField}
+                  label={i18n.t("messagesAPI.mediaMessage.number")}
+                  name="number"
+                  autoFocus
+                  variant="outlined"
+                  margin="dense"
+                  fullWidth
+                  className={classes.textField}
+                  required
+                />
+              </Grid>
+              <Grid item xs={12}>
+                <input type="file" name="medias" id="medias" required onChange={(e) => setFile(e.target.files)} />
+              </Grid>
+              <Grid item xs={12} className={classes.textRight}>
+                <Button
+                  type="submit"
+                  color="primary"
+                  variant="contained"
+                  className={classes.btnWrapper}
+                >
+                  {isSubmitting ? (
+                    <CircularProgress
+                      size={24}
+                      className={classes.buttonProgress}
+                    />
+                  ) : i18n.t('messagesAPI.buttons.send')}
+                </Button>
+              </Grid>
+            </Grid>
+          </Form>
+        )}
+      </Formik>
+    )
+  }
+
+  return (
+    <Paper
+      className={classes.mainPaper}
+      style={{marginLeft: "5px"}}
+      // className={classes.elementMargin}
+      variant="outlined"
+    >
+      <Typography variant="h5">
+        {i18n.t('messagesAPI.labels.doc')}
+      </Typography>
+      <Typography variant="h6" color="primary" className={classes.elementMargin}>
+        {i18n.t('messagesAPI.labels.method')}
+      </Typography>
+      <Typography component="div">
+        <ol>
+          <li>
+            {i18n.t('messagesAPI.labels.textMessage')}
+          </li>
+          <li>
+            {i18n.t('messagesAPI.labels.mediaMessage')}
+          </li>
+        </ol>
+      </Typography>
+      <Typography variant="h6" color="primary" className={classes.elementMargin}>
+        {i18n.t('messagesAPI.labels.instructions')}
+      </Typography>
+      <Typography className={classes.elementMargin} component="div">
+        <b>{i18n.t('messagesAPI.labels.observations')}</b><br />
+        <ul>
+          <li>{i18n.t('messagesAPI.labels.before1')} <br />{i18n.t('messagesAPI.labels.before2')}</li>
+          <li>
+            {i18n.t('messagesAPI.labels.numberDescription')}
+            <ul>
+              <li>{i18n.t('messagesAPI.labels.countryCode')}</li>
+              <li>DDD</li>
+              <li>{i18n.t('messagesAPI.labels.number')}</li>
+            </ul>
+          </li>
+        </ul>
+      </Typography>
+      <Typography variant="h6" color="primary" className={classes.elementMargin}>
+        {i18n.t('messagesAPI.labels.textMessage2')}
+      </Typography>
+      <Grid container>
+        <Grid item xs={12} sm={6}>
+          <Typography className={classes.elementMargin} component="div">
+            <p>{i18n.t('messagesAPI.labels.textMessageInstructions')}</p>
+            <b>Endpoint: </b> {getEndpoint()} <br />
+            <b>{i18n.t('messagesAPI.labels.method2')}: </b> POST <br />
+            <b>Headers: </b> Authorization (Bearer token) {i18n.t('messagesAPI.labels.e')} Content-Type (application/json) <br />
+            <b>Body: </b> {"{ \"number\": \"5599999999999\", \"body\": \"Sua mensagem\" }"}
+          </Typography>
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <Typography className={classes.elementMargin}>
+            <b>{i18n.t('messagesAPI.labels.tests')}</b>
+          </Typography>
+          {renderFormMessageText()}
+        </Grid>
+      </Grid>
+      <Typography variant="h6" color="primary" className={classes.elementMargin}>
+        {i18n.t('messagesAPI.labels.mediaMessage2')}
+      </Typography>
+      <Grid container>
+        <Grid item xs={12} sm={6}>
+          <Typography className={classes.elementMargin} component="div">
+            <p>{i18n.t('messagesAPI.labels.textMessageInstructions')}</p>
+            <b>Endpoint: </b> {getEndpoint()} <br />
+            <b>{i18n.t('messagesAPI.labels.method2')}: </b> POST <br />
+            <b>Headers: </b> Authorization (Bearer token) {i18n.t('messagesAPI.labels.e')} Content-Type (multipart/form-data) <br />
+            <b>FormData: </b> <br />
+            <ul>
+              <li>
+                <b>number: </b> 5599999999999
+              </li>
+              <li>
+                <b>medias: </b> arquivo
+              </li>
+            </ul>
+          </Typography>
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <Typography className={classes.elementMargin}>
+            <b>{i18n.t('messagesAPI.labels.tests')}</b>
+          </Typography>
+          {renderFormMessageMedia()}
+        </Grid>
+      </Grid>
+    </Paper>
+  );
+};
+
+export default MessagesAPI;

+ 287 - 0
frontend/src/pages/Prompts/index.js

@@ -0,0 +1,287 @@
+import React, { useContext, useEffect, useReducer, useState } from "react";
+
+import {
+  Button,
+  IconButton,
+  Paper,
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableRow,
+  Typography // Importar Typography do Material-UI
+} from "@material-ui/core";
+
+import { makeStyles } from "@material-ui/core/styles";
+
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+import Title from "../../components/Title";
+import { i18n } from "../../translate/i18n";
+import toastError from "../../errors/toastError";
+import api from "../../services/api";
+import { DeleteOutline, Edit } from "@material-ui/icons";
+import PromptModal from "../../components/PromptModal";
+import { toast } from "react-toastify";
+import ConfirmationModal from "../../components/ConfirmationModal";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import usePlans from "../../hooks/usePlans";
+import { useHistory } from "react-router-dom/cjs/react-router-dom.min";
+import { SocketContext } from "../../context/Socket/SocketContext";
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+  customTableCell: {
+    display: "flex",
+    alignItems: "center",
+    justifyContent: "center",
+  },
+  // Adicione um estilo para a box vermelha
+  redBox: {
+    backgroundColor: "#ffcccc", // Definindo a cor de fundo vermelha
+    padding: theme.spacing(2), // Adicionando um espaçamento interno
+    marginBottom: theme.spacing(2), // Adicionando margem inferior para separar do conteúdo abaixo
+  },
+}));
+
+const reducer = (state, action) => {
+  if (action.type === "LOAD_PROMPTS") {
+    const prompts = action.payload;
+    const newPrompts = [];
+
+    if( prompts.length === 0 )
+      return [];
+
+    prompts.forEach((prompt) => {
+      const promptIndex = state.findIndex((p) => p.id === prompt.id);
+      if (promptIndex !== -1) {
+        state[promptIndex] = prompt;
+      } else {
+        newPrompts.push(prompt);
+      }
+    });
+
+    return [...state, ...newPrompts];
+  }
+
+  if (action.type === "UPDATE_PROMPTS") {
+    const prompt = action.payload;
+    const promptIndex = state.findIndex((p) => p.id === prompt.id);
+
+    if (promptIndex !== -1) {
+      state[promptIndex] = prompt;
+      return [...state];
+    } else {
+      return [prompt, ...state];
+    }
+  }
+
+  if (action.type === "DELETE_PROMPT") {
+    const promptId = action.payload;
+    const promptIndex = state.findIndex((p) => p.id === promptId);
+    if (promptIndex !== -1) {
+      state.splice(promptIndex, 1);
+    }
+    return [...state];
+  }
+
+  if (action.type === "RESET") {
+    return [];
+  }
+};
+
+const Prompts = () => {
+  const classes = useStyles();
+
+  const [prompts, dispatch] = useReducer(reducer, []);
+  const [loading, setLoading] = useState(false);
+
+  const [promptModalOpen, setPromptModalOpen] = useState(false);
+  const [selectedPrompt, setSelectedPrompt] = useState(null);
+  const [confirmModalOpen, setConfirmModalOpen] = useState(false);
+  const { user } = useContext(AuthContext);
+  const { getPlanCompany } = usePlans();
+  const history = useHistory();
+  const companyId = user.companyId;
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    async function fetchData() {
+      const planConfigs = await getPlanCompany(undefined, companyId);
+      if (!planConfigs.plan.useOpenAi) {
+        toast.error("Esta empresa não possui permissão para acessar essa página! Estamos lhe redirecionando.");
+        setTimeout(() => {
+          history.push(`/`)
+        }, 1000);
+      }
+    }
+    fetchData();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  useEffect(() => {
+    (async () => {
+      setLoading(true);
+      try {
+        getPrompts(  );
+
+        setLoading(false);
+      } catch (err) {
+        toastError(err);
+        setLoading(false);
+      }
+    })();
+  }, []);
+
+  useEffect(() => {
+    const socket = socketManager.getSocket(companyId);
+
+    socket.on(`company-${companyId}-prompt`, (data) => {
+      if (data.action === "update" || data.action === "create") {
+        dispatch({ type: "UPDATE_PROMPTS", payload: data.prompt });
+      }
+
+      if (data.action === "delete") {
+        dispatch({ type: "DELETE_PROMPT", payload: data.promptId });
+      }
+    });
+
+    return () => {
+      socket.disconnect();
+    };
+  }, [companyId, socketManager]);
+
+  const getPrompts = async (  ) => {
+
+    const { data } = await api.get("/prompt");
+    dispatch({ type: "LOAD_PROMPTS", payload: data.prompts });
+  }
+
+  const handleOpenPromptModal = () => {
+    setPromptModalOpen(true);
+    setSelectedPrompt(null);
+  };
+
+  const handleClosePromptModal = () => {
+    setPromptModalOpen(false);
+    setSelectedPrompt(null);
+  };
+
+  const handleEditPrompt = (prompt) => {
+    setSelectedPrompt(prompt);
+    setPromptModalOpen(true);
+  };
+
+  const handleCloseConfirmationModal = () => {
+    setConfirmModalOpen(false);
+    setSelectedPrompt(null);
+  };
+
+  const handleDeletePrompt = async (promptId) => {
+    try {
+
+      const { data } = await api.delete(`/prompt/${promptId}`);
+      dispatch({type: "DELETE_PROMPT", payload: promptId});
+      toast.info(i18n.t(data.message));
+  
+    } catch (err) {
+      toastError(err);
+    }
+    setSelectedPrompt(null);
+  };
+
+  return (
+    <MainContainer>
+      <ConfirmationModal
+        title={
+          selectedPrompt &&
+          `${i18n.t("prompts.confirmationModal.deleteTitle")} ${selectedPrompt.name
+          }?`
+        }
+        open={confirmModalOpen}
+        onClose={handleCloseConfirmationModal}
+        onConfirm={() => handleDeletePrompt(selectedPrompt.id)}
+      >
+        {i18n.t("prompts.confirmationModal.deleteMessage")}
+      </ConfirmationModal>
+      <PromptModal
+        open={promptModalOpen}
+        onClose={handleClosePromptModal}
+        promptId={selectedPrompt?.id}
+        refreshPrompts={getPrompts}
+      />
+      <MainHeader>
+        <Title>{i18n.t("prompts.title")}</Title>
+        <MainHeaderButtonsWrapper>
+          <Button
+            variant="contained"
+            color="primary"
+            onClick={handleOpenPromptModal}
+          >
+            {i18n.t("prompts.buttons.add")}
+          </Button>
+        </MainHeaderButtonsWrapper>
+      </MainHeader>
+      <Paper className={classes.mainPaper} variant="outlined">
+        <Table size="small">
+          <TableHead>
+            <TableRow>
+              <TableCell align="left">
+                {i18n.t("prompts.table.name")}
+              </TableCell>
+              <TableCell align="left">
+                {i18n.t("prompts.table.queue")}
+              </TableCell>
+              <TableCell align="left">
+                {i18n.t("prompts.table.max_tokens")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("prompts.table.actions")}
+              </TableCell>
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            <>
+              {prompts.map((prompt) => (
+                <TableRow key={prompt.id}>
+                  <TableCell align="left">{prompt.name}</TableCell>
+                  <TableCell align="left">{prompt.queue?.name}</TableCell>
+                  <TableCell align="left">{prompt.maxTokens}</TableCell>
+                  <TableCell align="center">
+                    <IconButton
+                      size="small"
+                      onClick={() => handleEditPrompt(prompt)}
+                    >
+                      <Edit />
+                    </IconButton>
+
+                    <IconButton
+                      size="small"
+                      onClick={() => {
+                        setSelectedPrompt(prompt);
+                        setConfirmModalOpen(true);
+                      }}
+                    >
+                      <DeleteOutline />
+                    </IconButton>
+                  </TableCell>
+                </TableRow>
+              ))}
+              {loading && <TableRowSkeleton columns={4} />}
+            </>
+          </TableBody>
+        </Table>
+      </Paper>
+    </MainContainer>
+  );
+};
+
+export default Prompts;

+ 331 - 0
frontend/src/pages/QueueIntegration/index.js

@@ -0,0 +1,331 @@
+import React, { useState, useEffect, useReducer, useContext } from "react";
+import { toast } from "react-toastify";
+import { SocketContext } from "../../context/Socket/SocketContext";
+import n8n from "../../assets/n8n.png";
+import dialogflow from "../../assets/dialogflow.png";
+import webhooks from "../../assets/webhook.png";
+import typebot from "../../assets/typebot.jpg";
+
+import { makeStyles } from "@material-ui/core/styles";
+
+import {
+  Avatar,
+  Button,
+  IconButton,
+  InputAdornment,
+  Paper,
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableRow,
+  TextField,
+  Tooltip
+} from "@material-ui/core";
+
+import {
+  DeleteOutline,
+  Edit
+} from "@material-ui/icons";
+
+import SearchIcon from "@material-ui/icons/Search";
+
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
+import Title from "../../components/Title";
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+import IntegrationModal from "../../components/QueueIntegrationModal";
+import ConfirmationModal from "../../components/ConfirmationModal";
+
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+import toastError from "../../errors/toastError";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import usePlans from "../../hooks/usePlans";
+import { useHistory } from "react-router-dom/cjs/react-router-dom.min";
+
+const reducer = (state, action) => {
+  if (action.type === "LOAD_INTEGRATIONS") {
+    const queueIntegration = action.payload;
+    const newIntegrations = [];
+
+    queueIntegration.forEach((integration) => {
+      const integrationIndex = state.findIndex((u) => u.id === integration.id);
+      if (integrationIndex !== -1) {
+        state[integrationIndex] = integration;
+      } else {
+        newIntegrations.push(integration);
+      }
+    });
+
+    return [...state, ...newIntegrations];
+  }
+
+  if (action.type === "UPDATE_INTEGRATIONS") {
+    const queueIntegration = action.payload;
+    const integrationIndex = state.findIndex((u) => u.id === queueIntegration.id);
+
+    if (integrationIndex !== -1) {
+      state[integrationIndex] = queueIntegration;
+      return [...state];
+    } else {
+      return [queueIntegration, ...state];
+    }
+  }
+
+  if (action.type === "DELETE_INTEGRATION") {
+    const integrationId = action.payload;
+
+    const integrationIndex = state.findIndex((u) => u.id === integrationId);
+    if (integrationIndex !== -1) {
+      state.splice(integrationIndex, 1);
+    }
+    return [...state];
+  }
+
+  if (action.type === "RESET") {
+    return [];
+  }
+};
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(2),
+    margin: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+  avatar: {
+    width: "140px",
+    height: "40px",
+    borderRadius: 4
+  },
+}));
+
+const QueueIntegration = () => {
+  const classes = useStyles();
+
+  const [loading, setLoading] = useState(false);
+  const [pageNumber, setPageNumber] = useState(1);
+  const [hasMore, setHasMore] = useState(false);
+  const [selectedIntegration, setSelectedIntegration] = useState(null);
+  const [deletingUser, setDeletingUser] = useState(null);
+  const [userModalOpen, setUserModalOpen] = useState(false);
+  const [confirmModalOpen, setConfirmModalOpen] = useState(false);
+  const [searchParam, setSearchParam] = useState("");
+  const [queueIntegration, dispatch] = useReducer(reducer, []);
+  const { user } = useContext(AuthContext);
+  const { getPlanCompany } = usePlans();
+  const companyId = user.companyId;
+  const history = useHistory();
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    async function fetchData() {
+      const planConfigs = await getPlanCompany(undefined, companyId);
+      if (!planConfigs.plan.useIntegrations) {
+        toast.error("Esta empresa não possui permissão para acessar essa página! Estamos lhe redirecionando.");
+        setTimeout(() => {
+          history.push(`/`)
+        }, 1000);
+      }
+    }
+    fetchData();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  useEffect(() => {
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+  }, [searchParam]);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      const fetchIntegrations = async () => {
+        try {
+          const { data } = await api.get("/queueIntegration/", {
+            params: { searchParam, pageNumber },
+          });
+          dispatch({ type: "LOAD_INTEGRATIONS", payload: data.queueIntegrations });
+          setHasMore(data.hasMore);
+          setLoading(false);
+        } catch (err) {
+          toastError(err);
+        }
+      };
+      fetchIntegrations();
+    }, 500);
+    return () => clearTimeout(delayDebounceFn);
+  }, [searchParam, pageNumber]);
+
+  useEffect(() => {
+    const companyId = localStorage.getItem("companyId");
+    const socket = socketManager.getSocket(companyId);
+
+    socket.on(`company-${companyId}-queueIntegration`, (data) => {
+      if (data.action === "update" || data.action === "create") {
+        dispatch({ type: "UPDATE_INTEGRATIONS", payload: data.queueIntegration });
+      }
+
+      if (data.action === "delete") {
+        dispatch({ type: "DELETE_INTEGRATION", payload: +data.integrationId });
+      }
+    });
+
+    return () => {
+      socket.disconnect();
+    };
+  }, [socketManager]);
+
+  const handleOpenUserModal = () => {
+    setSelectedIntegration(null);
+    setUserModalOpen(true);
+  };
+
+  const handleCloseIntegrationModal = () => {
+    setSelectedIntegration(null);
+    setUserModalOpen(false);
+  };
+
+  const handleSearch = (event) => {
+    setSearchParam(event.target.value.toLowerCase());
+  };
+
+  const handleEditIntegration = (queueIntegration) => {
+    setSelectedIntegration(queueIntegration);
+    setUserModalOpen(true);
+  };
+
+  const handleDeleteIntegration = async (integrationId) => {
+    try {
+      await api.delete(`/queueIntegration/${integrationId}`);
+      toast.success(i18n.t("queueIntegration.toasts.deleted"));
+    } catch (err) {
+      toastError(err);
+    }
+    setDeletingUser(null);
+    setSearchParam("");
+    setPageNumber(1);
+  };
+
+  const loadMore = () => {
+    setPageNumber((prevState) => prevState + 1);
+  };
+
+  const handleScroll = (e) => {
+    if (!hasMore || loading) return;
+    const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
+    if (scrollHeight - (scrollTop + 100) < clientHeight) {
+      loadMore();
+    }
+  };
+
+  return (
+    <MainContainer>
+      <ConfirmationModal
+        title={
+          deletingUser &&
+          `${i18n.t("queueIntegration.confirmationModal.deleteTitle")} ${deletingUser.name
+          }?`
+        }
+        open={confirmModalOpen}
+        onClose={setConfirmModalOpen}
+        onConfirm={() => handleDeleteIntegration(deletingUser.id)}
+      >
+        {i18n.t("queueIntegration.confirmationModal.deleteMessage")}
+      </ConfirmationModal>
+      <IntegrationModal
+        open={userModalOpen}
+        onClose={handleCloseIntegrationModal}
+        aria-labelledby="form-dialog-title"
+        integrationId={selectedIntegration && selectedIntegration.id}
+      />
+      <MainHeader>
+        <Title>{i18n.t("queueIntegration.title")} ({queueIntegration.length})</Title>
+        <MainHeaderButtonsWrapper>
+          <TextField
+            placeholder={i18n.t("queueIntegration.searchPlaceholder")}
+            type="search"
+            value={searchParam}
+            onChange={handleSearch}
+            InputProps={{
+              startAdornment: (
+                <InputAdornment position="start">
+                  <SearchIcon color="secondary" />
+                </InputAdornment>
+              ),
+            }}
+          />
+          <Button
+            variant="contained"
+            color="primary"
+            onClick={handleOpenUserModal}
+          >
+            {i18n.t("queueIntegration.buttons.add")}
+          </Button>
+        </MainHeaderButtonsWrapper>
+      </MainHeader>
+      <Paper
+        className={classes.mainPaper}
+        variant="outlined"
+        onScroll={handleScroll}
+      >
+        <Table size="small">
+          <TableHead>
+            <TableRow>
+              <TableCell padding="checkbox"></TableCell>
+              <TableCell align="center">{i18n.t("queueIntegration.table.id")}</TableCell>
+              <TableCell align="center">{i18n.t("queueIntegration.table.name")}</TableCell>
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            <>
+              {queueIntegration.map((integration) => (
+                <TableRow key={integration.id}>
+                  <TableCell >
+                    {integration.type === "dialogflow" && (<Avatar 
+                      src={dialogflow} className={classes.avatar} />)}
+                    {integration.type === "n8n" && (<Avatar
+                      src={n8n} className={classes.avatar} />)}
+                    {integration.type === "webhook" && (<Avatar
+                      src={webhooks} className={classes.avatar} />)}
+                    {integration.type === "typebot" && (<Avatar
+                      src={typebot} className={classes.avatar} />)}
+                  </TableCell>
+
+                  <TableCell align="center">{integration.id}</TableCell>
+                  <TableCell align="center">{integration.name}</TableCell>
+                  <TableCell align="center">
+                    <IconButton
+                      size="small"
+                      onClick={() => handleEditIntegration(integration)}
+                    >
+                      <Edit color="secondary" />
+                    </IconButton>
+
+                    <IconButton
+                      size="small"
+                      onClick={(e) => {
+                        setConfirmModalOpen(true);
+                        setDeletingUser(integration);
+                      }}
+                    >
+                      <DeleteOutline color="secondary" />
+                    </IconButton>
+                  </TableCell>
+                </TableRow>
+              ))}
+              {loading && <TableRowSkeleton columns={7} />}
+            </>
+          </TableBody>
+        </Table>
+      </Paper>
+    </MainContainer>
+  );
+};
+
+export default QueueIntegration;

+ 288 - 0
frontend/src/pages/Queues/index.js

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

+ 327 - 0
frontend/src/pages/QuickMessages/index.js

@@ -0,0 +1,327 @@
+import React, { useState, useEffect, useReducer, useContext, useCallback } from "react";
+import { toast } from "react-toastify";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+import Button from "@material-ui/core/Button";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableHead from "@material-ui/core/TableHead";
+import TableRow from "@material-ui/core/TableRow";
+import IconButton from "@material-ui/core/IconButton";
+import SearchIcon from "@material-ui/icons/Search";
+import TextField from "@material-ui/core/TextField";
+import InputAdornment from "@material-ui/core/InputAdornment";
+import CheckCircleIcon from '@material-ui/icons/CheckCircle';
+
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import EditIcon from "@material-ui/icons/Edit";
+
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import Title from "../../components/Title";
+
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+import QuickMessageDialog from "../../components/QuickMessageDialog";
+import ConfirmationModal from "../../components/ConfirmationModal";
+import toastError from "../../errors/toastError";
+import { Grid } from "@material-ui/core";
+import { isArray } from "lodash";
+import { SocketContext } from "../../context/Socket/SocketContext";
+import { AuthContext } from "../../context/Auth/AuthContext";
+
+
+const reducer = (state, action) => {
+  if (action.type === "LOAD_QUICKMESSAGES") {
+    //console.log("aqui");
+    //console.log(action);
+    //console.log(action.payload);
+    const quickmessages = action.payload;
+    const newQuickmessages = [];
+    //console.log(newQuickmessages);
+
+    if (isArray(quickmessages)) {
+      quickmessages.forEach((quickemessage) => {
+        const quickemessageIndex = state.findIndex(
+          (u) => u.id === quickemessage.id
+        );
+        if (quickemessageIndex !== -1) {
+          state[quickemessageIndex] = quickemessage;
+        } else {
+          newQuickmessages.push(quickemessage);
+        }
+      });
+    }
+
+    return [...state, ...newQuickmessages];
+  }
+
+  if (action.type === "UPDATE_QUICKMESSAGES") {
+    const quickemessage = action.payload;
+    const quickemessageIndex = state.findIndex((u) => u.id === quickemessage.id);
+
+    if (quickemessageIndex !== -1) {
+      state[quickemessageIndex] = quickemessage;
+      return [...state];
+    } else {
+      return [quickemessage, ...state];
+    }
+  }
+
+  if (action.type === "DELETE_QUICKMESSAGE") {
+    const quickemessageId = action.payload;
+
+    const quickemessageIndex = state.findIndex((u) => u.id === quickemessageId);
+    if (quickemessageIndex !== -1) {
+      state.splice(quickemessageIndex, 1);
+    }
+    return [...state];
+  }
+
+  if (action.type === "RESET") {
+    return [];
+  }
+};
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+}));
+
+const Quickemessages = () => {
+  const classes = useStyles();
+
+  const [loading, setLoading] = useState(false);
+  const [pageNumber, setPageNumber] = useState(1);
+  const [hasMore, setHasMore] = useState(false);
+  const [selectedQuickemessage, setSelectedQuickemessage] = useState(null);
+  const [deletingQuickemessage, setDeletingQuickemessage] = useState(null);
+  const [quickemessageModalOpen, setQuickMessageDialogOpen] = useState(false);
+  const [confirmModalOpen, setConfirmModalOpen] = useState(false);
+  const [searchParam, setSearchParam] = useState("");
+  const [quickemessages, dispatch] = useReducer(reducer, []);
+  const { user } = useContext(AuthContext);
+  const { profile } = user;
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+  }, [searchParam]);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      fetchQuickemessages();
+    }, 500);
+    return () => clearTimeout(delayDebounceFn);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [searchParam, pageNumber]);
+
+  useEffect(() => {
+    const companyId = user.companyId;
+    const socket = socketManager.getSocket(companyId);
+
+    socket.on(`company${companyId}-quickemessage`, (data) => {
+      if (data.action === "update" || data.action === "create") {
+        dispatch({ type: "UPDATE_QUICKMESSAGES", payload: data.record });
+      }
+      if (data.action === "delete") {
+        dispatch({ type: "DELETE_QUICKMESSAGE", payload: +data.id });
+      }
+    });
+    return () => {
+      socket.disconnect();
+    };
+  }, [socketManager, user.companyId]);
+
+  const fetchQuickemessages = async () => {
+    try {
+      const companyId = user.companyId;
+      //const searchParam = ({ companyId, userId: user.id });
+      const { data } = await api.get("/quick-messages", {
+        params: { searchParam, pageNumber, userId: user.id },
+      });
+
+      dispatch({ type: "LOAD_QUICKMESSAGES", payload: data.records });
+      setHasMore(data.hasMore);
+      setLoading(false);
+    } catch (err) {
+      toastError(err);
+    }
+  };
+
+  const handleOpenQuickMessageDialog = () => {
+    setSelectedQuickemessage(null);
+    setQuickMessageDialogOpen(true);
+  };
+
+  const handleCloseQuickMessageDialog = () => {
+    setSelectedQuickemessage(null);
+    setQuickMessageDialogOpen(false);
+    //window.location.reload();
+    fetchQuickemessages();
+  };
+
+  const handleSearch = (event) => {
+    setSearchParam(event.target.value.toLowerCase());
+  };
+
+  const handleEditQuickemessage = (quickemessage) => {
+    //console.log(quickemessage);
+    setSelectedQuickemessage(quickemessage);
+    setQuickMessageDialogOpen(true);
+  };
+
+  const handleDeleteQuickemessage = async (quickemessageId) => {
+    try {
+      await api.delete(`/quick-messages/${quickemessageId}`);
+      toast.success(i18n.t("quickemessages.toasts.deleted"));
+    } catch (err) {
+      toastError(err);
+    }
+    setDeletingQuickemessage(null);
+    setSearchParam("");
+    setPageNumber(1);
+    fetchQuickemessages();
+    dispatch({ type: "RESET" });
+
+  };
+
+  const loadMore = () => {
+    setPageNumber((prevState) => prevState + 1);
+  };
+
+  const handleScroll = (e) => {
+    if (!hasMore || loading) return;
+    const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
+    if (scrollHeight - (scrollTop + 100) < clientHeight) {
+      loadMore();
+    }
+  };
+
+  return (
+    <MainContainer>
+      <ConfirmationModal
+        title={deletingQuickemessage && `${i18n.t("quickMessages.confirmationModal.deleteTitle")} ${deletingQuickemessage.shortcode}?`}
+        open={confirmModalOpen}
+        onClose={setConfirmModalOpen}
+        onConfirm={() => handleDeleteQuickemessage(deletingQuickemessage.id)}
+      >
+        {i18n.t("quickMessages.confirmationModal.deleteMessage")}
+      </ConfirmationModal>
+      <QuickMessageDialog
+        resetPagination={() => {
+          setPageNumber(1);
+          fetchQuickemessages();
+        }}
+        open={quickemessageModalOpen}
+        onClose={handleCloseQuickMessageDialog}
+        aria-labelledby="form-dialog-title"
+        quickemessageId={selectedQuickemessage && selectedQuickemessage.id}
+      />
+      <MainHeader>
+        <Grid style={{ width: "99.6%" }} container>
+          <Grid xs={12} sm={8} item>
+            <Title>{i18n.t("quickMessages.title")}</Title>
+          </Grid>
+          <Grid xs={12} sm={4} item>
+            <Grid spacing={2} container>
+              <Grid xs={6} sm={6} item>
+                <TextField
+                  fullWidth
+                  placeholder={i18n.t("quickMessages.searchPlaceholder")}
+                  type="search"
+                  value={searchParam}
+                  onChange={handleSearch}
+                  InputProps={{
+                    startAdornment: (
+                      <InputAdornment position="start">
+                        <SearchIcon style={{ color: "gray" }} />
+                      </InputAdornment>
+                    ),
+                  }}
+                />
+              </Grid>
+              <Grid xs={6} sm={6} item>
+                <Button
+                  fullWidth
+                  variant="contained"
+                  onClick={handleOpenQuickMessageDialog}
+                  color="primary"
+                >
+                  {i18n.t("quickMessages.buttons.add")}
+                </Button>
+              </Grid>
+            </Grid>
+          </Grid>
+        </Grid>
+      </MainHeader>
+      <Paper
+        className={classes.mainPaper}
+        variant="outlined"
+        onScroll={handleScroll}
+      >
+        <Table size="small">
+          <TableHead>
+            <TableRow>
+              <TableCell align="center">
+                {i18n.t("quickMessages.table.shortcode")}
+              </TableCell>
+
+              <TableCell align="center">
+                {i18n.t("quickMessages.table.mediaName")}
+              </TableCell>        
+              <TableCell align="center">
+                {i18n.t("quickMessages.table.actions")}
+              </TableCell>
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            <>
+              {quickemessages.map((quickemessage) => (
+                <TableRow key={quickemessage.id}>
+                  <TableCell align="center">{quickemessage.shortcode}</TableCell>
+
+                  <TableCell align="center">
+                    {quickemessage.mediaName ?? i18n.t("quickMessages.noAttachment")}
+                  </TableCell>
+                  <TableCell align="center">
+                    <IconButton
+                      size="small"
+                      onClick={() => handleEditQuickemessage(quickemessage)}
+                    >
+                      <EditIcon />
+                    </IconButton>
+
+
+                    <IconButton
+                      size="small"
+                      onClick={(e) => {
+                        setConfirmModalOpen(true);
+                        setDeletingQuickemessage(quickemessage);
+                      }}
+                    >
+                      <DeleteOutlineIcon />
+                    </IconButton>
+                  </TableCell>
+                </TableRow>
+              ))}
+              {loading && <TableRowSkeleton columns={5} />}
+            </>
+          </TableBody>
+        </Table>
+      </Paper>
+    </MainContainer>
+  );
+};
+
+export default Quickemessages;

binární
frontend/src/pages/Schedules.bkp


+ 22 - 0
frontend/src/pages/Schedules/Schedules.css

@@ -0,0 +1,22 @@
+/* Adicione as regras CSS para ocultar os ícones */
+.event-container {
+    position: relative;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  
+  .delete-icon,
+  .edit-icon {
+    opacity: 0;
+    transition: opacity 0.3s;
+    margin-left: 5px; /* Adicione margem à esquerda */
+    z-index: 1; /* Garante que os ícones apareçam sobre o evento */
+  }
+  
+  .event-container:hover .delete-icon,
+  .event-container:hover .edit-icon {
+    opacity: 1;
+  }
+  

+ 325 - 0
frontend/src/pages/Schedules/index.js

@@ -0,0 +1,325 @@
+import React, { useState, useEffect, useReducer, useCallback, useContext } from "react";
+import { toast } from "react-toastify";
+import { useHistory } from "react-router-dom";
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+import Button from "@material-ui/core/Button";
+import TextField from "@material-ui/core/TextField";
+import InputAdornment from "@material-ui/core/InputAdornment";
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import Title from "../../components/Title";
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
+import ScheduleModal from "../../components/ScheduleModal";
+import ConfirmationModal from "../../components/ConfirmationModal";
+import toastError from "../../errors/toastError";
+import moment from "moment";
+import { SocketContext } from "../../context/Socket/SocketContext";
+import { AuthContext } from "../../context/Auth/AuthContext";
+import usePlans from "../../hooks/usePlans";
+import { Calendar, momentLocalizer } from "react-big-calendar";
+import "moment/locale/pt-br";
+import "react-big-calendar/lib/css/react-big-calendar.css";
+import SearchIcon from "@material-ui/icons/Search";
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import EditIcon from "@material-ui/icons/Edit";
+
+import "./Schedules.css"; // Importe o arquivo CSS
+import { createMomentLocalizer } from "../../translate/calendar-locale";
+
+// Defina a função getUrlParam antes de usá-la
+function getUrlParam(paramName) {
+  const searchParams = new URLSearchParams(window.location.search);
+  return searchParams.get(paramName);
+}
+
+const eventTitleStyle = {
+  fontSize: "14px", // Defina um tamanho de fonte menor
+  overflow: "hidden", // Oculte qualquer conteúdo excedente
+  whiteSpace: "nowrap", // Evite a quebra de linha do texto
+  textOverflow: "ellipsis", // Exiba "..." se o texto for muito longo
+};
+
+var defaultMessages = {
+  date: i18n.t("schedules.messages.date"),
+  time: i18n.t("schedules.messages.time"),
+  event: i18n.t("schedules.messages.event"),
+  allDay: i18n.t("schedules.messages.allDay"),
+  week: i18n.t("schedules.messages.week"),
+  work_week: i18n.t("schedules.messages.work_week"),
+  day: i18n.t("schedules.messages.day"),
+  month: i18n.t("schedules.messages.month"),
+  previous: i18n.t("schedules.messages.previous"),
+  next: i18n.t("schedules.messages.next"),
+  yesterday: i18n.t("schedules.messages.yesterday"),
+  tomorrow: i18n.t("schedules.messages.tomorrow"),
+  today: i18n.t("schedules.messages.today"),
+  agenda: i18n.t("schedules.messages.agenda"),
+  noEventsInRange: i18n.t("schedules.messages.noEventsInRange"),
+  showMore: function showMore(total) {
+    return "+" + total + " " + i18n.t("schedules.messages.showMore");
+  }
+};
+
+const reducer = (state, action) => {
+  if (action.type === "LOAD_SCHEDULES") {
+    return [...state, ...action.payload];
+  }
+
+  if (action.type === "UPDATE_SCHEDULES") {
+    const schedule = action.payload;
+    const scheduleIndex = state.findIndex((s) => s.id === schedule.id);
+
+    if (scheduleIndex !== -1) {
+      state[scheduleIndex] = schedule;
+      return [...state];
+    } else {
+      return [schedule, ...state];
+    }
+  }
+
+  if (action.type === "DELETE_SCHEDULE") {
+    const scheduleId = action.payload;
+    return state.filter((s) => s.id !== scheduleId);
+  }
+
+  if (action.type === "RESET") {
+    return [];
+  }
+
+  return state;
+};
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+}));
+
+const Schedules = () => {
+  const localizer = createMomentLocalizer();
+  const classes = useStyles();
+  const history = useHistory();
+
+  const { user } = useContext(AuthContext);
+
+  const [loading, setLoading] = useState(false);
+  const [pageNumber, setPageNumber] = useState(1);
+  const [hasMore, setHasMore] = useState(false);
+  const [selectedSchedule, setSelectedSchedule] = useState(null);
+  const [deletingSchedule, setDeletingSchedule] = useState(null);
+  const [confirmModalOpen, setConfirmModalOpen] = useState(false);
+  const [searchParam, setSearchParam] = useState("");
+  const [schedules, dispatch] = useReducer(reducer, []);
+  const [scheduleModalOpen, setScheduleModalOpen] = useState(false);
+  const [contactId, setContactId] = useState(+getUrlParam("contactId"));
+
+
+  const fetchSchedules = useCallback(async () => {
+    try {
+      const { data } = await api.get("/schedules/", {
+        params: { searchParam, pageNumber },
+      });
+
+      dispatch({ type: "LOAD_SCHEDULES", payload: data.schedules });
+      setHasMore(data.hasMore);
+      setLoading(false);
+    } catch (err) {
+      toastError(err);
+    }
+  }, [searchParam, pageNumber]);
+
+  const handleOpenScheduleModalFromContactId = useCallback(() => {
+    if (contactId) {
+      handleOpenScheduleModal();
+    }
+  }, [contactId]);
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+  }, [searchParam]);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      fetchSchedules();
+    }, 500);
+    return () => clearTimeout(delayDebounceFn);
+  }, [
+    searchParam,
+    pageNumber,
+    contactId,
+    fetchSchedules,
+    handleOpenScheduleModalFromContactId,
+  ]);
+
+  useEffect(() => {
+    handleOpenScheduleModalFromContactId();
+    const socket = socketManager.getSocket(user.companyId);
+
+    socket.on(`company${user.companyId}-schedule`, (data) => {
+      if (data.action === "update" || data.action === "create") {
+        dispatch({ type: "UPDATE_SCHEDULES", payload: data.schedule });
+      }
+
+      if (data.action === "delete") {
+        dispatch({ type: "DELETE_SCHEDULE", payload: +data.scheduleId });
+      }
+    });
+
+    return () => {
+      socket.disconnect();
+    };
+  }, [handleOpenScheduleModalFromContactId, socketManager, user]);
+
+  const cleanContact = () => {
+    setContactId("");
+  };
+
+  const handleOpenScheduleModal = () => {
+    setSelectedSchedule(null);
+    setScheduleModalOpen(true);
+  };
+
+  const handleCloseScheduleModal = () => {
+    setSelectedSchedule(null);
+    setScheduleModalOpen(false);
+  };
+
+  const handleSearch = (event) => {
+    setSearchParam(event.target.value.toLowerCase());
+  };
+
+  const handleEditSchedule = (schedule) => {
+    setSelectedSchedule(schedule);
+    setScheduleModalOpen(true);
+  };
+
+  const handleDeleteSchedule = async (scheduleId) => {
+    try {
+      await api.delete(`/schedules/${scheduleId}`);
+      toast.success(i18n.t("schedules.toasts.deleted"));
+    } catch (err) {
+      toastError(err);
+    }
+    setDeletingSchedule(null);
+    setSearchParam("");
+    setPageNumber(1);
+
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+    await fetchSchedules();
+  };
+
+  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 truncate = (str, len) => {
+    if (str.length > len) {
+      return str.substring(0, len) + "...";
+    }
+    return str;
+  };
+
+  return (
+    <MainContainer>
+      <ConfirmationModal
+        title={
+          deletingSchedule &&
+          `${i18n.t("schedules.confirmationModal.deleteTitle")}`
+        }
+        open={confirmModalOpen}
+        onClose={() => setConfirmModalOpen(false)}
+        onConfirm={() => handleDeleteSchedule(deletingSchedule.id)}
+      >
+        {i18n.t("schedules.confirmationModal.deleteMessage")}
+      </ConfirmationModal>
+      <ScheduleModal
+        open={scheduleModalOpen}
+        onClose={handleCloseScheduleModal}
+        reload={fetchSchedules}
+        aria-labelledby="form-dialog-title"
+        scheduleId={selectedSchedule && selectedSchedule.id}
+        contactId={contactId}
+        cleanContact={cleanContact}
+      />
+      <MainHeader>
+        <Title>{i18n.t("schedules.title")} ({schedules.length})</Title>
+        <MainHeaderButtonsWrapper>
+          <TextField
+            placeholder={i18n.t("contacts.searchPlaceholder")}
+            type="search"
+            value={searchParam}
+            onChange={handleSearch}
+            InputProps={{
+              startAdornment: (
+                <InputAdornment position="start">
+                  <SearchIcon style={{ color: "gray" }} />
+                </InputAdornment>
+              ),
+            }}
+          />
+          <Button
+            variant="contained"
+            color="primary"
+            onClick={handleOpenScheduleModal}
+          >
+            {i18n.t("schedules.buttons.add")}
+          </Button>
+        </MainHeaderButtonsWrapper>
+      </MainHeader>
+      <Paper className={classes.mainPaper} variant="outlined" onScroll={handleScroll}>
+        <Calendar
+          messages={defaultMessages}
+          formats={{
+          agendaDateFormat: "DD/MM ddd",
+          weekdayFormat: "dddd"
+      }}
+          localizer={localizer}
+          events={schedules.map((schedule) => ({
+            title: (
+              <div className="event-container">
+                <div style={eventTitleStyle}>{schedule.contact.name}</div>
+                <DeleteOutlineIcon
+                  onClick={() => handleDeleteSchedule(schedule.id)}
+                  className="delete-icon"
+                />
+                <EditIcon
+                  onClick={() => {
+                    handleEditSchedule(schedule);
+                    setScheduleModalOpen(true);
+                  }}
+                  className="edit-icon"
+                />
+              </div>
+            ),
+            start: new Date(schedule.sendAt),
+            end: new Date(schedule.sendAt),
+          }))}
+          startAccessor="start"
+          endAccessor="end"
+          style={{ height: 500 }}
+        />
+      </Paper>
+    </MainContainer>
+  );
+};
+
+export default Schedules;

+ 129 - 0
frontend/src/pages/Settings/index.js

@@ -0,0 +1,129 @@
+import React, { useState, useEffect, useContext } from "react";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+import Typography from "@material-ui/core/Typography";
+import Container from "@material-ui/core/Container";
+import Select from "@material-ui/core/Select";
+import { toast } from "react-toastify";
+
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n.js";
+import toastError from "../../errors/toastError";
+import { SocketContext } from "../../context/Socket/SocketContext";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    display: "flex",
+    alignItems: "center",
+    padding: theme.spacing(4),
+  },
+
+  paper: {
+    padding: theme.spacing(2),
+    display: "flex",
+    alignItems: "center",
+  },
+
+  settingOption: {
+    marginLeft: "auto",
+  },
+  margin: {
+    margin: theme.spacing(1),
+  },
+}));
+
+const Settings = () => {
+  const classes = useStyles();
+
+  const [settings, setSettings] = useState([]);
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    const fetchSession = async () => {
+      try {
+        const { data } = await api.get("/settings");
+        setSettings(data);
+      } catch (err) {
+        toastError(err);
+      }
+    };
+    fetchSession();
+  }, []);
+
+  useEffect(() => {
+    const companyId = localStorage.getItem("companyId");
+    const socket = socketManager.getSocket(companyId);
+
+    socket.on(`company-${companyId}-settings`, (data) => {
+      if (data.action === "update") {
+        setSettings((prevState) => {
+          const aux = [...prevState];
+          const settingIndex = aux.findIndex((s) => s.key === data.setting.key);
+          aux[settingIndex].value = data.setting.value;
+          return aux;
+        });
+      }
+    });
+
+    return () => {
+      socket.disconnect();
+    };
+  }, [socketManager]);
+
+  const handleChangeSetting = async (e) => {
+    const selectedValue = e.target.value;
+    const settingKey = e.target.name;
+
+    try {
+      await api.put(`/settings/${settingKey}`, {
+        value: selectedValue,
+      });
+      toast.success(i18n.t("settings.success"));
+    } catch (err) {
+      toastError(err);
+    }
+  };
+
+  const getSettingValue = (key) => {
+    const { value } = settings.find((s) => s.key === key);
+    return value;
+  };
+
+  return (
+    <div className={classes.root}>
+      <Container className={classes.container} maxWidth="sm">
+        <Typography variant="body2" gutterBottom>
+          {i18n.t("settings.title")}
+        </Typography>
+        <Paper className={classes.paper}>
+          <Typography variant="body1">
+            {i18n.t("settings.settings.userCreation.name")}
+          </Typography>
+          <Select
+            margin="dense"
+            variant="outlined"
+            native
+            id="userCreation-setting"
+            name="userCreation"
+            value={
+              settings && settings.length > 0 && getSettingValue("userCreation")
+            }
+            className={classes.settingOption}
+            onChange={handleChangeSetting}
+          >
+            <option value="enabled">
+              {i18n.t("settings.settings.userCreation.options.enabled")}
+            </option>
+            <option value="disabled">
+              {i18n.t("settings.settings.userCreation.options.disabled")}
+            </option>
+          </Select>
+        </Paper>
+      </Container>
+    </div>
+  );
+};
+
+export default Settings;

+ 235 - 0
frontend/src/pages/SettingsCustom/index.js

@@ -0,0 +1,235 @@
+import React, { useState, useEffect } from "react";
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import Title from "../../components/Title";
+import { makeStyles, Paper, Tabs, Tab } from "@material-ui/core";
+
+import TabPanel from "../../components/TabPanel";
+
+import SchedulesForm from "../../components/SchedulesForm";
+import CompaniesManager from "../../components/CompaniesManager";
+import PlansManager from "../../components/PlansManager";
+import HelpsManager from "../../components/HelpsManager";
+import Options from "../../components/Settings/Options";
+
+import { i18n } from "../../translate/i18n.js";
+import { toast } from "react-toastify";
+
+import useCompanies from "../../hooks/useCompanies";
+import useAuth from "../../hooks/useAuth.js";
+import useSettings from "../../hooks/useSettings";
+
+import OnlyForSuperUser from "../../components/OnlyForSuperUser";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    flex: 1,
+    backgroundColor: theme.palette.background.paper,
+  },
+  mainPaper: {
+    ...theme.scrollbarStyles,
+    overflowY: "scroll",
+    flex: 1,
+  },
+  tab: {
+    backgroundColor: theme.palette.options,
+    borderRadius: 4,
+  },
+  paper: {
+    ...theme.scrollbarStyles,
+    overflowY: "scroll",
+    padding: theme.spacing(2),
+    display: "flex",
+    alignItems: "center",
+    width: "100%",
+  },
+  container: {
+    width: "100%",
+    maxHeight: "100%",
+  },
+  control: {
+    padding: theme.spacing(1),
+  },
+  textfield: {
+    width: "100%",
+  },
+}));
+
+const SettingsCustom = () => {
+  const classes = useStyles();
+  const [tab, setTab] = useState("options");
+  const [schedules, setSchedules] = useState([]);
+  const [company, setCompany] = useState({});
+  const [loading, setLoading] = useState(false);
+  const [currentUser, setCurrentUser] = useState({});
+  const [settings, setSettings] = useState({});
+  const [schedulesEnabled, setSchedulesEnabled] = useState(false);
+
+  const { getCurrentUserInfo } = useAuth();
+  const { find, updateSchedules } = useCompanies();
+  const { getAll: getAllSettings } = useSettings();
+
+  useEffect(() => {
+    async function findData() {
+      setLoading(true);
+      try {
+        const companyId = localStorage.getItem("companyId");
+        const company = await find(companyId);
+        const settingList = await getAllSettings();
+        setCompany(company);
+        setSchedules(company.schedules);
+        setSettings(settingList);
+
+        if (Array.isArray(settingList)) {
+          const scheduleType = settingList.find(
+            (d) => d.key === "scheduleType"
+          );
+          if (scheduleType) {
+            setSchedulesEnabled(scheduleType.value === "company");
+          }
+        }
+
+        const user = await getCurrentUserInfo();
+        setCurrentUser(user);
+      } catch (e) {
+        toast.error(e);
+      }
+      setLoading(false);
+    }
+    findData();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  const handleTabChange = (event, newValue) => {
+      async function findData() {
+        setLoading(true);
+        try {
+          const companyId = localStorage.getItem("companyId");
+          const company = await find(companyId);
+          const settingList = await getAllSettings();
+          setCompany(company);
+          setSchedules(company.schedules);
+          setSettings(settingList);
+  
+          if (Array.isArray(settingList)) {
+            const scheduleType = settingList.find(
+              (d) => d.key === "scheduleType"
+            );
+            if (scheduleType) {
+              setSchedulesEnabled(scheduleType.value === "company");
+            }
+          }
+  
+          const user = await getCurrentUserInfo();
+          setCurrentUser(user);
+        } catch (e) {
+          toast.error(e);
+        }
+        setLoading(false);
+      }
+      findData();
+      // eslint-disable-next-line react-hooks/exhaustive-deps
+
+    setTab(newValue);
+  };
+
+  const handleSubmitSchedules = async (data) => {
+    setLoading(true);
+    try {
+      setSchedules(data);
+      await updateSchedules({ id: company.id, schedules: data });
+      toast.success(i18n.t("settings.schedulesUpdated"));
+    } catch (e) {
+      toast.error(e);
+    }
+    setLoading(false);
+  };
+
+  const isSuper = () => {
+    return currentUser.super;
+  };
+
+  return (
+    <MainContainer className={classes.root}>
+      <MainHeader>
+        <Title>{i18n.t("settings.title")}</Title>
+      </MainHeader>
+      <Paper className={classes.mainPaper} elevation={1}>
+        <Tabs
+          value={tab}
+          indicatorColor="primary"
+          textColor="primary"
+          scrollButtons="on"
+          variant="scrollable"
+          onChange={handleTabChange}
+          className={classes.tab}
+        >
+          <Tab label={i18n.t("settings.tabs.options")} value={"options"} />
+          {schedulesEnabled && <Tab label={i18n.t("settings.tabs.schedules")} value={"schedules"} />}
+          {isSuper() ? <Tab label={i18n.t("settings.tabs.companies")} value={"companies"} /> : null}
+          {isSuper() ? <Tab label={i18n.t("settings.tabs.plans")} value={"plans"} /> : null}
+          {isSuper() ? <Tab label={i18n.t("settings.tabs.helps")} value={"helps"} /> : null}
+        </Tabs>
+        <Paper className={classes.paper} elevation={0}>
+          <TabPanel
+            className={classes.container}
+            value={tab}
+            name={"schedules"}
+          >
+            <SchedulesForm
+              loading={loading}
+              onSubmit={handleSubmitSchedules}
+              initialValues={schedules}
+            />
+          </TabPanel>
+          <OnlyForSuperUser
+            user={currentUser}
+            yes={() => (
+              <TabPanel
+                className={classes.container}
+                value={tab}
+                name={"companies"}
+              >
+                <CompaniesManager />
+              </TabPanel>
+            )}
+          />
+          <OnlyForSuperUser
+            user={currentUser}
+            yes={() => (
+              <TabPanel
+                className={classes.container}
+                value={tab}
+                name={"plans"}
+              >
+                <PlansManager />
+              </TabPanel>
+            )}
+          />
+          <OnlyForSuperUser
+            user={currentUser}
+            yes={() => (
+              <TabPanel
+                className={classes.container}
+                value={tab}
+                name={"helps"}
+              >
+                <HelpsManager />
+              </TabPanel>
+            )}
+          />
+          <TabPanel className={classes.container} value={tab} name={"options"}>
+            <Options
+              settings={settings}
+              scheduleTypeChanged={(value) =>
+                setSchedulesEnabled(value === "company")
+              }
+            />
+          </TabPanel>
+        </Paper>
+      </Paper>
+    </MainContainer>
+  );
+};
+
+export default SettingsCustom;

+ 259 - 0
frontend/src/pages/Signup/index.js

@@ -0,0 +1,259 @@
+import React, { useState, useEffect } from "react";
+import qs from 'query-string'
+
+import * as Yup from "yup";
+import { useHistory } from "react-router-dom";
+import { Link as RouterLink } from "react-router-dom";
+import { toast } from "react-toastify";
+import { Formik, Form, Field } from "formik";
+import usePlans from "../../hooks/usePlans";
+import Avatar from "@material-ui/core/Avatar";
+import Button from "@material-ui/core/Button";
+import CssBaseline from "@material-ui/core/CssBaseline";
+import TextField from "@material-ui/core/TextField";
+import Link from "@material-ui/core/Link";
+import Grid from "@material-ui/core/Grid";
+import Box from "@material-ui/core/Box";
+import InputMask from 'react-input-mask';
+import {
+	FormControl,
+	InputLabel,
+	MenuItem,
+	Select,
+} from "@material-ui/core";
+import LockOutlinedIcon from "@material-ui/icons/LockOutlined";
+import Typography from "@material-ui/core/Typography";
+import { makeStyles } from "@material-ui/core/styles";
+import Container from "@material-ui/core/Container";
+import logo from "../../assets/logo.png";
+import { i18n } from "../../translate/i18n";
+
+import { openApi } from "../../services/api";
+import toastError from "../../errors/toastError";
+import moment from "moment";
+const Copyright = () => {
+	return (
+		<Typography variant="body2" color="textSecondary" align="center">
+			{"Copyright © "}
+			<Link color="inherit" href="#">
+				PLW
+			</Link>{" "}
+		   {new Date().getFullYear()}
+			{"."}
+		</Typography>
+	);
+};
+
+const useStyles = makeStyles(theme => ({
+	paper: {
+		marginTop: theme.spacing(8),
+		display: "flex",
+		flexDirection: "column",
+		alignItems: "center",
+	},
+	avatar: {
+		margin: theme.spacing(1),
+		backgroundColor: theme.palette.secondary.main,
+	},
+	form: {
+		width: "100%",
+		marginTop: theme.spacing(3),
+	},
+	submit: {
+		margin: theme.spacing(3, 0, 2),
+	},
+}));
+
+const UserSchema = Yup.object().shape({
+	name: Yup.string()
+		.min(2, i18n.t("signup.formErrors.name.short"))
+		.max(50, i18n.t("signup.formErrors.name.long"))
+		.required(i18n.t("signup.formErrors.name.required")),
+	password: Yup.string().min(5, i18n.t("signup.formErrors.password.short")).max(50, i18n.t("signup.formErrors.password.long")),
+	email: Yup.string().email(i18n.t("signup.formErrors.email.invalid")).required(i18n.t("signup.formErrors.email.required")),
+});
+
+const SignUp = () => {
+	const classes = useStyles();
+	const history = useHistory();
+	let companyId = null
+
+	const params = qs.parse(window.location.search)
+	if (params.companyId !== undefined) {
+		companyId = params.companyId
+	}
+
+	const initialState = { name: "", email: "", phone: "", password: "", planId: "", };
+
+	const [user] = useState(initialState);
+	const dueDate = moment().add(3, "day").format();
+	const handleSignUp = async values => {
+		Object.assign(values, { recurrence: "MENSAL" });
+		Object.assign(values, { dueDate: dueDate });
+		Object.assign(values, { status: "t" });
+		Object.assign(values, { campaignsEnabled: true });
+		try {
+			await openApi.post("/companies/cadastro", values);
+			toast.success(i18n.t("signup.toasts.success"));
+			history.push("/login");
+		} catch (err) {
+			console.log(err);
+			toastError(err);
+		}
+	};
+
+	const [plans, setPlans] = useState([]);
+	const { list: listPlans } = usePlans();
+
+	useEffect(() => {
+		async function fetchData() {
+			const list = await listPlans();
+			setPlans(list);
+		}
+		fetchData();
+	}, []);
+
+
+	return (
+		<Container component="main" maxWidth="xs">
+			<CssBaseline />
+			<div className={classes.paper}>
+				<div>
+					<center><img style={{ margin: "0 auto", width: "70%" }} src={logo} alt="Whats" /></center>
+				</div>
+				{/*<Typography component="h1" variant="h5">
+					{i18n.t("signup.title")}
+				</Typography>*/}
+				{/* <form className={classes.form} noValidate onSubmit={handleSignUp}> */}
+				<Formik
+					initialValues={user}
+					enableReinitialize={true}
+					validationSchema={UserSchema}
+					onSubmit={(values, actions) => {
+						setTimeout(() => {
+							handleSignUp(values);
+							actions.setSubmitting(false);
+						}, 400);
+					}}
+				>
+					{({ touched, errors, isSubmitting }) => (
+						<Form className={classes.form}>
+							<Grid container spacing={2}>
+								<Grid item xs={12}>
+									<Field
+										as={TextField}
+										autoComplete="name"
+										name="name"
+										error={touched.name && Boolean(errors.name)}
+										helperText={touched.name && errors.name}
+										variant="outlined"
+										fullWidth
+										id="name"
+										label={i18n.t("signup.form.name")}
+									/>
+								</Grid>
+
+								<Grid item xs={12}>
+									<Field
+										as={TextField}
+										variant="outlined"
+										fullWidth
+										id="email"
+										label={i18n.t("signup.form.email")}
+										name="email"
+										error={touched.email && Boolean(errors.email)}
+										helperText={touched.email && errors.email}
+										autoComplete="email"
+										required
+									/>
+								</Grid>
+								
+							<Grid item xs={12}>
+								<Field
+									as={InputMask}
+									mask="(99) 99999-9999"
+									variant="outlined"
+									fullWidth
+									id="phone"
+									name="phone"
+									error={touched.phone && Boolean(errors.phone)}
+									helperText={touched.phone && errors.phone}
+									autoComplete="phone"
+									required
+								>
+									{({ field }) => (
+										<TextField
+											{...field}
+											variant="outlined"
+											fullWidth
+											label={i18n.t("signup.form.phone")}
+											inputProps={{ maxLength: 11 }} // Definindo o limite de caracteres
+										/>
+									)}
+								</Field>
+							</Grid>
+								<Grid item xs={12}>
+									<Field
+										as={TextField}
+										variant="outlined"
+										fullWidth
+										name="password"
+										error={touched.password && Boolean(errors.password)}
+										helperText={touched.password && errors.password}
+										label={i18n.t("signup.form.password")}
+										type="password"
+										id="password"
+										autoComplete="current-password"
+										required
+									/>
+								</Grid>
+								<Grid item xs={12}>
+									<InputLabel htmlFor="plan-selection">Plano</InputLabel>
+									<Field
+										as={Select}
+										variant="outlined"
+										fullWidth
+										id="plan-selection"
+										label={i18n.t("signup.form.plan")}
+										name="planId"
+										required
+									>
+										{plans.map((plan, key) => (
+											<MenuItem key={key} value={plan.id}>
+												{plan.name} - {i18n.t("signup.plan.attendant")}: {plan.users} - {i18n.t("signup.plan.whatsapp")}: {plan.connections} - {i18n.t("signup.plan.queues")}: {plan.queues} - R$ {plan.value}
+											</MenuItem>
+										))}
+									</Field>
+								</Grid>
+							</Grid>
+							<Button
+								type="submit"
+								fullWidth
+								variant="contained"
+								color="primary"
+								className={classes.submit}
+							>
+								{i18n.t("signup.buttons.submit")}
+							</Button>
+							<Grid container justify="flex-end">
+								<Grid item>
+									<Link
+										href="#"
+										variant="body2"
+										component={RouterLink}
+										to="/login"
+									>
+										{i18n.t("signup.buttons.login")}
+									</Link>
+								</Grid>
+							</Grid>
+						</Form>
+					)}
+				</Formik>
+			</div>
+			<Box mt={5}>{/* <Copyright /> */}</Box>
+		</Container>
+	);
+};
+
+export default SignUp;

+ 126 - 0
frontend/src/pages/Signup/style.css

@@ -0,0 +1,126 @@
+* {
+  padding: 0;
+  margin: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: "Poppins", sans-serif;
+  /* overflow: hidden; */
+}
+
+.geral-signup {
+  display: flex;
+}
+.img-logo-signup {
+  width: 350px;
+  height: 100px;
+}
+
+.register {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+}
+.paper {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  max-width: 500px;
+  /* max-height: 100vh; */
+}
+
+.container-signup {
+  align-items: center;
+  display: flex;
+  justify-content: center;
+  flex-direction: column;
+  width: 100%;
+
+  padding: 10px;
+  /* max-height: 100vh; */
+}
+.h4 {
+  caret-color: rgb(52, 137, 255);
+  color: rgb(52, 137, 255);
+  column-rule-color: rgb(52, 137, 255);
+  font-size: 34px;
+  line-height: 41.99px;
+  margin: 10px 0 25px;
+}
+.span {
+  font-size: 16px;
+  line-height: 28px;
+}
+
+.container-img-signup {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh;
+  width: 100%;
+}
+
+.img-signup {
+  background-image: url(../../assets/backgroundStep.png);
+  background-position: 50% 50%;
+  background-repeat: no-repeat;
+  background-size: cover;
+  position: fixed;
+  width: 700px;
+  height: 100vh;
+  object-fit: cover;
+}
+
+.p {
+  font-size: 12px;
+  color: #999;
+}
+.footer {
+  text-align: center;
+  margin-top: 30px;
+  margin-bottom: 20px;
+}
+.footer a {
+  text-decoration: none;
+  cursor: pointer;
+  color: #3489ff;
+}
+.footer a:hover {
+  text-decoration: underline;
+}
+
+@media screen and (max-width: 320px) {
+  .container-img-signup {
+    display: none;
+  }
+  .paper {
+    width: 300px;
+  }
+  .img-logo-signup {
+    width: 200px;
+    height: 70px;
+  }
+}
+@media screen and (max-width: 940px) {
+  .container-img-signup {
+    display: none;
+  }
+}
+
+@media screen and (max-width: 1024px) {
+  .paper {
+    max-width: 450px;
+    max-height: fit-content;
+  }
+  .img-signup {
+    width: 499px;
+  }
+}
+
+@media screen and (min-width: 1400px) {
+  .img-signup {
+    width: 50%;
+  }
+}

+ 140 - 0
frontend/src/pages/Subscription/index.js

@@ -0,0 +1,140 @@
+import React, { useState, useContext } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+
+import Paper from "@material-ui/core/Paper";
+import Button from "@material-ui/core/Button";
+import Grid from '@material-ui/core/Grid';
+import TextField from '@material-ui/core/TextField';
+
+import SubscriptionModal from "../../components/SubscriptionModal";
+import MainHeader from "../../components/MainHeader";
+import Title from "../../components/Title";
+import MainContainer from "../../components/MainContainer";
+
+import { AuthContext } from "../../context/Auth/AuthContext";
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+}));
+
+const _formatDate = (date) => {
+  const now = new Date();
+  const past = new Date(date);
+  const diff = Math.abs(now.getTime() - past.getTime());
+  const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
+
+  return days;
+}
+
+const Contacts = () => {
+  const classes = useStyles();
+  const { user } = useContext(AuthContext);
+
+  const [loading,] = useState(false);
+  const [, setPageNumber] = useState(1);
+  const [selectedContactId, setSelectedContactId] = useState(null);
+  const [contactModalOpen, setContactModalOpen] = useState(false);
+  const [hasMore,] = useState(false);
+
+
+  const handleOpenContactModal = () => {
+    setSelectedContactId(null);
+    setContactModalOpen(true);
+  };
+
+  const handleCloseContactModal = () => {
+    setSelectedContactId(null);
+    setContactModalOpen(false);
+  };
+
+  const loadMore = () => {
+    setPageNumber((prevState) => prevState + 1);
+  };
+
+  const handleScroll = (e) => {
+    if (!hasMore || loading) return;
+    const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
+    if (scrollHeight - (scrollTop + 100) < clientHeight) {
+      loadMore();
+    }
+  };
+
+  return (
+    <MainContainer className={classes.mainContainer}>
+      <SubscriptionModal
+        open={contactModalOpen}
+        onClose={handleCloseContactModal}
+        aria-labelledby="form-dialog-title"
+        contactId={selectedContactId}
+      ></SubscriptionModal>
+
+      <MainHeader>
+        <Title>{i18n.t("subscription.title")}</Title>
+      </MainHeader>
+      <Grid item xs={12} sm={4}>
+        <Paper
+          className={classes.mainPaper}
+          variant="outlined"
+          onScroll={handleScroll}
+        >
+
+          <div>
+            <TextField
+              id="outlined-full-width"
+              label={i18n.t("subscription.testPeriod")}
+              defaultValue={`${i18n.t("subscription.remainingTest")} ${_formatDate(user?.company?.trialExpiration)} ${i18n.t("subscription.remainingTest2")}`}
+              fullWidth
+              margin="normal"
+              InputLabelProps={{
+                shrink: true,
+              }}
+              InputProps={{
+                readOnly: true,
+              }}
+              variant="outlined"
+            />
+
+          </div>
+
+          <div>
+            <TextField
+              id="outlined-full-width"
+              label={i18n.t("subscription.chargeEmail")}
+              defaultValue={user?.company?.email}
+              fullWidth
+              margin="normal"
+              InputLabelProps={{
+                shrink: true,
+              }}
+              InputProps={{
+                readOnly: true,
+              }}
+              variant="outlined"
+            />
+
+          </div>
+
+          <div>
+            <Button
+              variant="contained"
+              color="primary"
+              onClick={handleOpenContactModal}
+              fullWidth
+            >
+              {i18n.t("subscription.signNow")}
+            </Button>
+          </div>
+
+        </Paper>
+      </Grid>
+    </MainContainer>
+  );
+};
+
+export default Contacts;

+ 304 - 0
frontend/src/pages/Tags/index.js

@@ -0,0 +1,304 @@
+import React, {
+  useState,
+  useEffect,
+  useReducer,
+  useCallback,
+  useContext,
+} from "react";
+import { toast } from "react-toastify";
+
+import { makeStyles } from "@material-ui/core/styles";
+import Paper from "@material-ui/core/Paper";
+import Button from "@material-ui/core/Button";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableHead from "@material-ui/core/TableHead";
+import TableRow from "@material-ui/core/TableRow";
+import IconButton from "@material-ui/core/IconButton";
+import SearchIcon from "@material-ui/icons/Search";
+import TextField from "@material-ui/core/TextField";
+import InputAdornment from "@material-ui/core/InputAdornment";
+
+import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
+import EditIcon from "@material-ui/icons/Edit";
+import MainContainer from "../../components/MainContainer";
+import MainHeader from "../../components/MainHeader";
+import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
+import Title from "../../components/Title";
+
+import api from "../../services/api";
+import { i18n } from "../../translate/i18n";
+import TableRowSkeleton from "../../components/TableRowSkeleton";
+import TagModal from "../../components/TagModal";
+import ConfirmationModal from "../../components/ConfirmationModal";
+import toastError from "../../errors/toastError";
+import { Chip } from "@material-ui/core";
+import { Tooltip } from "@material-ui/core";
+import { SocketContext } from "../../context/Socket/SocketContext";
+import { AuthContext } from "../../context/Auth/AuthContext";
+
+const reducer = (state, action) => {
+  if (action.type === "LOAD_TAGS") {
+    const tags = action.payload;
+    const newTags = [];
+
+    tags.forEach((tag) => {
+      const tagIndex = state.findIndex((s) => s.id === tag.id);
+      if (tagIndex !== -1) {
+        state[tagIndex] = tag;
+      } else {
+        newTags.push(tag);
+      }
+    });
+
+    return [...state, ...newTags];
+  }
+
+  if (action.type === "UPDATE_TAGS") {
+    const tag = action.payload;
+    const tagIndex = state.findIndex((s) => s.id === tag.id);
+
+    if (tagIndex !== -1) {
+      state[tagIndex] = tag;
+      return [...state];
+    } else {
+      return [tag, ...state];
+    }
+  }
+
+  if (action.type === "DELETE_TAG") {
+    const tagId = action.payload;
+
+    const tagIndex = state.findIndex((s) => s.id === tagId);
+    if (tagIndex !== -1) {
+      state.splice(tagIndex, 1);
+    }
+    return [...state];
+  }
+
+  if (action.type === "RESET") {
+    return [];
+  }
+};
+
+const useStyles = makeStyles((theme) => ({
+  mainPaper: {
+    flex: 1,
+    padding: theme.spacing(1),
+    overflowY: "scroll",
+    ...theme.scrollbarStyles,
+  },
+}));
+
+const Tags = () => {
+  const classes = useStyles();
+
+  const { user } = useContext(AuthContext);
+
+  const [loading, setLoading] = useState(false);
+  const [pageNumber, setPageNumber] = useState(1);
+  const [hasMore, setHasMore] = useState(false);
+  const [selectedTag, setSelectedTag] = useState(null);
+  const [deletingTag, setDeletingTag] = useState(null);
+  const [confirmModalOpen, setConfirmModalOpen] = useState(false);
+  const [searchParam, setSearchParam] = useState("");
+  const [tags, dispatch] = useReducer(reducer, []);
+  const [tagModalOpen, setTagModalOpen] = useState(false);
+
+  const fetchTags = useCallback(async () => {
+    try {
+      const { data } = await api.get("/tags/", {
+        params: { searchParam, pageNumber },
+      });
+      dispatch({ type: "LOAD_TAGS", payload: data.tags });
+      setHasMore(data.hasMore);
+      setLoading(false);
+    } catch (err) {
+      toastError(err);
+    }
+  }, [searchParam, pageNumber]);
+
+  const socketManager = useContext(SocketContext);
+
+  useEffect(() => {
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+  }, [searchParam]);
+
+  useEffect(() => {
+    setLoading(true);
+    const delayDebounceFn = setTimeout(() => {
+      fetchTags();
+    }, 500);
+    return () => clearTimeout(delayDebounceFn);
+  }, [searchParam, pageNumber, fetchTags]);
+
+  useEffect(() => {
+    const socket = socketManager.getSocket(user.companyId);
+
+    socket.on("user", (data) => {
+      if (data.action === "update" || data.action === "create") {
+        dispatch({ type: "UPDATE_TAGS", payload: data.tags });
+      }
+
+      if (data.action === "delete") {
+        dispatch({ type: "DELETE_USER", payload: +data.tagId });
+      }
+    });
+
+    return () => {
+      socket.disconnect();
+    };
+  }, [socketManager, user]);
+
+  const handleOpenTagModal = () => {
+    setSelectedTag(null);
+    setTagModalOpen(true);
+  };
+
+  const handleCloseTagModal = () => {
+    setSelectedTag(null);
+    setTagModalOpen(false);
+  };
+
+  const handleSearch = (event) => {
+    setSearchParam(event.target.value.toLowerCase());
+  };
+
+  const handleEditTag = (tag) => {
+    setSelectedTag(tag);
+    setTagModalOpen(true);
+  };
+
+  const handleDeleteTag = async (tagId) => {
+    try {
+      await api.delete(`/tags/${tagId}`);
+      toast.success(i18n.t("tags.toasts.deleted"));
+    } catch (err) {
+      toastError(err);
+    }
+    setDeletingTag(null);
+    setSearchParam("");
+    setPageNumber(1);
+
+    dispatch({ type: "RESET" });
+    setPageNumber(1);
+    await fetchTags();
+  };
+
+  const loadMore = () => {
+    setPageNumber((prevState) => prevState + 1);
+  };
+
+  const handleScroll = (e) => {
+    if (!hasMore || loading) return;
+    const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
+    if (scrollHeight - (scrollTop + 100) < clientHeight) {
+      loadMore();
+    }
+  };
+
+return (
+    <MainContainer>
+      <ConfirmationModal
+        title={deletingTag && `${i18n.t("tags.confirmationModal.deleteTitle")}`}
+        open={confirmModalOpen}
+        onClose={setConfirmModalOpen}
+        onConfirm={() => handleDeleteTag(deletingTag.id)}
+      >
+        {i18n.t("tags.confirmationModal.deleteMessage")}
+      </ConfirmationModal>
+      <TagModal
+        open={tagModalOpen}
+        onClose={handleCloseTagModal}
+        reload={fetchTags}
+        aria-labelledby="form-dialog-title"
+        tagId={selectedTag && selectedTag.id}
+      />
+      <MainHeader>
+        <Title>{i18n.t("tags.title")}</Title>
+        <MainHeaderButtonsWrapper>
+          <TextField
+            placeholder={i18n.t("contacts.searchPlaceholder")}
+            type="search"
+            value={searchParam}
+            onChange={handleSearch}
+            InputProps={{
+              startAdornment: (
+                <InputAdornment position="start">
+                  <SearchIcon style={{ color: "gray" }} />
+                </InputAdornment>
+              ),
+            }}
+          />
+          <Button
+            variant="contained"
+            color="primary"
+            onClick={handleOpenTagModal}
+          >
+            {i18n.t("tags.buttons.add")}
+          </Button>		  
+        </MainHeaderButtonsWrapper>
+      </MainHeader>
+      <Paper
+        className={classes.mainPaper}
+        variant="outlined"
+        onScroll={handleScroll}
+      >
+        <Table size="small">
+          <TableHead>
+            <TableRow>
+              <TableCell align="center">{i18n.t("tags.table.name")}</TableCell>
+              <TableCell align="center">
+                {i18n.t("tags.table.tickets")}
+              </TableCell>
+              <TableCell align="center">
+                {i18n.t("tags.table.actions")}
+              </TableCell>
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            <>
+              {tags.map((tag) => (
+                <TableRow key={tag.id}>
+                  <TableCell align="center">
+                    <Chip
+                      variant="outlined"
+                      style={{
+                        backgroundColor: tag.color,
+                        textShadow: "1px 1px 1px #000",
+                        color: "white",
+                      }}
+                      label={tag.name}
+                      size="small"
+                    />
+                  </TableCell>
+                  <TableCell align="center">{tag.ticketsCount}</TableCell>
+                  <TableCell align="center">
+                    <IconButton size="small" onClick={() => handleEditTag(tag)}>
+                      <EditIcon />
+                    </IconButton>
+
+                    <IconButton
+                      size="small"
+                      onClick={(e) => {
+                        setConfirmModalOpen(true);
+                        setDeletingTag(tag);
+                      }}
+                    >
+                      <DeleteOutlineIcon />
+                    </IconButton>
+                  </TableCell>
+                </TableRow>
+              ))}
+              {loading && <TableRowSkeleton columns={4} />}
+            </>
+          </TableBody>
+        </Table>
+      </Paper>
+    </MainContainer>
+  );
+};
+
+export default Tags;

+ 14 - 0
frontend/src/pages/TicketResponsiveContainer/index.js

@@ -0,0 +1,14 @@
+import React from "react";
+import withWidth, { isWidthUp } from '@material-ui/core/withWidth';
+
+import Tickets from "../TicketsCustom"
+import TicketAdvanced from "../TicketsAdvanced";
+
+function TicketResponsiveContainer (props) {
+    if (isWidthUp('md', props.width)) {
+        return <Tickets />;    
+    }
+    return <TicketAdvanced />
+}
+
+export default withWidth()(TicketResponsiveContainer);

+ 83 - 0
frontend/src/pages/Tickets/index.js

@@ -0,0 +1,83 @@
+import React from "react";
+import { useParams } from "react-router-dom";
+import Grid from "@material-ui/core/Grid";
+import Paper from "@material-ui/core/Paper";
+import { makeStyles } from "@material-ui/core/styles";
+
+import TicketsManager from "../../components/TicketsManager/";
+import Ticket from "../../components/Ticket/";
+
+import logo from "../../assets/logo.png";
+
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles(theme => ({
+	chatContainer: {
+		flex: 1,
+		// backgroundColor: "#eee",
+		padding: theme.spacing(4),
+		height: `calc(100% - 48px)`,
+		overflowY: "hidden",
+	},
+
+	chatPapper: {
+		// backgroundColor: "red",
+		display: "flex",
+		height: "100%",
+	},
+
+	contactsWrapper: {
+		display: "flex",
+		height: "100%",
+		flexDirection: "column",
+		overflowY: "hidden",
+	},
+	messagessWrapper: {
+		display: "flex",
+		height: "100%",
+		flexDirection: "column",
+	},
+	welcomeMsg: {
+		backgroundColor: theme.palette.boxticket, 
+		display: "flex",
+		justifyContent: "space-evenly",
+		alignItems: "center",
+		height: "100%",
+		textAlign: "center",
+	},
+}));
+
+const Chat = () => {
+	const classes = useStyles();
+	const { ticketId } = useParams();
+
+	return (
+		<div className={classes.chatContainer}>
+			<div className={classes.chatPapper}>
+				<Grid container spacing={0}>
+					<Grid item xs={4} className={classes.contactsWrapper}>
+						<TicketsManager />
+					</Grid>
+					<Grid item xs={8} className={classes.messagessWrapper}>
+						{ticketId ? (
+							<>
+								<Ticket />
+							</>
+						) : (
+							<Paper square variant="outlined" className={classes.welcomeMsg}>
+							
+							<div>
+							<center><img style={{ margin: "0 auto", width: "70%" }} src={logo} alt="logologin" /></center>
+							</div>
+							
+							{/*<span>{i18n.t("chat.noTicketMessage")}</span>*/}
+							</Paper>
+						)}
+					</Grid>
+				</Grid>
+			</div>
+		</div>
+	);
+};
+
+export default Chat;

+ 110 - 0
frontend/src/pages/TicketsAdvanced/index.js

@@ -0,0 +1,110 @@
+import React, { useState, useEffect, useContext } from "react";
+import { useParams } from "react-router-dom";
+import { makeStyles } from "@material-ui/core/styles";
+import Button from '@material-ui/core/Button';
+import Box from '@material-ui/core/Box';
+import BottomNavigation from '@material-ui/core/BottomNavigation';
+import BottomNavigationAction from '@material-ui/core/BottomNavigationAction';
+import QuestionAnswerIcon from '@material-ui/icons/QuestionAnswer';
+import ChatIcon from '@material-ui/icons/Chat';
+
+import TicketsManagerTabs from "../../components/TicketsManagerTabs/";
+import Ticket from "../../components/Ticket/";
+import TicketAdvancedLayout from "../../components/TicketAdvancedLayout";
+import logo from "../../assets/logo.png"; //PLW DESIGN LOGO//
+import { TicketsContext } from "../../context/Tickets/TicketsContext";
+
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles(theme => ({
+    header: {
+    },
+    content: {
+        overflow: "auto"
+    },
+    placeholderContainer: {
+        display: "flex",
+        flexDirection: "column",
+        alignItems: "center",
+        justifyContent: "center",
+        height: "100%",
+		backgroundColor: theme.palette.boxticket, //DARK MODE PLW DESIGN//
+    },
+    placeholderItem: {
+    }
+}));
+
+const TicketAdvanced = (props) => {
+	const classes = useStyles();
+	const { ticketId } = useParams();
+	const [option, setOption] = useState(0);
+    const { currentTicket, setCurrentTicket } = useContext(TicketsContext)
+
+    useEffect(() => {
+        if(currentTicket.id !== null) {
+            setCurrentTicket({ id: currentTicket.id, code: '#open' })
+        }
+        if (!ticketId) {
+            setOption(1)
+        }
+        return () => {
+            setCurrentTicket({ id: null, code: null })
+        }
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [])
+
+    useEffect(() => {
+        if (currentTicket.id !== null) {
+            setOption(0)
+        }
+    }, [currentTicket])
+
+	const renderPlaceholder = () => {
+		return <Box className={classes.placeholderContainer}>
+             {/*<div className={classes.placeholderItem}>{i18n.t("chat.noTicketMessage")}</div>*/}
+			//PLW DESIGN LOGO//
+			<div>
+			<center><img style={{ margin: "0 auto", width: "70%" }} src={logo} alt="logologin" /></center>
+			</div>
+			//PLW DESIGN LOGO//
+			<br />
+            <Button onClick={() => setOption(1)} variant="contained" color="primary">
+                {i18n.t("ticketAdvanced.selectTicket")}
+            </Button>
+        </Box>
+	}
+
+	const renderMessageContext = () => {
+		if (ticketId) {
+			return <Ticket />
+		}
+		return renderPlaceholder()
+	}
+
+	const renderTicketsManagerTabs = () => {
+		return <TicketsManagerTabs />
+	}
+
+	return (
+        <TicketAdvancedLayout>
+            <Box className={classes.header}>
+                <BottomNavigation
+                    value={option}
+                    onChange={(event, newValue) => {
+                        setOption(newValue);
+                    }}
+                    showLabels
+                    className={classes.root}
+                >
+                    <BottomNavigationAction label={i18n.t("ticketAdvanced.ticketNav")} icon={<ChatIcon />} />
+                    <BottomNavigationAction label={i18n.t("ticketAdvanced.attendanceNav")} icon={<QuestionAnswerIcon />} />
+                </BottomNavigation>
+            </Box>
+            <Box className={classes.content}>
+                { option === 0 ? renderMessageContext() : renderTicketsManagerTabs() }
+            </Box>
+        </TicketAdvancedLayout>
+	);
+};
+
+export default TicketAdvanced;

+ 81 - 0
frontend/src/pages/TicketsCustom/index.js

@@ -0,0 +1,81 @@
+import React from "react";
+import { useParams } from "react-router-dom";
+import Grid from "@material-ui/core/Grid";
+import Paper from "@material-ui/core/Paper";
+import { makeStyles } from "@material-ui/core/styles";
+
+import TicketsManager from "../../components/TicketsManagerTabs/";
+import Ticket from "../../components/Ticket/";
+import logo from "../../assets/logo.png"; //PLW DESIGN LOGO//
+import { i18n } from "../../translate/i18n";
+
+const useStyles = makeStyles(theme => ({
+	chatContainer: {
+		flex: 1,
+		// backgroundColor: "#eee",
+		padding: theme.spacing(1), //Aqui ele ajusta espaço na tela de ticket
+		height: `calc(100% - 48px)`,
+		overflowY: "hidden",
+	},
+
+	chatPapper: {
+		// backgroundColor: "red",
+		display: "flex",
+		height: "100%",
+	},
+
+	contactsWrapper: {
+		display: "flex",
+		height: "100%",
+		flexDirection: "column",
+		overflowY: "hidden",
+	},
+	messagesWrapper: {
+		display: "flex",
+		height: "100%",
+		flexDirection: "column",
+	},
+	welcomeMsg: {
+		backgroundColor: theme.palette.boxticket, //DARK MODE PLW DESIGN//
+		display: "flex",
+		justifyContent: "space-evenly",
+		alignItems: "center",
+		height: "100%",
+		textAlign: "center",
+	},
+}));
+
+const TicketsCustom = () => {
+	const classes = useStyles();
+	const { ticketId } = useParams();
+
+	return (
+		<div className={classes.chatContainer}>
+			<div className={classes.chatPapper}>
+				<Grid container spacing={0}>
+					<Grid item xs={4} className={classes.contactsWrapper}>
+						<TicketsManager />
+					</Grid>
+					<Grid item xs={8} className={classes.messagesWrapper}>
+						{ticketId ? (
+							<>
+								<Ticket />
+							</>
+						) : (
+							<Paper square variant="outlined" className={classes.welcomeMsg}>
+							{/* PLW DESIGN LOGO */}
+							<div>
+							<center><img style={{ margin: "0 auto", width: "70%" }} src={logo} alt="logologin" /></center>
+							</div>
+							{/* PLW DESIGN LOGO */}
+							{/*<span>{i18n.t("chat.noTicketMessage")}</span>*/}
+							</Paper>
+						)}
+					</Grid>
+				</Grid>
+			</div>
+		</div>
+	);
+};
+
+export default TicketsCustom;

+ 132 - 0
frontend/src/pages/ToDoList/index.js

@@ -0,0 +1,132 @@
+import React, { useState, useEffect } from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import TextField from '@material-ui/core/TextField';
+import Button from '@material-ui/core/Button';
+import List from '@material-ui/core/List';
+import ListItem from '@material-ui/core/ListItem';
+import ListItemText from '@material-ui/core/ListItemText';
+import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
+import IconButton from '@material-ui/core/IconButton';
+import DeleteIcon from '@material-ui/icons/Delete';
+import EditIcon from '@material-ui/icons/Edit';
+import { i18n } from '../../translate/i18n';
+
+const useStyles = makeStyles({
+  root: {
+    display: 'flex',
+    flexDirection: 'column',
+    alignItems: 'center',
+    margin: '2rem'
+  },
+  inputContainer: {
+    display: 'flex',
+    width: '100%',
+    marginBottom: '1rem'
+  },
+  input: {
+    flexGrow: 1,
+    marginRight: '1rem'
+  },
+  listContainer: {
+    width: '100%',
+    height: '100%',
+    marginTop: '1rem',
+    backgroundColor: '#f5f5f5',
+    borderRadius: '5px',
+  },
+  list: {
+    marginBottom: '5px'
+  }
+});
+
+const ToDoList = () => {
+  const classes = useStyles();
+
+  const [task, setTask] = useState('');
+  const [tasks, setTasks] = useState([]);
+  const [editIndex, setEditIndex] = useState(-1);
+
+  useEffect(() => {
+    const savedTasks = localStorage.getItem('tasks');
+    if (savedTasks) {
+      setTasks(JSON.parse(savedTasks));
+    }
+  }, []);
+
+  useEffect(() => {
+    localStorage.setItem('tasks', JSON.stringify(tasks));
+  }, [tasks]);
+
+  const handleTaskChange = (event) => {
+    setTask(event.target.value);
+  };
+
+  const handleAddTask = () => {
+    if (!task.trim()) {
+      // Impede que o usuário crie uma tarefa sem texto
+      return;
+    }
+
+    const now = new Date();
+    if (editIndex >= 0) {
+      // Editar tarefa existente
+      const newTasks = [...tasks];
+      newTasks[editIndex] = {text: task, updatedAt: now, createdAt: newTasks[editIndex].createdAt};
+      setTasks(newTasks);
+      setTask('');
+      setEditIndex(-1);
+    } else {
+      // Adicionar nova tarefa
+      setTasks([...tasks, {text: task, createdAt: now, updatedAt: now}]);
+      setTask('');
+    }
+  };
+
+  const handleEditTask = (index) => {
+    setTask(tasks[index].text);
+    setEditIndex(index);
+  };
+
+  const handleDeleteTask = (index) => {
+    const newTasks = [...tasks];
+    newTasks.splice(index, 1);
+    setTasks(newTasks);
+  };
+
+  return (
+    <div className={classes.root}>
+      <div className={classes.inputContainer}>
+        <TextField
+          className={classes.input}
+          label={i18n.t('todolist.input')}
+          value={task}
+          onChange={handleTaskChange}
+          variant="outlined"
+        />
+        <Button variant="contained" color="primary" onClick={handleAddTask}>
+          {editIndex >= 0 ? i18n.t('todolist.buttons.save') : i18n.t('todolist.buttons.add')}
+        </Button>
+      </div>
+      <div className={classes.listContainer}>
+        <List>
+          {tasks.map((task, index) => (
+            <ListItem key={index} className={classes.list}>
+              <ListItemText primary={task.text} secondary={task.updatedAt.toLocaleString()} />
+              <ListItemSecondaryAction>
+                <IconButton onClick={() => handleEditTask(index)}>
+                  <EditIcon />
+                </IconButton>
+                <IconButton onClick={() => handleDeleteTask(index)}>
+                  <DeleteIcon />
+                </IconButton>
+              </ListItemSecondaryAction>
+            </ListItem>
+          ))}
+        </List>
+      </div>
+    </div>
+  );
+};
+
+
+export default ToDoList;

+ 294 - 0
frontend/src/pages/Users/index.js

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

+ 36 - 0
frontend/src/routes/Route.js

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

+ 183 - 0
frontend/src/routes/index.js

@@ -0,0 +1,183 @@
+import React, { useEffect, useState } from "react";
+import { BrowserRouter, Switch } from "react-router-dom";
+import { ToastContainer } from "react-toastify";
+
+import LoggedInLayout from "../layout";
+import Dashboard from "../pages/Dashboard/";
+import TicketResponsiveContainer from "../pages/TicketResponsiveContainer";
+import Signup from "../pages/Signup/";
+import Login from "../pages/Login/";
+import Connections from "../pages/Connections/";
+import SettingsCustom from "../pages/SettingsCustom/";
+import Financeiro from "../pages/Financeiro/";
+import Users from "../pages/Users";
+import Contacts from "../pages/Contacts/";
+import Queues from "../pages/Queues/";
+import Tags from "../pages/Tags/";
+import MessagesAPI from "../pages/MessagesAPI/";
+import Helps from "../pages/Helps/";
+import ContactLists from "../pages/ContactLists/";
+import ContactListItems from "../pages/ContactListItems/";
+// import Companies from "../pages/Companies/";
+import QuickMessages from "../pages/QuickMessages/";
+import Kanban from "../pages/Kanban";
+import { AuthProvider } from "../context/Auth/AuthContext";
+import { TicketsContextProvider } from "../context/Tickets/TicketsContext";
+import { WhatsAppsProvider } from "../context/WhatsApp/WhatsAppsContext";
+import Route from "./Route";
+import Schedules from "../pages/Schedules";
+import Campaigns from "../pages/Campaigns";
+import CampaignsConfig from "../pages/CampaignsConfig";
+import CampaignReport from "../pages/CampaignReport";
+import Annoucements from "../pages/Annoucements";
+import Chat from "../pages/Chat";
+import ToDoList from "../pages/ToDoList/";
+import Subscription from "../pages/Subscription/";
+import Files from "../pages/Files/";
+import Prompts from "../pages/Prompts";
+import QueueIntegration from "../pages/QueueIntegration";
+import ForgetPassword from "../pages/ForgetPassWord/"; // Reset PassWd
+
+const Routes = () => {
+  const [showCampaigns, setShowCampaigns] = useState(false);
+
+  useEffect(() => {
+    const cshow = localStorage.getItem("cshow");
+    if (cshow !== undefined) {
+      setShowCampaigns(true);
+    }
+  }, []);
+
+  return (
+    <BrowserRouter>
+      <AuthProvider>
+        <TicketsContextProvider>
+          <Switch>
+            <Route exact path="/login" component={Login} />
+            <Route exact path="/signup" component={Signup} />
+			      <Route exact path="/forgetpsw" component={ForgetPassword} /> 
+            {/* <Route exact path="/create-company" component={Companies} /> */}
+            <WhatsAppsProvider>
+              <LoggedInLayout>
+                <Route exact path="/" component={Dashboard} isPrivate />
+                <Route
+                  exact
+                  path="/tickets/:ticketId?"
+                  component={TicketResponsiveContainer}
+                  isPrivate
+                />
+                <Route
+                  exact
+                  path="/connections"
+                  component={Connections}
+                  isPrivate
+                />
+                <Route
+                  exact
+                  path="/quick-messages"
+                  component={QuickMessages}
+                  isPrivate
+                />
+                <Route
+                  exact
+                  path="/todolist"
+                  component={ToDoList}
+                  isPrivate
+                  />
+                <Route
+                  exact
+                  path="/schedules"
+                  component={Schedules}
+                  isPrivate
+                />
+                <Route exact path="/tags" component={Tags} isPrivate />
+                <Route exact path="/contacts" component={Contacts} isPrivate />
+                <Route exact path="/helps" component={Helps} isPrivate />
+                <Route exact path="/users" component={Users} isPrivate />
+                <Route exact path="/files" component={Files} isPrivate />
+                <Route exact path="/prompts" component={Prompts} isPrivate />
+                <Route exact path="/queue-integration" component={QueueIntegration} isPrivate />
+
+                <Route
+                  exact
+                  path="/messages-api"
+                  component={MessagesAPI}
+                  isPrivate
+                />
+                <Route
+                  exact
+                  path="/settings"
+                  component={SettingsCustom}
+                  isPrivate
+                />
+				        <Route 
+                  exact
+                  path="/kanban"
+                  component={Kanban}
+                  isPrivate
+                />
+                <Route
+                  exact
+                  path="/financeiro"
+                  component={Financeiro}
+                  isPrivate
+                />
+                <Route exact path="/queues" component={Queues} isPrivate />
+                <Route
+                  exact
+                  path="/announcements"
+                  component={Annoucements}
+                  isPrivate
+                />
+                <Route
+                  exact
+                  path="/subscription"
+                  component={Subscription}
+                  isPrivate
+                />
+                <Route exact path="/chats/:id?" component={Chat} isPrivate />
+                {showCampaigns && (
+                  <>
+                    <Route
+                      exact
+                      path="/contact-lists"
+                      component={ContactLists}
+                      isPrivate
+                    />
+                    <Route
+                      exact
+                      path="/contact-lists/:contactListId/contacts"
+                      component={ContactListItems}
+                      isPrivate
+                    />
+                    <Route
+                      exact
+                      path="/campaigns"
+                      component={Campaigns}
+                      isPrivate
+                    />
+                    <Route
+                      exact
+                      path="/campaign/:campaignId/report"
+                      component={CampaignReport}
+                      isPrivate
+                    />
+                    <Route
+                      exact
+                      path="/campaigns-config"
+                      component={CampaignsConfig}
+                      isPrivate
+                    />
+                  </>
+                )}
+              </LoggedInLayout>
+            </WhatsAppsProvider>
+          </Switch>
+          <ToastContainer autoClose={3000} />
+        </TicketsContextProvider>
+      </AuthProvider>
+    </BrowserRouter>
+  );
+};
+
+export default Routes;

+ 12 - 0
frontend/src/services/api.js

@@ -0,0 +1,12 @@
+import axios from "axios";
+
+const api = axios.create({
+	baseURL: process.env.REACT_APP_BACKEND_URL,
+	withCredentials: true,
+});
+
+export const openApi = axios.create({
+	baseURL: process.env.REACT_APP_BACKEND_URL
+});
+
+export default api;

+ 3 - 0
frontend/src/services/socket.js

@@ -0,0 +1,3 @@
+export function socketConnection(params) {
+  throw new Error("Favor usar o SocketContext");
+}