Initial commit

This commit is contained in:
2025-10-13 00:23:31 -03:00
commit 42921527f8
725 changed files with 91395 additions and 0 deletions
@@ -0,0 +1,338 @@
import React, { useState, useEffect, useRef } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import CircularProgress from "@material-ui/core/CircularProgress";
import AttachFileIcon from "@material-ui/icons/AttachFile";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import IconButton from "@material-ui/core/IconButton";
import { i18n } from "../../translate/i18n";
import { head } from "lodash";
import api from "../../services/api";
import toastError from "../../errors/toastError";
import {
FormControl,
Grid,
InputLabel,
MenuItem,
Select,
} from "@material-ui/core";
import ConfirmationModal from "../ConfirmationModal";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
flexWrap: "wrap",
},
multFieldLine: {
display: "flex",
"& > *:not(:last-child)": {
marginRight: theme.spacing(1),
},
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
colorAdorment: {
width: 20,
height: 20,
},
}));
const AnnouncementSchema = Yup.object().shape({
title: Yup.string().required("Obrigatório"),
text: Yup.string().required("Obrigatório"),
});
const AnnouncementModal = ({ open, onClose, announcementId, reload }) => {
const classes = useStyles();
const initialState = {
title: "",
text: "",
priority: 3,
status: true,
};
const [confirmationOpen, setConfirmationOpen] = useState(false);
const [announcement, setAnnouncement] = useState(initialState);
const [attachment, setAttachment] = useState(null);
const attachmentFile = useRef(null);
useEffect(() => {
try {
(async () => {
if (!announcementId) return;
const { data } = await api.get(`/announcements/${announcementId}`);
setAnnouncement((prevState) => {
return { ...prevState, ...data };
});
})();
} catch (err) {
toastError(err);
}
}, [announcementId, open]);
const handleClose = () => {
setAnnouncement(initialState);
setAttachment(null);
onClose();
};
const handleAttachmentFile = (e) => {
const file = head(e.target.files);
if (file) {
setAttachment(file);
}
};
const handleSaveAnnouncement = async (values) => {
const announcementData = { ...values };
try {
if (announcementId) {
await api.put(`/announcements/${announcementId}`, announcementData);
if (attachment != null) {
const formData = new FormData();
formData.append("file", attachment);
await api.post(
`/announcements/${announcementId}/media-upload`,
formData
);
}
} else {
const { data } = await api.post("/announcements", announcementData);
if (attachment != null) {
const formData = new FormData();
formData.append("file", attachment);
await api.post(`/announcements/${data.id}/media-upload`, formData);
}
}
toast.success(i18n.t("announcements.toasts.success"));
if (typeof reload == "function") {
reload();
}
} catch (err) {
toastError(err);
}
handleClose();
};
const deleteMedia = async () => {
if (attachment) {
setAttachment(null);
attachmentFile.current.value = null;
}
if (announcement.mediaPath) {
await api.delete(`/announcements/${announcement.id}/media-upload`);
setAnnouncement((prev) => ({
...prev,
mediaPath: null,
}));
toast.success(i18n.t("announcements.toasts.deleted"));
if (typeof reload == "function") {
reload();
}
}
};
return (
<div className={classes.root}>
<ConfirmationModal
title={i18n.t("announcements.confirmationModal.deleteTitle")}
open={confirmationOpen}
onClose={() => setConfirmationOpen(false)}
onConfirm={deleteMedia}
>
{i18n.t("announcements.confirmationModal.deleteMessage")}
</ConfirmationModal>
<Dialog
open={open}
onClose={handleClose}
maxWidth="xs"
fullWidth
scroll="paper"
>
<DialogTitle id="form-dialog-title">
{announcementId
? `${i18n.t("announcements.dialog.edit")}`
: `${i18n.t("announcements.dialog.add")}`}
</DialogTitle>
<div style={{ display: "none" }}>
<input
type="file"
accept=".png,.jpg,.jpeg"
ref={attachmentFile}
onChange={(e) => handleAttachmentFile(e)}
/>
</div>
<Formik
initialValues={announcement}
enableReinitialize={true}
validationSchema={AnnouncementSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveAnnouncement(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting, values }) => (
<Form>
<DialogContent dividers>
<Grid spacing={2} container>
<Grid xs={12} item>
<Field
as={TextField}
label={i18n.t("announcements.dialog.form.title")}
name="title"
error={touched.title && Boolean(errors.title)}
helperText={touched.title && errors.title}
variant="outlined"
margin="dense"
fullWidth
/>
</Grid>
<Grid xs={12} item>
<Field
as={TextField}
label={i18n.t("announcements.dialog.form.text")}
name="text"
error={touched.text && Boolean(errors.text)}
helperText={touched.text && errors.text}
variant="outlined"
margin="dense"
multiline={true}
rows={7}
fullWidth
/>
</Grid>
<Grid xs={12} item>
<FormControl variant="outlined" margin="dense" fullWidth>
<InputLabel id="status-selection-label">
{i18n.t("announcements.dialog.form.status")}
</InputLabel>
<Field
as={Select}
label={i18n.t("announcements.dialog.form.status")}
placeholder={i18n.t("announcements.dialog.form.status")}
labelId="status-selection-label"
id="status"
name="status"
error={touched.status && Boolean(errors.status)}
>
<MenuItem value={true}>Ativo</MenuItem>
<MenuItem value={false}>Inativo</MenuItem>
</Field>
</FormControl>
</Grid>
<Grid xs={12} item>
<FormControl variant="outlined" margin="dense" fullWidth>
<InputLabel id="priority-selection-label">
{i18n.t("announcements.dialog.form.priority")}
</InputLabel>
<Field
as={Select}
label={i18n.t("announcements.dialog.form.priority")}
placeholder={i18n.t(
"announcements.dialog.form.priority"
)}
labelId="priority-selection-label"
id="priority"
name="priority"
error={touched.priority && Boolean(errors.priority)}
>
<MenuItem value={1}>Alta</MenuItem>
<MenuItem value={2}>Média</MenuItem>
<MenuItem value={3}>Baixa</MenuItem>
</Field>
</FormControl>
</Grid>
{(announcement.mediaPath || attachment) && (
<Grid xs={12} item>
<Button startIcon={<AttachFileIcon />}>
{attachment ? attachment.name : announcement.mediaName}
</Button>
<IconButton
onClick={() => setConfirmationOpen(true)}
color="secondary"
>
<DeleteOutlineIcon />
</IconButton>
</Grid>
)}
</Grid>
</DialogContent>
<DialogActions>
{!attachment && !announcement.mediaPath && (
<Button
color="primary"
onClick={() => attachmentFile.current.click()}
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("announcements.dialog.buttons.attach")}
</Button>
)}
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("announcements.dialog.buttons.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{announcementId
? `${i18n.t("announcements.dialog.buttons.add")}`
: `${i18n.t("announcements.dialog.buttons.edit")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default AnnouncementModal;
@@ -0,0 +1,329 @@
import React, { useEffect, useReducer, useState, useContext } from "react";
import { makeStyles } from "@material-ui/core/styles";
import toastError from "../../errors/toastError";
import Popover from "@material-ui/core/Popover";
import AnnouncementIcon from "@material-ui/icons/Announcement";
import Notifications from "@material-ui/icons/Notifications"
import {
Avatar,
Badge,
IconButton,
List,
ListItem,
ListItemAvatar,
ListItemText,
Dialog,
Paper,
Typography,
DialogTitle,
DialogContent,
DialogActions,
Button,
DialogContentText,
} from "@material-ui/core";
import api from "../../services/api";
import { isArray } from "lodash";
import moment from "moment";
import { SocketContext } from "../../context/Socket/SocketContext";
const useStyles = makeStyles((theme) => ({
mainPaper: {
flex: 1,
maxHeight: 3000,
maxWidth: 5000,
padding: theme.spacing(1),
overflowY: "scroll",
...theme.scrollbarStyles,
},
}));
function AnnouncementDialog({ announcement, open, handleClose }) {
const getMediaPath = (filename) => {
return `${process.env.REACT_APP_BACKEND_URL}/public/${filename}`;
};
return (
<Dialog
open={open}
onClose={() => handleClose()}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{announcement.title}</DialogTitle>
<DialogContent>
{announcement.mediaPath && (
<div
style={{
border: "1px solid #f1f1f1",
margin: "0 auto 20px",
textAlign: "center",
width: "400px",
height: 300,
backgroundImage: `url(${getMediaPath(announcement.mediaPath)})`,
backgroundRepeat: "no-repeat",
backgroundSize: "contain",
backgroundPosition: "center",
}}
></div>
)}
<DialogContentText id="alert-dialog-description">
{announcement.text}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => handleClose()} color="primary" autoFocus>
Fechar
</Button>
</DialogActions>
</Dialog>
);
}
const reducer = (state, action) => {
if (action.type === "LOAD_ANNOUNCEMENTS") {
const announcements = action.payload;
const newAnnouncements = [];
if (isArray(announcements)) {
announcements.forEach((announcement) => {
const announcementIndex = state.findIndex(
(u) => u.id === announcement.id
);
if (announcementIndex !== -1) {
state[announcementIndex] = announcement;
} else {
newAnnouncements.push(announcement);
}
});
}
return [...state, ...newAnnouncements];
}
if (action.type === "UPDATE_ANNOUNCEMENTS") {
const announcement = action.payload;
const announcementIndex = state.findIndex((u) => u.id === announcement.id);
if (announcementIndex !== -1) {
state[announcementIndex] = announcement;
return [...state];
} else {
return [announcement, ...state];
}
}
if (action.type === "DELETE_ANNOUNCEMENT") {
const announcementId = action.payload;
const announcementIndex = state.findIndex((u) => u.id === announcementId);
if (announcementIndex !== -1) {
state.splice(announcementIndex, 1);
}
return [...state];
}
if (action.type === "RESET") {
return [];
}
};
export default function AnnouncementsPopover() {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const [pageNumber, setPageNumber] = useState(1);
const [hasMore, setHasMore] = useState(false);
const [searchParam] = useState("");
const [announcements, dispatch] = useReducer(reducer, []);
const [invisible, setInvisible] = useState(false);
const [announcement, setAnnouncement] = useState({});
const [showAnnouncementDialog, setShowAnnouncementDialog] = useState(false);
const socketManager = useContext(SocketContext);
useEffect(() => {
dispatch({ type: "RESET" });
setPageNumber(1);
}, [searchParam]);
useEffect(() => {
setLoading(true);
const delayDebounceFn = setTimeout(() => {
fetchAnnouncements();
}, 500);
return () => clearTimeout(delayDebounceFn);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParam, pageNumber]);
useEffect(() => {
const companyId = localStorage.getItem("companyId");
const socket = socketManager.getSocket(companyId);
if (!socket) {
return () => {};
}
socket.on(`company-announcement`, (data) => {
if (data.action === "update" || data.action === "create") {
dispatch({ type: "UPDATE_ANNOUNCEMENTS", payload: data.record });
setInvisible(false);
}
if (data.action === "delete") {
dispatch({ type: "DELETE_ANNOUNCEMENT", payload: +data.id });
}
});
return () => {
socket.disconnect();
};
}, [socketManager]);
const fetchAnnouncements = async () => {
try {
const { data } = await api.get("/announcements/", {
params: { searchParam, pageNumber },
});
dispatch({ type: "LOAD_ANNOUNCEMENTS", payload: data.records });
setHasMore(data.hasMore);
setLoading(false);
} catch (err) {
toastError(err);
}
};
const loadMore = () => {
setPageNumber((prevState) => prevState + 1);
};
const handleScroll = (e) => {
if (!hasMore || loading) return;
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
if (scrollHeight - (scrollTop + 100) < clientHeight) {
loadMore();
}
};
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
setInvisible(true);
};
const handleClose = () => {
setAnchorEl(null);
};
const borderPriority = (priority) => {
if (priority === 1) {
return "4px solid #b81111";
}
if (priority === 2) {
return "4px solid orange";
}
if (priority === 3) {
return "4px solid grey";
}
};
const getMediaPath = (filename) => {
return `${process.env.REACT_APP_BACKEND_URL}/public/${filename}`;
};
const handleShowAnnouncementDialog = (record) => {
setAnnouncement(record);
setShowAnnouncementDialog(true);
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? "simple-popover" : undefined;
return (
<div>
<AnnouncementDialog
announcement={announcement}
open={showAnnouncementDialog}
handleClose={() => setShowAnnouncementDialog(false)}
/>
<IconButton
variant="contained"
aria-describedby={id}
onClick={handleClick}
style={{ color: "white" }}
>
<Badge
color="secondary"
variant="dot"
invisible={invisible || announcements.length < 1}
>
<Notifications />
</Badge>
</IconButton>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
>
<Paper
variant="outlined"
onScroll={handleScroll}
className={classes.mainPaper}
>
<List
component="nav"
aria-label="main mailbox folders"
style={{ minWidth: 300 }}
>
{isArray(announcements) &&
announcements.map((item, key) => (
<ListItem
key={key}
style={{
//background: key % 2 === 0 ? "#ededed" : "white",
border: "1px solid #eee",
borderLeft: borderPriority(item.priority),
cursor: "pointer",
}}
onClick={() => handleShowAnnouncementDialog(item)}
>
{item.mediaPath && (
<ListItemAvatar>
<Avatar
alt={item.mediaName}
src={getMediaPath(item.mediaPath)}
/>
</ListItemAvatar>
)}
<ListItemText
primary={item.title}
secondary={
<>
<Typography component="span" style={{ fontSize: 12 }}>
{moment(item.createdAt).format("DD/MM/YYYY")}
</Typography>
<span style={{ marginTop: 5, display: "block" }}></span>
<Typography component="span" variant="body2">
{item.text}
</Typography>
</>
}
/>
</ListItem>
))}
{isArray(announcements) && announcements.length === 0 && (
<ListItemText primary="Nenhum registro" />
)}
</List>
</Paper>
</Popover>
</div>
);
}
@@ -0,0 +1,23 @@
import React from "react";
import Backdrop from "@material-ui/core/Backdrop";
import CircularProgress from "@material-ui/core/CircularProgress";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
backdrop: {
zIndex: theme.zIndex.drawer + 1,
color: "#fff",
},
}));
const BackdropLoading = () => {
const classes = useStyles();
return (
<Backdrop className={classes.backdrop} open={true}>
<CircularProgress color="inherit" />
</Backdrop>
);
};
export default BackdropLoading;
@@ -0,0 +1,35 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import { CircularProgress, Button } from "@material-ui/core";
const useStyles = makeStyles(theme => ({
button: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
}));
const ButtonWithSpinner = ({ loading, children, ...rest }) => {
const classes = useStyles();
return (
<Button className={classes.button} disabled={loading} {...rest}>
{children}
{loading && (
<CircularProgress size={24} className={classes.buttonProgress} />
)}
</Button>
);
};
export default ButtonWithSpinner;
@@ -0,0 +1,771 @@
import React, { useState, useEffect, useRef, useContext } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { head } from "lodash";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import IconButton from "@material-ui/core/IconButton";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import CircularProgress from "@material-ui/core/CircularProgress";
import AttachFileIcon from "@material-ui/icons/AttachFile";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import { i18n } from "../../translate/i18n";
import moment from "moment";
import api from "../../services/api";
import toastError from "../../errors/toastError";
import {
Box,
FormControl,
Grid,
InputLabel,
MenuItem,
Select,
Tab,
Tabs,
} from "@material-ui/core";
import { AuthContext } from "../../context/Auth/AuthContext";
import ConfirmationModal from "../ConfirmationModal";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
flexWrap: "wrap",
backgroundColor: "#fff"
},
tabmsg: {
backgroundColor: theme.palette.campaigntab,
},
textField: {
marginRight: theme.spacing(1),
flex: 1,
},
extraAttr: {
display: "flex",
justifyContent: "center",
alignItems: "center",
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
}));
const CampaignSchema = Yup.object().shape({
name: Yup.string()
.min(2, "Too Short!")
.max(50, "Too Long!")
.required("Required"),
});
const CampaignModal = ({
open,
onClose,
campaignId,
initialValues,
onSave,
resetPagination,
}) => {
const classes = useStyles();
const isMounted = useRef(true);
const { user } = useContext(AuthContext);
const { companyId } = user;
const [file, setFile] = useState(null);
const initialState = {
name: "",
message1: "",
message2: "",
message3: "",
message4: "",
message5: "",
confirmationMessage1: "",
confirmationMessage2: "",
confirmationMessage3: "",
confirmationMessage4: "",
confirmationMessage5: "",
status: "INATIVA", // INATIVA, PROGRAMADA, EM_ANDAMENTO, CANCELADA, FINALIZADA,
confirmation: false,
scheduledAt: "",
whatsappId: "",
contactListId: "",
tagListId: "Nenhuma",
companyId,
};
const [campaign, setCampaign] = useState(initialState);
const [whatsapps, setWhatsapps] = useState([]);
const [contactLists, setContactLists] = useState([]);
const [messageTab, setMessageTab] = useState(0);
const [attachment, setAttachment] = useState(null);
const [confirmationOpen, setConfirmationOpen] = useState(false);
const [campaignEditable, setCampaignEditable] = useState(true);
const attachmentFile = useRef(null);
const [tagLists, setTagLists] = useState([]);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
(async () => {
try {
const { data } = await api.get("/files/", {
params: { companyId }
});
setFile(data.files);
} catch (err) {
toastError(err);
}
})();
}, []);
useEffect(() => {
if (isMounted.current) {
if (initialValues) {
setCampaign((prevState) => {
return { ...prevState, ...initialValues };
});
}
api
.get(`/contact-lists/list`, { params: { companyId } })
.then(({ data }) => setContactLists(data));
api
.get(`/whatsapp`, { params: { companyId, session: 0 } })
.then(({ data }) => setWhatsapps(data));
api.get(`/tags`, { params: { companyId } })
.then(({ data }) => {
const fetchedTags = data.tags;
// Perform any necessary data transformation here
const formattedTagLists = fetchedTags.map((tag) => ({
id: tag.id,
name: tag.name,
}));
setTagLists(formattedTagLists);
})
.catch((error) => {
console.error("Error retrieving tags:", error);
});
if (!campaignId) return;
api.get(`/campaigns/${campaignId}`).then(({ data }) => {
setCampaign((prev) => {
let prevCampaignData = Object.assign({}, prev);
Object.entries(data).forEach(([key, value]) => {
if (key === "scheduledAt" && value !== "" && value !== null) {
prevCampaignData[key] = moment(value).format("YYYY-MM-DDTHH:mm");
} else {
prevCampaignData[key] = value === null ? "" : value;
}
});
return prevCampaignData;
});
});
}
}, [campaignId, open, initialValues, companyId]);
useEffect(() => {
const now = moment();
const scheduledAt = moment(campaign.scheduledAt);
const moreThenAnHour =
!Number.isNaN(scheduledAt.diff(now)) && scheduledAt.diff(now, "hour") > 1;
const isEditable =
campaign.status === "INATIVA" ||
(campaign.status === "PROGRAMADA" && moreThenAnHour);
setCampaignEditable(isEditable);
}, [campaign.status, campaign.scheduledAt]);
const handleClose = () => {
onClose();
setCampaign(initialState);
};
const handleAttachmentFile = (e) => {
const file = head(e.target.files);
if (file) {
setAttachment(file);
}
};
const handleSaveCampaign = async (values) => {
try {
const dataValues = {};
Object.entries(values).forEach(([key, value]) => {
if (key === "scheduledAt" && value !== "" && value !== null) {
dataValues[key] = moment(value).format("YYYY-MM-DD HH:mm:ss");
} else {
dataValues[key] = value === "" ? null : value;
}
});
if (campaignId) {
await api.put(`/campaigns/${campaignId}`, dataValues);
if (attachment != null) {
const formData = new FormData();
formData.append("file", attachment);
await api.post(`/campaigns/${campaignId}/media-upload`, formData);
}
handleClose();
} else {
const { data } = await api.post("/campaigns", dataValues);
if (attachment != null) {
const formData = new FormData();
formData.append("file", attachment);
await api.post(`/campaigns/${data.id}/media-upload`, formData);
}
if (onSave) {
onSave(data);
}
handleClose();
}
toast.success(i18n.t("campaigns.toasts.success"));
} catch (err) {
console.log(err);
toastError(err);
}
};
const deleteMedia = async () => {
if (attachment) {
setAttachment(null);
attachmentFile.current.value = null;
}
if (campaign.mediaPath) {
await api.delete(`/campaigns/${campaign.id}/media-upload`);
setCampaign((prev) => ({ ...prev, mediaPath: null, mediaName: null }));
toast.success(i18n.t("campaigns.toasts.deleted"));
}
};
const renderMessageField = (identifier) => {
return (
<Field
as={TextField}
id={identifier}
name={identifier}
fullWidth
rows={5}
label={i18n.t(`campaigns.dialog.form.${identifier}`)}
placeholder={i18n.t("campaigns.dialog.form.messagePlaceholder")}
multiline={true}
variant="outlined"
helperText="Utilize variáveis como {nome}, {numero}, {email} ou defina variáveis personalziadas."
disabled={!campaignEditable && campaign.status !== "CANCELADA"}
/>
);
};
const renderConfirmationMessageField = (identifier) => {
return (
<Field
as={TextField}
id={identifier}
name={identifier}
fullWidth
rows={5}
label={i18n.t(`campaigns.dialog.form.${identifier}`)}
placeholder={i18n.t("campaigns.dialog.form.messagePlaceholder")}
multiline={true}
variant="outlined"
disabled={!campaignEditable && campaign.status !== "CANCELADA"}
/>
);
};
const cancelCampaign = async () => {
try {
await api.post(`/campaigns/${campaign.id}/cancel`);
toast.success(i18n.t("campaigns.toasts.cancel"));
setCampaign((prev) => ({ ...prev, status: "CANCELADA" }));
resetPagination();
} catch (err) {
toast.error(err.message);
}
};
const restartCampaign = async () => {
try {
await api.post(`/campaigns/${campaign.id}/restart`);
toast.success(i18n.t("campaigns.toasts.restart"));
setCampaign((prev) => ({ ...prev, status: "EM_ANDAMENTO" }));
resetPagination();
} catch (err) {
toast.error(err.message);
}
};
return (
<div className={classes.root}>
<ConfirmationModal
title={i18n.t("campaigns.confirmationModal.deleteTitle")}
open={confirmationOpen}
onClose={() => setConfirmationOpen(false)}
onConfirm={deleteMedia}
>
{i18n.t("campaigns.confirmationModal.deleteMessage")}
</ConfirmationModal>
<Dialog
open={open}
onClose={handleClose}
fullWidth
maxWidth="md"
scroll="paper"
>
<DialogTitle id="form-dialog-title">
{campaignEditable ? (
<>
{campaignId
? `${i18n.t("campaigns.dialog.update")}`
: `${i18n.t("campaigns.dialog.new")}`}
</>
) : (
<>{`${i18n.t("campaigns.dialog.readonly")}`}</>
)}
</DialogTitle>
<div style={{ display: "none" }}>
<input
type="file"
ref={attachmentFile}
onChange={(e) => handleAttachmentFile(e)}
/>
</div>
<Formik
initialValues={campaign}
enableReinitialize={true}
validationSchema={CampaignSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveCampaign(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ values, errors, touched, isSubmitting }) => (
<Form>
<DialogContent dividers>
<Grid spacing={2} container>
<Grid xs={12} md={9} item>
<Field
as={TextField}
label={i18n.t("campaigns.dialog.form.name")}
name="name"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
fullWidth
className={classes.textField}
disabled={!campaignEditable}
/>
</Grid>
<Grid xs={12} md={3} item>
<FormControl
variant="outlined"
margin="dense"
fullWidth
className={classes.formControl}
>
<InputLabel id="confirmation-selection-label">
{i18n.t("campaigns.dialog.form.confirmation")}
</InputLabel>
<Field
as={Select}
label={i18n.t("campaigns.dialog.form.confirmation")}
placeholder={i18n.t(
"campaigns.dialog.form.confirmation"
)}
labelId="confirmation-selection-label"
id="confirmation"
name="confirmation"
error={
touched.confirmation && Boolean(errors.confirmation)
}
disabled={!campaignEditable}
>
<MenuItem value={false}>Desabilitada</MenuItem>
<MenuItem value={true}>Habilitada</MenuItem>
</Field>
</FormControl>
</Grid>
<Grid xs={12} md={4} item>
<FormControl
variant="outlined"
margin="dense"
fullWidth
className={classes.formControl}
>
<InputLabel id="contactList-selection-label">
{i18n.t("campaigns.dialog.form.contactList")}
</InputLabel>
<Field
as={Select}
label={i18n.t("campaigns.dialog.form.contactList")}
placeholder={i18n.t(
"campaigns.dialog.form.contactList"
)}
labelId="contactList-selection-label"
id="contactListId"
name="contactListId"
error={
touched.contactListId && Boolean(errors.contactListId)
}
disabled={!campaignEditable}
>
<MenuItem value="">Nenhuma</MenuItem>
{contactLists &&
contactLists.map((contactList) => (
<MenuItem
key={contactList.id}
value={contactList.id}
>
{contactList.name}
</MenuItem>
))}
</Field>
</FormControl>
</Grid>
<Grid xs={12} md={4} item>
<FormControl
variant="outlined"
margin="dense"
fullWidth
className={classes.formControl}
>
<InputLabel id="tagList-selection-label">
{i18n.t("campaigns.dialog.form.tagList")}
</InputLabel>
<Field
as={Select}
label={i18n.t("campaigns.dialog.form.tagList")}
placeholder={i18n.t("campaigns.dialog.form.tagList")}
labelId="tagList-selection-label"
id="tagListId"
name="tagListId"
error={touched.tagListId && Boolean(errors.tagListId)}
disabled={!campaignEditable}
>
<MenuItem value="">Nenhuma</MenuItem>
{Array.isArray(tagLists) &&
tagLists.map((tagList) => (
<MenuItem key={tagList.id} value={tagList.id}>
{tagList.name}
</MenuItem>
))}
</Field>
</FormControl>
</Grid>
<Grid xs={12} md={4} item>
<FormControl
variant="outlined"
margin="dense"
fullWidth
className={classes.formControl}
>
<InputLabel id="whatsapp-selection-label">
{i18n.t("campaigns.dialog.form.whatsapp")}
</InputLabel>
<Field
as={Select}
label={i18n.t("campaigns.dialog.form.whatsapp")}
placeholder={i18n.t("campaigns.dialog.form.whatsapp")}
labelId="whatsapp-selection-label"
id="whatsappId"
name="whatsappId"
error={touched.whatsappId && Boolean(errors.whatsappId)}
disabled={!campaignEditable}
>
<MenuItem value="">Nenhuma</MenuItem>
{whatsapps &&
whatsapps.map((whatsapp) => (
<MenuItem key={whatsapp.id} value={whatsapp.id}>
{whatsapp.name}
</MenuItem>
))}
</Field>
</FormControl>
</Grid>
<Grid xs={12} md={4} item>
<Field
as={TextField}
label={i18n.t("campaigns.dialog.form.scheduledAt")}
name="scheduledAt"
error={touched.scheduledAt && Boolean(errors.scheduledAt)}
helperText={touched.scheduledAt && errors.scheduledAt}
variant="outlined"
margin="dense"
type="datetime-local"
InputLabelProps={{
shrink: true,
}}
fullWidth
className={classes.textField}
disabled={!campaignEditable}
/>
</Grid>
<Grid xs={12} md={4} item>
<FormControl
variant="outlined"
margin="dense"
className={classes.FormControl}
fullWidth
>
<InputLabel id="fileListId-selection-label">{i18n.t("campaigns.dialog.form.fileList")}</InputLabel>
<Field
as={Select}
label={i18n.t("campaigns.dialog.form.fileList")}
name="fileListId"
id="fileListId"
placeholder={i18n.t("campaigns.dialog.form.fileList")}
labelId="fileListId-selection-label"
value={values.fileListId || ""}
>
<MenuItem value={""} >{"Nenhum"}</MenuItem>
{file.map(f => (
<MenuItem key={f.id} value={f.id}>
{f.name}
</MenuItem>
))}
</Field>
</FormControl>
</Grid>
<Grid xs={12} item>
<Tabs
value={messageTab}
indicatorColor="primary"
textColor="primary"
className={classes.tabmsg}
onChange={(e, v) => setMessageTab(v)}
variant="fullWidth"
centered
style={{
borderRadius: 2,
}}
>
<Tab label="Msg. 1" index={0} />
<Tab label="Msg. 2" index={1} />
<Tab label="Msg. 3" index={2} />
<Tab label="Msg. 4" index={3} />
<Tab label="Msg. 5" index={4} />
</Tabs>
<Box style={{ paddingTop: 20, border: "none" }}>
{messageTab === 0 && (
<>
{values.confirmation ? (
<Grid spacing={2} container>
<Grid xs={12} md={8} item>
<>{renderMessageField("message1")}</>
</Grid>
<Grid xs={12} md={4} item>
<>
{renderConfirmationMessageField(
"confirmationMessage1"
)}
</>
</Grid>
</Grid>
) : (
<>{renderMessageField("message1")}</>
)}
</>
)}
{messageTab === 1 && (
<>
{values.confirmation ? (
<Grid spacing={2} container>
<Grid xs={12} md={8} item>
<>{renderMessageField("message2")}</>
</Grid>
<Grid xs={12} md={4} item>
<>
{renderConfirmationMessageField(
"confirmationMessage2"
)}
</>
</Grid>
</Grid>
) : (
<>{renderMessageField("message2")}</>
)}
</>
)}
{messageTab === 2 && (
<>
{values.confirmation ? (
<Grid spacing={2} container>
<Grid xs={12} md={8} item>
<>{renderMessageField("message3")}</>
</Grid>
<Grid xs={12} md={4} item>
<>
{renderConfirmationMessageField(
"confirmationMessage3"
)}
</>
</Grid>
</Grid>
) : (
<>{renderMessageField("message3")}</>
)}
</>
)}
{messageTab === 3 && (
<>
{values.confirmation ? (
<Grid spacing={2} container>
<Grid xs={12} md={8} item>
<>{renderMessageField("message4")}</>
</Grid>
<Grid xs={12} md={4} item>
<>
{renderConfirmationMessageField(
"confirmationMessage4"
)}
</>
</Grid>
</Grid>
) : (
<>{renderMessageField("message4")}</>
)}
</>
)}
{messageTab === 4 && (
<>
{values.confirmation ? (
<Grid spacing={2} container>
<Grid xs={12} md={8} item>
<>{renderMessageField("message5")}</>
</Grid>
<Grid xs={12} md={4} item>
<>
{renderConfirmationMessageField(
"confirmationMessage5"
)}
</>
</Grid>
</Grid>
) : (
<>{renderMessageField("message5")}</>
)}
</>
)}
</Box>
</Grid>
{(campaign.mediaPath || attachment) && (
<Grid xs={12} item>
<Button startIcon={<AttachFileIcon />}>
{attachment != null
? attachment.name
: campaign.mediaName}
</Button>
{campaignEditable && (
<IconButton
onClick={() => setConfirmationOpen(true)}
color="secondary"
>
<DeleteOutlineIcon />
</IconButton>
)}
</Grid>
)}
</Grid>
</DialogContent>
<DialogActions>
{campaign.status === "CANCELADA" && (
<Button
color="primary"
onClick={() => restartCampaign()}
variant="outlined"
>
{i18n.t("campaigns.dialog.buttons.restart")}
</Button>
)}
{campaign.status === "EM_ANDAMENTO" && (
<Button
color="primary"
onClick={() => cancelCampaign()}
variant="outlined"
>
{i18n.t("campaigns.dialog.buttons.cancel")}
</Button>
)}
{!attachment && !campaign.mediaPath && campaignEditable && (
<Button
color="primary"
onClick={() => attachmentFile.current.click()}
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("campaigns.dialog.buttons.attach")}
</Button>
)}
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("campaigns.dialog.buttons.close")}
</Button>
{(campaignEditable || campaign.status === "CANCELADA") && (
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{campaignId
? `${i18n.t("campaigns.dialog.buttons.edit")}`
: `${i18n.t("campaigns.dialog.buttons.add")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
)}
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default CampaignModal;
+39
View File
@@ -0,0 +1,39 @@
import rules from "../../rules";
const check = (role, action, data) => {
const permissions = rules[role];
if (!permissions) {
// role is not present in the rules
return false;
}
const staticPermissions = permissions.static;
if (staticPermissions && staticPermissions.includes(action)) {
// static rule not provided for action
return true;
}
const dynamicPermissions = permissions.dynamic;
if (dynamicPermissions) {
const permissionCondition = dynamicPermissions[action];
if (!permissionCondition) {
// dynamic rule not provided for action
return false;
}
return permissionCondition(data);
}
return false;
};
const Can = ({ role, perform, data, yes, no }) =>
check(role, perform, data) ? yes() : no();
Can.defaultProps = {
yes: () => null,
no: () => null,
};
export { Can };
@@ -0,0 +1,174 @@
import React, { useContext, useState } from "react";
import {
Stepper,
Step,
StepLabel,
Button,
Typography,
CircularProgress,
} from "@material-ui/core";
import { Formik, Form } from "formik";
import AddressForm from "./Forms/AddressForm";
import PaymentForm from "./Forms/PaymentForm";
import ReviewOrder from "./ReviewOrder";
import CheckoutSuccess from "./CheckoutSuccess";
import api from "../../services/api";
import toastError from "../../errors/toastError";
import { toast } from "react-toastify";
import { AuthContext } from "../../context/Auth/AuthContext";
import validationSchema from "./FormModel/validationSchema";
import checkoutFormModel from "./FormModel/checkoutFormModel";
import formInitialValues from "./FormModel/formInitialValues";
import useStyles from "./styles";
import Invoices from "../../pages/Financeiro";
export default function CheckoutPage(props) {
const steps = ["Dados", "Personalizar", "Revisar"];
const { formId, formField } = checkoutFormModel;
const classes = useStyles();
const [activeStep, setActiveStep] = useState(1);
const [datePayment, setDatePayment] = useState(null);
const [invoiceId, setinvoiceId] = useState(props.Invoice.id);
const currentValidationSchema = validationSchema[activeStep];
const isLastStep = activeStep === steps.length - 1;
const { user } = useContext(AuthContext);
function _renderStepContent(step, setFieldValue, setActiveStep, values ) {
switch (step) {
case 0:
return <AddressForm formField={formField} values={values} setFieldValue={setFieldValue} />;
case 1:
return <PaymentForm
formField={formField}
setFieldValue={setFieldValue}
setActiveStep={setActiveStep}
activeStep={step}
invoiceId={invoiceId}
values={values}
/>;
case 2:
return <ReviewOrder />;
default:
return <div>Not Found</div>;
}
}
async function _submitForm(values, actions) {
try {
const plan = JSON.parse(values.plan);
const newValues = {
firstName: values.firstName,
lastName: values.lastName,
address2: values.address2,
city: values.city,
state: values.state,
zipcode: values.zipcode,
country: values.country,
useAddressForPaymentDetails: values.useAddressForPaymentDetails,
nameOnCard: values.nameOnCard,
cardNumber: values.cardNumber,
cvv: values.cvv,
plan: values.plan,
price: plan.price,
users: plan.users,
connections: plan.connections,
invoiceId: invoiceId
}
const { data } = await api.post("/subscription", newValues);
setDatePayment(data)
actions.setSubmitting(false);
setActiveStep(activeStep + 1);
toast.success("Assinatura realizada com sucesso!, aguardando a realização do pagamento");
} catch (err) {
toastError(err);
}
}
function _handleSubmit(values, actions) {
if (isLastStep) {
_submitForm(values, actions);
} else {
setActiveStep(activeStep + 1);
actions.setTouched({});
actions.setSubmitting(false);
}
}
function _handleBack() {
setActiveStep(activeStep - 1);
}
return (
<React.Fragment>
<Typography component="h1" variant="h4" align="center">
Falta pouco!
</Typography>
<Stepper activeStep={activeStep} className={classes.stepper}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
<React.Fragment>
{activeStep === steps.length ? (
<CheckoutSuccess pix={datePayment} />
) : (
<Formik
initialValues={{
...user,
...formInitialValues
}}
validationSchema={currentValidationSchema}
onSubmit={_handleSubmit}
>
{({ isSubmitting, setFieldValue, values }) => (
<Form id={formId}>
{_renderStepContent(activeStep, setFieldValue, setActiveStep, values)}
<div className={classes.buttons}>
{activeStep !== 1 && (
<Button onClick={_handleBack} className={classes.button}>
VOLTAR
</Button>
)}
<div className={classes.wrapper}>
{activeStep !== 1 && (
<Button
disabled={isSubmitting}
type="submit"
variant="contained"
color="primary"
className={classes.button}
>
{isLastStep ? "PAGAR" : "PRÓXIMO"}
</Button>
)}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</div>
</div>
</Form>
)}
</Formik>
)}
</React.Fragment>
</React.Fragment>
);
}
@@ -0,0 +1,76 @@
import React, { useState, useEffect, useContext } from 'react';
import { useHistory } from "react-router-dom";
import QRCode from 'react-qr-code';
import { SuccessContent, Total } from './style';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { FaCopy, FaCheckCircle } from 'react-icons/fa';
import { SocketContext } from "../../../context/Socket/SocketContext";
import { useDate } from "../../../hooks/useDate";
import { toast } from "react-toastify";
function CheckoutSuccess(props) {
const { pix } = props;
const [pixString,] = useState(pix.qrcode.qrcode);
const [copied, setCopied] = useState(false);
const history = useHistory();
const { dateToClient } = useDate();
const socketManager = useContext(SocketContext);
useEffect(() => {
const companyId = localStorage.getItem("companyId");
const socket = socketManager.getSocket(companyId);
socket.on(`company-${companyId}-payment`, (data) => {
if (data.action === "CONCLUIDA") {
toast.success(`Sua licença foi renovada até ${dateToClient(data.company.dueDate)}!`);
setTimeout(() => {
history.push("/");
}, 4000);
}
});
}, [history, socketManager]);
const handleCopyQR = () => {
setTimeout(() => {
setCopied(false);
}, 1 * 1000);
setCopied(true);
};
return (
<React.Fragment>
<Total>
<span>TOTAL</span>
<strong>R${pix.valor.original.toLocaleString('pt-br', { minimumFractionDigits: 2 })}</strong>
</Total>
<SuccessContent>
<QRCode value={pixString} />
<CopyToClipboard text={pixString} onCopy={handleCopyQR}>
<button className="copy-button" type="button">
{copied ? (
<>
<span>Copiado</span>
<FaCheckCircle size={18} />
</>
) : (
<>
<span>Copiar código QR</span>
<FaCopy size={18} />
</>
)}
</button>
</CopyToClipboard>
<span>
Para finalizar, basta realizar o pagamento escaneando ou colando o
código Pix acima :)
</span>
</SuccessContent>
</React.Fragment>
);
}
export default CheckoutSuccess;
@@ -0,0 +1,2 @@
import CheckoutSuccess from './CheckoutSuccess';
export default CheckoutSuccess;
@@ -0,0 +1,117 @@
import styled from 'styled-components';
export const Container = styled.div`
footer {
margin-top: 30px;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
@media (max-width: 768px) {
flex-direction: column;
.checkout-buttons {
display: flex;
flex-direction: column-reverse;
width: 100%;
button {
width: 100%;
margin-top: 16px;
margin-left: 0;
}
}
}
button {
margin-left: 16px;
}
}
`;
export const Total = styled.div`
display: flex;
align-items: baseline;
span {
color: #333;
font-weight: bold;
}
strong {
color: #333;
font-size: 28px;
margin-left: 5px;
}
@media (max-width: 768px) {
min-width: 100%;
justify-content: space-between;
}
`;
export const SuccessContent = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> h2 {
text-align: center;
}
> svg {
margin-top: 16px;
}
> span {
margin-top: 24px;
text-align: center;
}
> p,
strong {
margin-top: 8px;
font-size: 9px;
color: #999;
}
.copy-button {
font-size: 14px;
cursor: pointer;
text-align: center;
user-select: none;
min-height: 56px;
display: inline-flex;
-webkit-box-pack: center;
justify-content: center;
-webkit-box-align: center;
align-items: center;
background-color: #f9f9f9;
color: #000;
-webkit-appearance: none !important;
outline: none;
margin-top: 16px;
width: 256px;
font-weight: 600;
text-transform: uppercase;
border: none;
> span {
margin-right: 8px;
}
}
`;
export const CheckoutWrapper = styled.div`
width: 100%;
margin: 0 auto 0;
max-width: 1110px;
display: flex;
flex-direction: column;
-webkit-box-align: center;
align-items: center;
padding: 50px 95px;
background: #fff;
@media (max-width: 768px) {
padding: 16px 24px;
`;
@@ -0,0 +1,73 @@
export default {
formId: 'checkoutForm',
formField: {
firstName: {
name: 'firstName',
label: 'Nome completo*',
requiredErrorMsg: 'O nome completo é obrigatório'
},
lastName: {
name: 'lastName',
label: 'Last name*',
requiredErrorMsg: 'Last name is required'
},
address1: {
name: 'address2',
label: 'Endereço*',
requiredErrorMsg: 'O Endereço é obrigatório'
},
city: {
name: 'city',
label: 'Cidade*',
requiredErrorMsg: 'Cidade é obrigatória'
},
state: {
name: 'state',
label: 'Estado*',
requiredErrorMsg: 'Cidade é obrigatória'
},
zipcode: {
name: 'zipcode',
label: 'CEP*',
requiredErrorMsg: 'CEP é obrigatório',
invalidErrorMsg: 'Formato de CEP inválido'
},
country: {
name: 'country',
label: 'País*',
requiredErrorMsg: 'País é obrigatório'
},
useAddressForPaymentDetails: {
name: 'useAddressForPaymentDetails',
label: 'Use this address for payment details'
},
invoiceId: {
name: 'invoiceId',
label: 'Use this invoiceId'
},
nameOnCard: {
name: 'nameOnCard',
label: 'Name on card*',
requiredErrorMsg: 'Name on card is required'
},
cardNumber: {
name: 'cardNumber',
label: 'Card number*',
requiredErrorMsg: 'Card number is required',
invalidErrorMsg: 'Card number is not valid (e.g. 4111111111111)'
},
expiryDate: {
name: 'expiryDate',
label: 'Expiry date*',
requiredErrorMsg: 'Expiry date is required',
invalidErrorMsg: 'Expiry date is not valid'
},
cvv: {
name: 'cvv',
label: 'CVV*',
requiredErrorMsg: 'CVV is required',
invalidErrorMsg: 'CVV is invalid (e.g. 357)'
}
}
};
@@ -0,0 +1,32 @@
import checkoutFormModel from './checkoutFormModel';
const {
formField: {
firstName,
lastName,
address1,
city,
state,
zipcode,
country,
useAddressForPaymentDetails,
nameOnCard,
cardNumber,
invoiceId,
cvv
}
} = checkoutFormModel;
export default {
[firstName.name]: '',
[lastName.name]: '',
[address1.name]: '',
[city.name]: '',
[state.name]: '',
[zipcode.name]: '',
[country.name]: '',
[useAddressForPaymentDetails.name]: false,
[nameOnCard.name]: '',
[cardNumber.name]: '',
[invoiceId.name]: '',
[cvv.name]: ''
};
@@ -0,0 +1,29 @@
import * as Yup from 'yup';
import checkoutFormModel from './checkoutFormModel';
const {
formField: {
firstName,
address1,
city,
zipcode,
country,
}
} = checkoutFormModel;
export default [
Yup.object().shape({
[firstName.name]: Yup.string().required(`${firstName.requiredErrorMsg}`),
[address1.name]: Yup.string().required(`${address1.requiredErrorMsg}`),
[city.name]: Yup.string()
.nullable()
.required(`${city.requiredErrorMsg}`),
[zipcode.name]: Yup.string()
.required(`${zipcode.requiredErrorMsg}`),
[country.name]: Yup.string()
.nullable()
.required(`${country.requiredErrorMsg}`)
}),
];
@@ -0,0 +1,134 @@
import React, { useContext, useEffect, useState } from "react";
import { Grid, Typography } from "@material-ui/core";
import { InputField, SelectField } from "../../FormFields";
import { AuthContext } from "../../../context/Auth/AuthContext";
const countries = [
{
value: "BR",
label: "Brasil",
},
{
value: "usa",
label: "United States",
},
];
export default function AddressForm(props) {
const { user } = useContext(AuthContext);
const [billingName, setBillingName] = useState(user.company.billingName);
const [addressZipCode, setAddressZipCode] = useState(user.company.addressZipCode);
const [addressStreet, setAddressStreet] = useState(user.company.addressStreet);
const [addressState, setAddressState] = useState(user.company.addressState);
const [addressCity, setAddressCity] = useState(user.company.addressCity);
const [addressDistrict, setAddressDistrict] = useState(user.company.addressDistrict);
const {
formField: {
firstName,
address1,
city,
state,
zipcode,
country,
},
setFieldValue
} = props;
useEffect(() => {
setFieldValue("firstName", billingName)
setFieldValue("zipcode", addressZipCode)
setFieldValue("address2", addressStreet)
setFieldValue("state", addressState)
setFieldValue("city", addressCity)
setFieldValue("country", addressDistrict)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<React.Fragment>
<Typography variant="h6" gutterBottom>
Vamos precisar de algumas informações
</Typography>
<Grid container spacing={3}>
<Grid item xs={6} sm={6}>
<InputField name={firstName.name} label={firstName.label} fullWidth
value={billingName}
onChange={(e) => {
setBillingName(e.target.value)
setFieldValue("firstName", e.target.value)
}}
/>
</Grid>
<Grid item xs={6} sm={6}>
<SelectField
name={country.name}
label={country.label}
data={countries}
fullWidth
value={addressDistrict}
onChange={(e) => {
setAddressDistrict(e.target.value)
setFieldValue("country", e.target.value)
}
}
/>
</Grid>
<Grid item xs={4}>
<InputField
name={zipcode.name}
label={zipcode.label}
fullWidth
value={addressZipCode}
onChange={(e) => {
setAddressZipCode(e.target.value)
setFieldValue("zipcode", e.target.value)
}}
/>
</Grid>
<Grid item xs={8}>
<InputField
name={address1.name}
label={address1.label}
fullWidth
value={addressStreet}
onChange={(e) => {
setAddressStreet(e.target.value)
setFieldValue("address2", e.target.value)
}}
/>
</Grid>
<Grid item xs={4}>
<InputField
name={state.name}
label={state.label}
fullWidth
value={addressState}
onChange={(e) => {
setAddressState(e.target.value)
setFieldValue("state", e.target.value)
}}
/>
</Grid>
<Grid item xs={8}>
<InputField
name={city.name}
label={city.label}
fullWidth
value={addressCity}
onChange={(e) => {
setAddressCity(e.target.value)
setFieldValue("city", e.target.value)
}}
/>
</Grid>
</Grid>
</React.Fragment>
);
}
@@ -0,0 +1,237 @@
import React, { useState, useEffect, useReducer } from 'react';
import Button from '@material-ui/core/Button';
import Card from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import CardHeader from '@material-ui/core/CardHeader';
import Grid from '@material-ui/core/Grid';
import StarIcon from '@material-ui/icons/StarBorder';
import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles';
import IconButton from '@material-ui/core/IconButton';
import MinimizeIcon from '@material-ui/icons/Minimize';
import AddIcon from '@material-ui/icons/Add';
import usePlans from "../../../hooks/usePlans";
import useCompanies from "../../../hooks/useCompanies";
const useStyles = makeStyles((theme) => ({
'@global': {
ul: {
margin: 0,
padding: 0,
listStyle: 'none',
},
},
margin: {
margin: theme.spacing(1),
},
cardHeader: {
backgroundColor:
theme.palette.type === 'light' ? theme.palette.grey[200] : theme.palette.grey[700],
},
cardPricing: {
display: 'flex',
justifyContent: 'center',
alignItems: 'baseline',
marginBottom: theme.spacing(2),
},
footer: {
borderTop: `1px solid ${theme.palette.divider}`,
marginTop: theme.spacing(8),
paddingTop: theme.spacing(3),
paddingBottom: theme.spacing(3),
[theme.breakpoints.up('sm')]: {
paddingTop: theme.spacing(6),
paddingBottom: theme.spacing(6),
},
},
customCard: {
display: "flex",
marginTop: "16px",
alignItems: "center",
flexDirection: "column",
},
custom: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}
}));
export default function Pricing(props) {
const {
setFieldValue,
setActiveStep,
activeStep,
} = props;
const handleChangeAdd = (event, newValue) => {
if (newValue < 3) return
const newPrice = 11.00;
setUsersPlans(newValue);
setCustomValuePlans(customValuePlans + newPrice);
}
const handleChangeMin = (event, newValue) => {
if (newValue < 3) return
const newPrice = 11;
setUsersPlans(newValue);
setCustomValuePlans(customValuePlans - newPrice);
}
const handleChangeConnectionsAdd = (event, newValue) => {
if (newValue < 3) return
const newPrice = 20.00;
setConnectionsPlans(newValue);
setCustomValuePlans(customValuePlans + newPrice);
}
const handleChangeConnectionsMin = (event, newValue) => {
if (newValue < 3) return
const newPrice = 20;
setConnectionsPlans(newValue);
setCustomValuePlans(customValuePlans - newPrice);
}
const { list, finder } = usePlans();
const { find } = useCompanies();
const classes = useStyles();
const [usersPlans, setUsersPlans] = React.useState(3);
const [companiesPlans, setCompaniesPlans] = useState(0);
const [connectionsPlans, setConnectionsPlans] = React.useState(3);
const [storagePlans, setStoragePlans] = React.useState([]);
const [customValuePlans, setCustomValuePlans] = React.useState(49.00);
const [loading, setLoading] = React.useState(false);
const companyId = localStorage.getItem("companyId");
useEffect(() => {
async function fetchData() {
await loadCompanies();
}
fetchData();
}, [])
const loadCompanies = async () => {
setLoading(true);
try {
const companiesList = await find(companyId);
setCompaniesPlans(companiesList.planId);
await loadPlans(companiesList.planId);
} catch (e) {
console.log(e);
// toast.error("Não foi possível carregar a lista de registros");
}
setLoading(false);
};
const loadPlans = async (companiesPlans) => {
setLoading(true);
try {
const plansCompanies = await finder(companiesPlans);
const plans = []
//plansCompanies.forEach((plan) => {
plans.push({
title: plansCompanies.name,
planId: plansCompanies.id,
price: plansCompanies.value,
description: [
`${plansCompanies.users} Usuários`,
`${plansCompanies.connections} Conexão`,
`${plansCompanies.queues} Filas`
],
users: plansCompanies.users,
connections: plansCompanies.connections,
queues: plansCompanies.queues,
buttonText: 'SELECIONAR',
buttonVariant: 'outlined',
})
// setStoragePlans(data);
//});
setStoragePlans(plans);
} catch (e) {
console.log(e);
// toast.error("Não foi possível carregar a lista de registros");
}
setLoading(false);
};
const tiers = storagePlans
return (
<React.Fragment>
<Grid container spacing={3}>
{tiers.map((tier) => (
// Enterprise card is full width at sm breakpoint
<Grid item key={tier.title} xs={12} sm={tier.title === 'Enterprise' ? 12 : 12} md={12}>
<Card>
<CardHeader
title={tier.title}
subheader={tier.subheader}
titleTypographyProps={{ align: 'center' }}
subheaderTypographyProps={{ align: 'center' }}
action={tier.title === 'Pro' ? <StarIcon /> : null}
className={classes.cardHeader}
/>
<CardContent>
<div className={classes.cardPricing}>
<Typography component="h2" variant="h3" color="textPrimary">
{
<React.Fragment>
R${tier.price.toLocaleString('pt-br', { minimumFractionDigits: 2 })}
</React.Fragment>
}
</Typography>
<Typography variant="h6" color="textSecondary">
/mês
</Typography>
</div>
<ul>
{tier.description.map((line) => (
<Typography component="li" variant="subtitle1" align="center" key={line}>
{line}
</Typography>
))}
</ul>
</CardContent>
<CardActions>
<Button
fullWidth
variant={tier.buttonVariant}
color="primary"
onClick={() => {
if (tier.custom) {
setFieldValue("plan", JSON.stringify({
users: usersPlans,
connections: connectionsPlans,
price: customValuePlans,
}));
} else {
setFieldValue("plan", JSON.stringify(tier));
}
setActiveStep(activeStep + 1);
}
}
>
{tier.buttonText}
</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
</React.Fragment>
);
}
@@ -0,0 +1,61 @@
import React, {useContext} from 'react';
import { Typography, Grid } from '@material-ui/core';
import useStyles from './styles';
import { AuthContext } from "../../../context/Auth/AuthContext";
function PaymentDetails(props) {
const { formValues } = props;
const classes = useStyles();
const { firstName, address2, city, zipcode, state, country, plan } = formValues;
const { user } = useContext(AuthContext);
const newPlan = JSON.parse(plan);
const { price } = newPlan;
return (
<Grid item container direction="column" xs={12} sm={6}>
<Typography variant="h6" gutterBottom className={classes.title}>
Informação de pagamento
</Typography>
<Grid container>
<React.Fragment>
<Grid item xs={6}>
<Typography gutterBottom>Email:</Typography>
</Grid>
<Grid item xs={6}>
<Typography gutterBottom>{user.company.email}</Typography>
</Grid>
</React.Fragment>
<React.Fragment>
<Grid item xs={6}>
<Typography gutterBottom>Nome:</Typography>
</Grid>
<Grid item xs={6}>
<Typography gutterBottom>{firstName}</Typography>
</Grid>
</React.Fragment>
<React.Fragment>
<Grid item xs={6}>
<Typography gutterBottom>Endereço:</Typography>
</Grid>
<Grid item xs={6}>
<Typography gutterBottom>
{address2}, {city} - {state} {zipcode} {country}
</Typography>
</Grid>
</React.Fragment>
<React.Fragment>
<Grid item xs={6}>
<Typography gutterBottom>Total:</Typography>
</Grid>
<Grid item xs={6}>
<Typography gutterBottom>R${price.toLocaleString('pt-br', {minimumFractionDigits: 2})}</Typography>
</Grid>
</React.Fragment>
</Grid>
</Grid>
);
}
export default PaymentDetails;
@@ -0,0 +1,33 @@
import React from 'react';
import { Typography, List, ListItem, ListItemText } from '@material-ui/core';
import useStyles from './styles';
const products = [
{ name: 'Product 1', desc: 'A nice thing', price: '$9.99' },
{ name: 'Product 2', desc: 'Another thing', price: '$3.45' },
{ name: 'Product 3', desc: 'Something else', price: '$6.51' },
{ name: 'Product 4', desc: 'Best thing of all', price: '$14.11' },
{ name: 'Shipping', desc: '', price: 'Free' }
];
function ProductDetails() {
const classes = useStyles();
return (
<List disablePadding>
{products.map(product => (
<ListItem className={classes.listItem} key={product.name}>
<ListItemText primary={product.name} secondary={product.desc} />
<Typography variant="body2">{product.price}</Typography>
</ListItem>
))}
<ListItem className={classes.listItem}>
<ListItemText primary="Total" />
<Typography variant="subtitle1" className={classes.total}>
$34.06
</Typography>
</ListItem>
</List>
);
}
export default ProductDetails;
@@ -0,0 +1,19 @@
import React from 'react';
import { useFormikContext } from 'formik';
import { Typography, Grid } from '@material-ui/core';
import ShippingDetails from './ShippingDetails';
import PaymentDetails from './PaymentDetails';
export default function ReviewOrder() {
const { values: formValues } = useFormikContext();
return (
<React.Fragment>
<Typography variant="h6" gutterBottom>
Resumo da assinatura
</Typography>
<Grid container spacing={2}>
<ShippingDetails formValues={formValues} />
</Grid>
</React.Fragment>
);
}
@@ -0,0 +1,25 @@
import React from 'react';
import { Typography, Grid } from '@material-ui/core';
import useStyles from './styles';
function PaymentDetails(props) {
const { formValues } = props;
const classes = useStyles();
const { plan } = formValues;
const newPlan = JSON.parse(plan);
const { users, connections, price } = newPlan;
return (
<Grid item xs={12} sm={12}>
<Typography variant="h6" gutterBottom className={classes.title}>
Detalhes do plano
</Typography>
<Typography gutterBottom>Usuários: {users}</Typography>
<Typography gutterBottom>Whatsapps: {connections}</Typography>
<Typography gutterBottom>Cobrança: Mensal</Typography>
<Typography gutterBottom>Total: R${price.toLocaleString('pt-br', {minimumFractionDigits: 2})}</Typography>
</Grid>
);
}
export default PaymentDetails;
@@ -0,0 +1,2 @@
import ReviewOrder from './ReviewOrder';
export default ReviewOrder;
@@ -0,0 +1,12 @@
import { makeStyles } from '@material-ui/core/styles';
export default makeStyles(theme => ({
listItem: {
padding: theme.spacing(1, 0)
},
total: {
fontWeight: '700'
},
title: {
marginTop: theme.spacing(2)
}
}));
@@ -0,0 +1,2 @@
import CheckoutPage from './CheckoutPage';
export default CheckoutPage;
@@ -0,0 +1,23 @@
import { makeStyles } from '@material-ui/core/styles';
export default makeStyles(theme => ({
stepper: {
padding: theme.spacing(3, 0, 5)
},
buttons: {
display: 'flex',
justifyContent: 'flex-end'
},
button: {
marginTop: theme.spacing(3),
marginLeft: theme.spacing(1)
},
wrapper: {
margin: theme.spacing(1),
position: 'relative'
},
buttonProgress: {
position: 'absolute',
top: '50%',
left: '50%'
}
}));
@@ -0,0 +1,85 @@
import { Dialog } from "@material-ui/core";
import React, { useState } from "react";
import { BlockPicker } from "react-color";
const ColorPicker = ({ onChange, currentColor, handleClose, open }) => {
const [selectedColor, setSelectedColor] = useState(currentColor);
const handleChange = color => {
setSelectedColor(color.hex);
handleClose();
};
const colors = [
"#B80000",
"#DB3E00",
"#FCCB00",
"#008B02",
"#006B76",
"#1273DE",
"#004DCF",
"#5300EB",
"#EB9694",
"#FAD0C3",
"#FEF3BD",
"#C1E1C5",
"#BEDADC",
"#C4DEF6",
"#BED3F3",
"#D4C4FB",
"#4D4D4D",
"#999999",
"#F44E3B",
"#FE9200",
"#FCDC00",
"#DBDF00",
"#A4DD00",
"#68CCCA",
"#73D8FF",
"#AEA1FF",
"#FDA1FF",
"#333333",
"#808080",
"#cccccc",
"#D33115",
"#E27300",
"#FCC400",
"#B0BC00",
"#68BC00",
"#16A5A5",
"#009CE0",
"#7B64FF",
"#FA28FF",
"#666666",
"#B3B3B3",
"#9F0500",
"#C45100",
"#FB9E00",
"#808900",
"#194D33",
"#0C797D",
"#0062B1",
"#653294",
"#AB149E",
];
return (
<Dialog
onClose={handleClose}
aria-labelledby="simple-dialog-title"
open={open}
>
<BlockPicker
width={"100%"}
triangle="hide"
color={selectedColor}
colors={colors}
onChange={handleChange}
onChangeComplete={color => onChange(color.hex)}
/>
</Dialog>
);
};
export default ColorPicker;
@@ -0,0 +1,627 @@
import React, { useState, useEffect } from "react";
import {
makeStyles,
Paper,
Grid,
FormControl,
InputLabel,
MenuItem,
TextField,
Table,
TableHead,
TableBody,
TableCell,
TableRow,
IconButton,
Select,
} from "@material-ui/core";
import { Formik, Form, Field } from "formik";
import ButtonWithSpinner from "../ButtonWithSpinner";
import ConfirmationModal from "../ConfirmationModal";
import { Edit as EditIcon } from "@material-ui/icons";
import { toast } from "react-toastify";
import useCompanies from "../../hooks/useCompanies";
import usePlans from "../../hooks/usePlans";
import ModalUsers from "../ModalUsers";
import api from "../../services/api";
import { head, isArray, has } from "lodash";
import { useDate } from "../../hooks/useDate";
import moment from "moment";
const useStyles = makeStyles((theme) => ({
root: {
width: "100%",
},
mainPaper: {
width: "100%",
flex: 1,
padding: theme.spacing(2),
},
fullWidth: {
width: "100%",
},
tableContainer: {
width: "100%",
overflowX: "scroll",
...theme.scrollbarStyles,
},
textfield: {
width: "100%",
},
textRight: {
textAlign: "right",
},
row: {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
},
control: {
paddingRight: theme.spacing(1),
paddingLeft: theme.spacing(1),
},
buttonContainer: {
textAlign: "right",
padding: theme.spacing(1),
},
}));
export function CompanyForm(props) {
const { onSubmit, onDelete, onCancel, initialValue, loading } = props;
const classes = useStyles();
const [plans, setPlans] = useState([]);
const [modalUser, setModalUser] = useState(false);
const [firstUser, setFirstUser] = useState({});
const [record, setRecord] = useState({
name: "",
email: "",
phone: "",
planId: "",
status: true,
campaignsEnabled: false,
dueDate: "",
recurrence: "",
...initialValue,
});
const { list: listPlans } = usePlans();
useEffect(() => {
async function fetchData() {
const list = await listPlans();
setPlans(list);
}
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setRecord((prev) => {
if (moment(initialValue).isValid()) {
initialValue.dueDate = moment(initialValue.dueDate).format(
"YYYY-MM-DD"
);
}
return {
...prev,
...initialValue,
};
});
}, [initialValue]);
const handleSubmit = async (data) => {
if (data.dueDate === "" || moment(data.dueDate).isValid() === false) {
data.dueDate = null;
}
onSubmit(data);
setRecord({ ...initialValue, dueDate: "" });
};
const handleOpenModalUsers = async () => {
try {
const { data } = await api.get("/users/list", {
params: {
companyId: initialValue.id,
},
});
if (isArray(data) && data.length) {
setFirstUser(head(data));
}
setModalUser(true);
} catch (e) {
toast.error(e);
}
};
const handleCloseModalUsers = () => {
setFirstUser({});
setModalUser(false);
};
const incrementDueDate = () => {
const data = { ...record };
if (data.dueDate !== "" && data.dueDate !== null) {
switch (data.recurrence) {
case "MENSAL":
data.dueDate = moment(data.dueDate)
.add(1, "month")
.format("YYYY-MM-DD");
break;
case "BIMESTRAL":
data.dueDate = moment(data.dueDate)
.add(2, "month")
.format("YYYY-MM-DD");
break;
case "TRIMESTRAL":
data.dueDate = moment(data.dueDate)
.add(3, "month")
.format("YYYY-MM-DD");
break;
case "SEMESTRAL":
data.dueDate = moment(data.dueDate)
.add(6, "month")
.format("YYYY-MM-DD");
break;
case "ANUAL":
data.dueDate = moment(data.dueDate)
.add(12, "month")
.format("YYYY-MM-DD");
break;
default:
break;
}
}
setRecord(data);
};
return (
<>
<ModalUsers
userId={firstUser.id}
companyId={initialValue.id}
open={modalUser}
onClose={handleCloseModalUsers}
/>
<Formik
enableReinitialize
className={classes.fullWidth}
initialValues={record}
onSubmit={(values, { resetForm }) =>
setTimeout(() => {
handleSubmit(values);
resetForm();
}, 500)
}
>
{(values, setValues) => (
<Form className={classes.fullWidth}>
<Grid spacing={2} justifyContent="flex-end" container>
<Grid xs={12} sm={6} md={4} item>
<Field
as={TextField}
label="Nome"
name="name"
variant="outlined"
className={classes.fullWidth}
margin="dense"
/>
</Grid>
<Grid xs={12} sm={6} md={2} item>
<Field
as={TextField}
label="E-mail"
name="email"
variant="outlined"
className={classes.fullWidth}
margin="dense"
required
/>
</Grid>
<Grid xs={12} sm={6} md={2} item>
<Field
as={TextField}
label="Telefone"
name="phone"
variant="outlined"
className={classes.fullWidth}
margin="dense"
/>
</Grid>
<Grid xs={12} sm={6} md={2} item>
<FormControl margin="dense" variant="outlined" fullWidth>
<InputLabel htmlFor="plan-selection">Plano</InputLabel>
<Field
as={Select}
id="plan-selection"
label="Plano"
labelId="plan-selection-label"
name="planId"
margin="dense"
required
>
{plans.map((plan, key) => (
<MenuItem key={key} value={plan.id}>
{plan.name}
</MenuItem>
))}
</Field>
</FormControl>
</Grid>
<Grid xs={12} sm={6} md={2} item>
<FormControl margin="dense" variant="outlined" fullWidth>
<InputLabel htmlFor="status-selection">Status</InputLabel>
<Field
as={Select}
id="status-selection"
label="Status"
labelId="status-selection-label"
name="status"
margin="dense"
>
<MenuItem value={true}>Sim</MenuItem>
<MenuItem value={false}>Não</MenuItem>
</Field>
</FormControl>
</Grid>
<Grid xs={12} sm={6} md={2} item>
<FormControl margin="dense" variant="outlined" fullWidth>
<InputLabel htmlFor="status-selection">Campanhas</InputLabel>
<Field
as={Select}
id="campaigns-selection"
label="Campanhas"
labelId="campaigns-selection-label"
name="campaignsEnabled"
margin="dense"
>
<MenuItem value={true}>Habilitadas</MenuItem>
<MenuItem value={false}>Desabilitadas</MenuItem>
</Field>
</FormControl>
</Grid>
<Grid xs={12} sm={6} md={2} item>
<FormControl variant="outlined" fullWidth>
<Field
as={TextField}
label="Data de Vencimento"
type="date"
name="dueDate"
InputLabelProps={{
shrink: true,
}}
variant="outlined"
fullWidth
margin="dense"
/>
</FormControl>
</Grid>
<Grid xs={12} sm={6} md={2} item>
<FormControl margin="dense" variant="outlined" fullWidth>
<InputLabel htmlFor="recorrencia-selection">
Recorrência
</InputLabel>
<Field
as={Select}
label="Recorrência"
labelId="recorrencia-selection-label"
id="recurrence"
name="recurrence"
margin="dense"
>
<MenuItem value="MENSAL">Mensal</MenuItem>
{/*<MenuItem value="BIMESTRAL">Bimestral</MenuItem>*/}
{/*<MenuItem value="TRIMESTRAL">Trimestral</MenuItem>*/}
{/*<MenuItem value="SEMESTRAL">Semestral</MenuItem>*/}
{/*<MenuItem value="ANUAL">Anual</MenuItem>*/}
</Field>
</FormControl>
</Grid>
<Grid xs={12} item>
<Grid justifyContent="flex-end" spacing={1} container>
<Grid xs={4} md={1} item>
<ButtonWithSpinner
className={classes.fullWidth}
style={{ marginTop: 7 }}
loading={loading}
onClick={() => onCancel()}
variant="contained"
>
Limpar
</ButtonWithSpinner>
</Grid>
{record.id !== undefined ? (
<>
<Grid xs={6} md={1} item>
<ButtonWithSpinner
style={{ marginTop: 7 }}
className={classes.fullWidth}
loading={loading}
onClick={() => onDelete(record)}
variant="contained"
color="secondary"
>
Excluir
</ButtonWithSpinner>
</Grid>
<Grid xs={6} md={2} item>
<ButtonWithSpinner
style={{ marginTop: 7 }}
className={classes.fullWidth}
loading={loading}
onClick={() => incrementDueDate()}
variant="contained"
color="primary"
>
+ Vencimento
</ButtonWithSpinner>
</Grid>
<Grid xs={6} md={1} item>
<ButtonWithSpinner
style={{ marginTop: 7 }}
className={classes.fullWidth}
loading={loading}
onClick={() => handleOpenModalUsers()}
variant="contained"
color="primary"
>
Usuário
</ButtonWithSpinner>
</Grid>
</>
) : null}
<Grid xs={6} md={1} item>
<ButtonWithSpinner
className={classes.fullWidth}
style={{ marginTop: 7 }}
loading={loading}
type="submit"
variant="contained"
color="primary"
>
Salvar
</ButtonWithSpinner>
</Grid>
</Grid>
</Grid>
</Grid>
</Form>
)}
</Formik>
</>
);
}
export function CompaniesManagerGrid(props) {
const { records, onSelect } = props;
const classes = useStyles();
const { dateToClient } = useDate();
const renderStatus = (row) => {
return row.status === false ? "Não" : "Sim";
};
const renderPlan = (row) => {
return row.planId !== null ? row.plan.name : "-";
};
const renderCampaignsStatus = (row) => {
if (
has(row, "settings") &&
isArray(row.settings) &&
row.settings.length > 0
) {
const setting = row.settings.find((s) => s.key === "campaignsEnabled");
if (setting) {
return setting.value === "true" ? "Habilitadas" : "Desabilitadas";
}
}
return "Desabilitadas";
};
const rowStyle = (record) => {
if (moment(record.dueDate).isValid()) {
const now = moment();
const dueDate = moment(record.dueDate);
const diff = dueDate.diff(now, "days");
if (diff === 5) {
return { backgroundColor: "#fffead" };
}
if (diff >= -3 && diff <= 4) {
return { backgroundColor: "#f7cc8f" };
}
if (diff === -4) {
return { backgroundColor: "#fa8c8c" };
}
}
return {};
};
return (
<Paper className={classes.tableContainer}>
<Table
className={classes.fullWidth}
size="small"
aria-label="a dense table"
>
<TableHead>
<TableRow>
<TableCell align="center" style={{ width: "1%" }}>
#
</TableCell>
<TableCell align="left">Nome</TableCell>
<TableCell align="left">E-mail</TableCell>
<TableCell align="left">Telefone</TableCell>
<TableCell align="left">Plano</TableCell>
<TableCell align="left">Campanhas</TableCell>
<TableCell align="left">Status</TableCell>
<TableCell align="left">Criada Em</TableCell>
<TableCell align="left">Vencimento</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map((row, key) => (
<TableRow style={rowStyle(row)} key={key}>
<TableCell align="center" style={{ width: "1%" }}>
<IconButton onClick={() => onSelect(row)} aria-label="delete">
<EditIcon />
</IconButton>
</TableCell>
<TableCell align="left">{row.name || "-"}</TableCell>
<TableCell align="left">{row.email || "-"}</TableCell>
<TableCell align="left">{row.phone || "-"}</TableCell>
<TableCell align="left">{renderPlan(row)}</TableCell>
<TableCell align="left">{renderCampaignsStatus(row)}</TableCell>
<TableCell align="left">{renderStatus(row)}</TableCell>
<TableCell align="left">{dateToClient(row.createdAt)}</TableCell>
<TableCell align="left">
{dateToClient(row.dueDate)}
<br />
<span>{row.recurrence}</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Paper>
);
}
export default function CompaniesManager() {
const classes = useStyles();
const { list, save, update, remove } = useCompanies();
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [loading, setLoading] = useState(false);
const [records, setRecords] = useState([]);
const [record, setRecord] = useState({
name: "",
email: "",
phone: "",
planId: "",
status: true,
campaignsEnabled: false,
dueDate: "",
recurrence: "",
});
useEffect(() => {
loadPlans();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const loadPlans = async () => {
setLoading(true);
try {
const companyList = await list();
setRecords(companyList);
} catch (e) {
toast.error("Não foi possível carregar a lista de registros");
}
setLoading(false);
};
const handleSubmit = async (data) => {
setLoading(true);
try {
if (data.id !== undefined) {
await update(data);
} else {
await save(data);
}
await loadPlans();
handleCancel();
toast.success("Operação realizada com sucesso!");
} catch (e) {
toast.error(
"Não foi possível realizar a operação. Verifique se já existe uma empresa com o mesmo nome ou se os campos foram preenchidos corretamente"
);
}
setLoading(false);
};
const handleDelete = async () => {
setLoading(true);
try {
await remove(record.id);
await loadPlans();
handleCancel();
toast.success("Operação realizada com sucesso!");
} catch (e) {
toast.error("Não foi possível realizar a operação");
}
setLoading(false);
};
const handleOpenDeleteDialog = () => {
setShowConfirmDialog(true);
};
const handleCancel = () => {
setRecord((prev) => ({
...prev,
name: "",
email: "",
phone: "",
planId: "",
status: true,
campaignsEnabled: false,
dueDate: "",
recurrence: "",
}));
};
const handleSelect = (data) => {
let campaignsEnabled = false;
const setting = data.settings.find(
(s) => s.key.indexOf("campaignsEnabled") > -1
);
if (setting) {
campaignsEnabled =
setting.value === "true" || setting.value === "enabled";
}
setRecord((prev) => ({
...prev,
id: data.id,
name: data.name || "",
phone: data.phone || "",
email: data.email || "",
planId: data.planId || "",
status: data.status === false ? false : true,
campaignsEnabled,
dueDate: data.dueDate || "",
recurrence: data.recurrence || "",
}));
};
return (
<Paper className={classes.mainPaper} elevation={0}>
<Grid spacing={2} container>
<Grid xs={12} item>
<CompanyForm
initialValue={record}
onDelete={handleOpenDeleteDialog}
onSubmit={handleSubmit}
onCancel={handleCancel}
loading={loading}
/>
</Grid>
<Grid xs={12} item>
<CompaniesManagerGrid records={records} onSelect={handleSelect} />
</Grid>
</Grid>
<ConfirmationModal
title="Exclusão de Registro"
open={showConfirmDialog}
onClose={() => setShowConfirmDialog(false)}
onConfirm={() => handleDelete()}
>
Deseja realmente excluir esse registro?
</ConfirmationModal>
</Paper>
);
}
@@ -0,0 +1,45 @@
import React from "react";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import Typography from "@material-ui/core/Typography";
import { i18n } from "../../translate/i18n";
const ConfirmationModal = ({ title, children, open, onClose, onConfirm }) => {
return (
<Dialog
open={open}
onClose={() => onClose(false)}
aria-labelledby="confirm-dialog"
>
<DialogTitle id="confirm-dialog">{title}</DialogTitle>
<DialogContent dividers>
<Typography>{children}</Typography>
</DialogContent>
<DialogActions>
<Button
variant="contained"
onClick={() => onClose(false)}
color="default"
>
{i18n.t("confirmationModal.buttons.cancel")}
</Button>
<Button
variant="contained"
onClick={() => {
onClose(false);
onConfirm();
}}
color="secondary"
>
{i18n.t("confirmationModal.buttons.confirm")}
</Button>
</DialogActions>
</Dialog>
);
};
export default ConfirmationModal;
@@ -0,0 +1,199 @@
import React, { useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton";
import CloseIcon from "@material-ui/icons/Close";
import Drawer from "@material-ui/core/Drawer";
import Link from "@material-ui/core/Link";
import InputLabel from "@material-ui/core/InputLabel";
import Avatar from "@material-ui/core/Avatar";
import Button from "@material-ui/core/Button";
import Paper from "@material-ui/core/Paper";
import CreateIcon from '@material-ui/icons/Create';
import { i18n } from "../../translate/i18n";
import ContactDrawerSkeleton from "../ContactDrawerSkeleton";
import MarkdownWrapper from "../MarkdownWrapper";
import { CardHeader } from "@material-ui/core";
import { ContactForm } from "../ContactForm";
import ContactModal from "../ContactModal";
import { ContactNotes } from "../ContactNotes";
const drawerWidth = 320;
const useStyles = makeStyles(theme => ({
drawer: {
width: drawerWidth,
flexShrink: 0,
},
drawerPaper: {
width: drawerWidth,
display: "flex",
borderTop: "1px solid rgba(0, 0, 0, 0.12)",
borderRight: "1px solid rgba(0, 0, 0, 0.12)",
borderBottom: "1px solid rgba(0, 0, 0, 0.12)",
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
header: {
display: "flex",
borderBottom: "1px solid rgba(0, 0, 0, 0.12)",
backgroundColor: theme.palette.contactdrawer, //DARK MODE PLW DESIGN//
alignItems: "center",
padding: theme.spacing(0, 1),
minHeight: "73px",
justifyContent: "flex-start",
},
content: {
display: "flex",
backgroundColor: theme.palette.contactdrawer, //DARK MODE PLW DESIGN//
flexDirection: "column",
padding: "8px 0px 8px 8px",
height: "100%",
overflowY: "scroll",
...theme.scrollbarStyles,
},
contactAvatar: {
margin: 15,
width: 100,
height: 100,
},
contactHeader: {
display: "flex",
padding: 8,
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
"& > *": {
margin: 4,
},
},
contactDetails: {
marginTop: 8,
padding: 8,
display: "flex",
flexDirection: "column",
},
contactExtraInfo: {
marginTop: 4,
padding: 6,
},
}));
const ContactDrawer = ({ open, handleDrawerClose, contact, ticket, loading }) => {
const classes = useStyles();
const [modalOpen, setModalOpen] = useState(false);
const [openForm, setOpenForm] = useState(false);
useEffect(() => {
setOpenForm(false);
}, [open, contact]);
return (
<>
<Drawer
className={classes.drawer}
variant="persistent"
anchor="right"
open={open}
PaperProps={{ style: { position: "absolute" } }}
BackdropProps={{ style: { position: "absolute" } }}
ModalProps={{
container: document.getElementById("drawer-container"),
style: { position: "absolute" },
}}
classes={{
paper: classes.drawerPaper,
}}
>
<div className={classes.header}>
<IconButton onClick={handleDrawerClose}>
<CloseIcon />
</IconButton>
<Typography style={{ justifySelf: "center" }}>
{i18n.t("contactDrawer.header")}
</Typography>
</div>
{loading ? (
<ContactDrawerSkeleton classes={classes} />
) : (
<div className={classes.content}>
<Paper square variant="outlined" className={classes.contactHeader}>
<CardHeader
onClick={() => {}}
style={{ cursor: "pointer", width: '100%' }}
titleTypographyProps={{ noWrap: true }}
subheaderTypographyProps={{ noWrap: true }}
avatar={<Avatar src={contact.profilePicUrl} alt="contact_image" style={{ width: 60, height: 60 }} />}
title={
<>
<Typography onClick={() => setOpenForm(true)}>
{contact.name}
<CreateIcon style={{fontSize: 16, marginLeft: 5}} />
</Typography>
</>
}
subheader={
<>
<Typography style={{fontSize: 12}}>
<Link href={`tel:${contact.number}`}>{contact.number}</Link>
</Typography>
<Typography style={{fontSize: 12}}>
<Link href={`mailto:${contact.email}`}>{contact.email}</Link>
</Typography>
</>
}
/>
<Button
variant="outlined"
color="primary"
onClick={() => setModalOpen(!openForm)}
style={{fontSize: 12}}
>
{i18n.t("contactDrawer.buttons.edit")}
</Button>
{(contact.id && openForm) && <ContactForm initialContact={contact} onCancel={() => setOpenForm(false)} />}
</Paper>
<Paper square variant="outlined" className={classes.contactDetails}>
<Typography variant="subtitle1" style={{marginBottom: 10}}>
{i18n.t("ticketOptionsMenu.appointmentsModal.title")}
</Typography>
<ContactNotes ticket={ticket} />
</Paper>
<Paper square variant="outlined" className={classes.contactDetails}>
<ContactModal
open={modalOpen}
onClose={() => setModalOpen(false)}
contactId={contact.id}
></ContactModal>
<Typography variant="subtitle1">
{i18n.t("contactDrawer.extraInfo")}
</Typography>
{contact?.extraInfo?.map(info => (
<Paper
key={info.id}
square
variant="outlined"
className={classes.contactExtraInfo}
>
<InputLabel>{info.name}</InputLabel>
<Typography component="div" noWrap style={{ paddingTop: 2 }}>
<MarkdownWrapper>{info.value}</MarkdownWrapper>
</Typography>
</Paper>
))}
</Paper>
</div>
)}
</Drawer>
</>
);
};
export default ContactDrawer;
@@ -0,0 +1,50 @@
import React from "react";
import Skeleton from "@material-ui/lab/Skeleton";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
import { i18n } from "../../translate/i18n";
import { Grid } from "@material-ui/core";
const ContactDrawerSkeleton = ({ classes }) => {
return (
<div className={classes.content}>
<Paper square variant="outlined" className={classes.contactHeader}>
<Grid container>
<Grid item>
<Skeleton
animation="wave"
variant="circle"
width={60}
height={60}
className={classes.contactAvatar}
/>
</Grid>
<Grid item>
<Skeleton animation="wave" height={25} width={90} />
<Skeleton animation="wave" height={25} width={80} />
<Skeleton animation="wave" height={25} width={80} />
</Grid>
</Grid>
</Paper>
<Paper square className={classes.contactDetails}>
<Typography variant="subtitle1">
{i18n.t("contactDrawer.extraInfo")}
</Typography>
<Paper square variant="outlined" className={classes.contactExtraInfo}>
<Skeleton animation="wave" height={20} width={60} />
<Skeleton animation="wave" height={20} width={160} />
</Paper>
<Paper square variant="outlined" className={classes.contactExtraInfo}>
<Skeleton animation="wave" height={20} width={60} />
<Skeleton animation="wave" height={20} width={160} />
</Paper>
<Paper square variant="outlined" className={classes.contactExtraInfo}>
<Skeleton animation="wave" height={20} width={60} />
<Skeleton animation="wave" height={20} width={160} />
</Paper>
</Paper>
</div>
);
};
export default ContactDrawerSkeleton;
@@ -0,0 +1,187 @@
import React, { useState, useEffect } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import CircularProgress from "@material-ui/core/CircularProgress";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
import { Grid } from "@material-ui/core";
const useStyles = makeStyles(theme => ({
root: {
display: "flex",
flexWrap: "wrap",
},
textField: {
marginRight: theme.spacing(1),
flex: 1,
},
extraAttr: {
display: "flex",
justifyContent: "center",
alignItems: "center",
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
textCenter: {
backgroundColor: 'red'
}
}));
const ContactSchema = Yup.object().shape({
name: Yup.string()
.min(2, "Too Short!")
.max(50, "Too Long!")
.required("Required"),
number: Yup.string().min(8, "Too Short!").max(50, "Too Long!"),
email: Yup.string().email("Invalid email"),
});
export function ContactForm ({ initialContact, onSave, onCancel }) {
const classes = useStyles();
const [contact, setContact] = useState(initialContact);
useEffect(() => {
setContact(initialContact);
}, [initialContact]);
const handleSaveContact = async values => {
try {
if (contact.id) {
await api.put(`/contacts/${contact.id}`, values);
} else {
const { data } = await api.post("/contacts", values);
if (onSave) {
onSave(data);
}
}
toast.success(i18n.t("contactModal.success"));
} catch (err) {
toastError(err);
}
};
return (
<Formik
initialValues={contact}
enableReinitialize={true}
validationSchema={ContactSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveContact(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ values, errors, touched, isSubmitting }) => (
<Form>
<Grid container spacing={1}>
{/* <Grid item xs={12}>
<Typography variant="subtitle1" gutterBottom>
{i18n.t("contactModal.form.mainInfo")}
</Typography>
</Grid> */}
<Grid item xs={12}>
<Field
as={TextField}
label={i18n.t("contactModal.form.name")}
name="name"
autoFocus
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
className={classes.textField}
fullWidth
/>
</Grid>
<Grid item xs={12}>
<Field
as={TextField}
label={i18n.t("contactModal.form.number")}
name="number"
error={touched.number && Boolean(errors.number)}
helperText={touched.number && errors.number}
placeholder="5513912344321"
variant="outlined"
margin="dense"
fullWidth
/>
</Grid>
<Grid item xs={12}>
<Field
as={TextField}
label={i18n.t("contactModal.form.email")}
name="email"
error={touched.email && Boolean(errors.email)}
helperText={touched.email && errors.email}
placeholder="Email address"
fullWidth
margin="dense"
variant="outlined"
/>
</Grid>
<Grid item xs={12} spacing={1}>
<Grid container spacing={1}>
<Grid xs={6} item>
<Button
onClick={onCancel}
color="secondary"
disabled={isSubmitting}
variant="outlined"
fullWidth
>
{i18n.t("contactModal.buttons.cancel")}
</Button>
</Grid>
<Grid classes={classes.textCenter} xs={6} item>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
fullWidth
>
{contact.id
? `${i18n.t("contactModal.buttons.okEdit")}`
: `${i18n.t("contactModal.buttons.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</Grid>
</Grid>
</Grid>
</Grid>
</Form>
)}
</Formik>
)
}
@@ -0,0 +1,181 @@
import React, { useState, useEffect } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import CircularProgress from "@material-ui/core/CircularProgress";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
flexWrap: "wrap",
},
multFieldLine: {
display: "flex",
"& > *:not(:last-child)": {
marginRight: theme.spacing(1),
},
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
}));
const ContactListSchema = Yup.object().shape({
name: Yup.string()
.min(2, "Too Short!")
.max(50, "Too Long!")
.required("Required"),
});
const ContactListModal = ({ open, onClose, contactListId }) => {
const classes = useStyles();
const initialState = {
name: "",
};
const [contactList, setContactList] = useState(initialState);
useEffect(() => {
const fetchContactList = async () => {
if (!contactListId) return;
try {
const { data } = await api.get(`/contact-lists/${contactListId}`);
setContactList((prevState) => {
return { ...prevState, ...data };
});
} catch (err) {
toastError(err);
}
};
fetchContactList();
}, [contactListId, open]);
const handleClose = () => {
onClose();
setContactList(initialState);
};
const handleSaveContactList = async (values) => {
const contactListData = { ...values };
try {
if (contactListId) {
await api.put(`/contact-lists/${contactListId}`, contactListData);
} else {
await api.post("/contact-lists", contactListData);
}
toast.success(i18n.t("contactList.dialog"));
} catch (err) {
toastError(err);
}
handleClose();
};
return (
<div className={classes.root}>
<Dialog
open={open}
onClose={handleClose}
maxWidth="xs"
fullWidth
scroll="paper"
>
<DialogTitle id="form-dialog-title">
{contactListId
? `${i18n.t("contactLists.dialog.edit")}`
: `${i18n.t("contactLists.dialog.add")}`}
</DialogTitle>
<Formik
initialValues={contactList}
enableReinitialize={true}
validationSchema={ContactListSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveContactList(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting }) => (
<Form>
<DialogContent dividers>
<div className={classes.multFieldLine}>
<Field
as={TextField}
label={i18n.t("contactLists.dialog.name")}
autoFocus
name="name"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
fullWidth
/>
</div>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("contactLists.dialog.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{contactListId
? `${i18n.t("contactLists.dialog.okEdit")}`
: `${i18n.t("contactLists.dialog.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default ContactListModal;
@@ -0,0 +1,242 @@
import React, { useState, useEffect, useRef, useContext } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import Typography from "@material-ui/core/Typography";
import CircularProgress from "@material-ui/core/CircularProgress";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
import { useParams } from "react-router-dom";
import { AuthContext } from "../../context/Auth/AuthContext";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
flexWrap: "wrap",
},
textField: {
marginRight: theme.spacing(1),
flex: 1,
},
extraAttr: {
display: "flex",
justifyContent: "center",
alignItems: "center",
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
}));
const ContactSchema = Yup.object().shape({
name: Yup.string()
.min(2, "Too Short!")
.max(50, "Too Long!")
.required("Required"),
number: Yup.string().min(8, "Too Short!").max(50, "Too Long!"),
email: Yup.string().email("Invalid email"),
});
const ContactListItemModal = ({
open,
onClose,
contactId,
initialValues,
onSave,
}) => {
const classes = useStyles();
const isMounted = useRef(true);
const {
user: { companyId },
} = useContext(AuthContext);
const { contactListId } = useParams();
const initialState = {
name: "",
number: "",
email: "",
};
const [contact, setContact] = useState(initialState);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
const fetchContact = async () => {
if (initialValues) {
setContact((prevState) => {
return { ...prevState, ...initialValues };
});
}
if (!contactId) return;
try {
const { data } = await api.get(`/contact-list-items/${contactId}`);
if (isMounted.current) {
setContact(data);
}
} catch (err) {
toastError(err);
}
};
fetchContact();
}, [contactId, open, initialValues]);
const handleClose = () => {
onClose();
setContact(initialState);
};
const handleSaveContact = async (values) => {
try {
if (contactId) {
await api.put(`/contact-list-items/${contactId}`, {
...values,
companyId,
contactListId,
});
handleClose();
} else {
const { data } = await api.post("/contact-list-items", {
...values,
companyId,
contactListId,
});
if (onSave) {
onSave(data);
}
handleClose();
}
toast.success(i18n.t("contactModal.success"));
} catch (err) {
toastError(err);
}
};
return (
<div className={classes.root}>
<Dialog open={open} onClose={handleClose} maxWidth="lg" scroll="paper">
<DialogTitle id="form-dialog-title">
{contactId
? `${i18n.t("contactModal.title.edit")}`
: `${i18n.t("contactModal.title.add")}`}
</DialogTitle>
<Formik
initialValues={contact}
enableReinitialize={true}
validationSchema={ContactSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveContact(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ values, errors, touched, isSubmitting }) => (
<Form>
<DialogContent dividers>
<Typography variant="subtitle1" gutterBottom>
{i18n.t("contactModal.form.mainInfo")}
</Typography>
<Field
as={TextField}
label={i18n.t("contactModal.form.name")}
name="name"
autoFocus
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
className={classes.textField}
/>
<Field
as={TextField}
label={i18n.t("contactModal.form.number")}
name="number"
error={touched.number && Boolean(errors.number)}
helperText={touched.number && errors.number}
placeholder="5513912344321"
variant="outlined"
margin="dense"
/>
<div>
<Field
as={TextField}
label={i18n.t("contactModal.form.email")}
name="email"
error={touched.email && Boolean(errors.email)}
helperText={touched.email && errors.email}
placeholder="Email address"
fullWidth
margin="dense"
variant="outlined"
/>
</div>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("contactModal.buttons.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{contactId
? `${i18n.t("contactModal.buttons.okEdit")}`
: `${i18n.t("contactModal.buttons.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default ContactListItemModal;
@@ -0,0 +1,103 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import {
Table,
TableHead,
TableBody,
TableCell,
TableRow,
IconButton,
} from "@material-ui/core";
import {
Edit as EditIcon,
DeleteOutline as DeleteOutlineIcon,
People as PeopleIcon,
} from "@material-ui/icons";
import TableRowSkeleton from "../../components/TableRowSkeleton";
function ContactListsTable(props) {
const {
contactLists,
showLoading,
editContactList,
deleteContactList,
readOnly,
} = props;
const [loading, setLoading] = useState(true);
const [rows, setRows] = useState([]);
useEffect(() => {
if (Array.isArray(contactLists)) {
setRows(contactLists);
}
if (showLoading !== undefined) {
setLoading(showLoading);
}
}, [contactLists, showLoading]);
const handleEdit = (contactList) => {
editContactList(contactList);
};
const handleDelete = (contactList) => {
deleteContactList(contactList);
};
const renderRows = () => {
return rows.map((contactList) => {
return (
<TableRow key={contactList.id}>
<TableCell align="left">{contactList.name}</TableCell>
<TableCell align="center"></TableCell>
{!readOnly ? (
<TableCell align="center">
<IconButton size="small">
<PeopleIcon />
</IconButton>
<IconButton size="small" onClick={() => handleEdit(contactList)}>
<EditIcon />
</IconButton>
<IconButton
size="small"
onClick={() => handleDelete(contactList)}
>
<DeleteOutlineIcon />
</IconButton>
</TableCell>
) : null}
</TableRow>
);
});
};
return (
<Table size="small">
<TableHead>
<TableRow>
<TableCell align="left">Nome</TableCell>
<TableCell align="center">Contatos</TableCell>
{!readOnly ? <TableCell align="center">Ações</TableCell> : null}
</TableRow>
</TableHead>
<TableBody>
{loading ? (
<TableRowSkeleton columns={readOnly ? 2 : 3} />
) : (
renderRows()
)}
</TableBody>
</Table>
);
}
ContactListsTable.propTypes = {
contactLists: PropTypes.array.isRequired,
showLoading: PropTypes.bool,
editContactList: PropTypes.func.isRequired,
deleteContactList: PropTypes.func.isRequired,
};
export default ContactListsTable;
@@ -0,0 +1,284 @@
import React, { useState, useEffect, useRef } from "react";
import * as Yup from "yup";
import { Formik, FieldArray, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import CircularProgress from "@material-ui/core/CircularProgress";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
const useStyles = makeStyles(theme => ({
root: {
display: "flex",
flexWrap: "wrap",
},
textField: {
marginRight: theme.spacing(1),
flex: 1,
},
extraAttr: {
display: "flex",
justifyContent: "center",
alignItems: "center",
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
}));
const ContactSchema = Yup.object().shape({
name: Yup.string()
.min(2, "Too Short!")
.max(50, "Too Long!")
.required("Required"),
number: Yup.string().min(8, "Too Short!").max(50, "Too Long!"),
email: Yup.string().email("Invalid email"),
});
const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
const classes = useStyles();
const isMounted = useRef(true);
const initialState = {
name: "",
number: "",
email: "",
};
const [contact, setContact] = useState(initialState);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
const fetchContact = async () => {
if (initialValues) {
setContact(prevState => {
return { ...prevState, ...initialValues };
});
}
if (!contactId) return;
try {
const { data } = await api.get(`/contacts/${contactId}`);
if (isMounted.current) {
console.log(data)
setContact(data);
}
} catch (err) {
toastError(err);
}
};
fetchContact();
}, [contactId, open, initialValues]);
const handleClose = () => {
onClose();
setContact(initialState);
};
const handleSaveContact = async values => {
try {
if (contactId) {
await api.put(`/contacts/${contactId}`, values);
handleClose();
} else {
const { data } = await api.post("/contacts", values);
if (onSave) {
onSave(data);
}
handleClose();
}
toast.success(i18n.t("contactModal.success"));
} catch (err) {
toastError(err);
}
};
return (
<div className={classes.root}>
<Dialog open={open} onClose={handleClose} maxWidth="lg" scroll="paper">
<DialogTitle id="form-dialog-title">
{contactId
? `${i18n.t("contactModal.title.edit")}`
: `${i18n.t("contactModal.title.add")}`}
</DialogTitle>
<Formik
initialValues={contact}
enableReinitialize={true}
validationSchema={ContactSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveContact(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ values, errors, touched, isSubmitting }) => (
<Form>
<DialogContent dividers>
<Typography variant="subtitle1" gutterBottom>
{i18n.t("contactModal.form.mainInfo")}
</Typography>
<Field
as={TextField}
label={i18n.t("contactModal.form.name")}
name="name"
autoFocus
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
className={classes.textField}
/>
<Field
as={TextField}
label={i18n.t("contactModal.form.number")}
name="number"
error={touched.number && Boolean(errors.number)}
helperText={touched.number && errors.number}
placeholder="5541998608485"
variant="outlined"
margin="dense"
/>
<div>
<Field
as={TextField}
label={i18n.t("contactModal.form.email")}
name="email"
error={touched.email && Boolean(errors.email)}
helperText={touched.email && errors.email}
placeholder="Email address"
fullWidth
margin="dense"
variant="outlined"
/>
</div>
<Typography
style={{ marginBottom: 8, marginTop: 12 }}
variant="subtitle1"
>
{i18n.t("contactModal.form.whatsapp")} {contact?.whatsapp ? contact?.whatsapp.name : ""}
</Typography>
<Typography
style={{ marginBottom: 8, marginTop: 12 }}
variant="subtitle1"
>
{i18n.t("contactModal.form.extraInfo")}
</Typography>
<FieldArray name="extraInfo">
{({ push, remove }) => (
<>
{values.extraInfo &&
values.extraInfo.length > 0 &&
values.extraInfo.map((info, index) => (
<div
className={classes.extraAttr}
key={`${index}-info`}
>
<Field
as={TextField}
label={i18n.t("contactModal.form.extraName")}
name={`extraInfo[${index}].name`}
variant="outlined"
margin="dense"
className={classes.textField}
/>
<Field
as={TextField}
label={i18n.t("contactModal.form.extraValue")}
name={`extraInfo[${index}].value`}
variant="outlined"
margin="dense"
className={classes.textField}
/>
<IconButton
size="small"
onClick={() => remove(index)}
>
<DeleteOutlineIcon />
</IconButton>
</div>
))}
<div className={classes.extraAttr}>
<Button
style={{ flex: 1, marginTop: 8 }}
variant="outlined"
color="primary"
onClick={() => push({ name: "", value: "" })}
>
{`+ ${i18n.t("contactModal.buttons.addExtraInfo")}`}
</Button>
</div>
</>
)}
</FieldArray>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("contactModal.buttons.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{contactId
? `${i18n.t("contactModal.buttons.okEdit")}`
: `${i18n.t("contactModal.buttons.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default ContactModal;
@@ -0,0 +1,204 @@
import React, { useState, useEffect } from 'react';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import List from '@material-ui/core/List';
import { makeStyles } from '@material-ui/core/styles';
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import ContactNotesDialogListItem from '../ContactNotesDialogListItem';
import ConfirmationModal from '../ConfirmationModal';
import { toast } from "react-toastify";
import { i18n } from "../../translate/i18n";
import ButtonWithSpinner from '../ButtonWithSpinner';
import useTicketNotes from '../../hooks/useTicketNotes';
import { Grid } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
'& .MuiTextField-root': {
margin: theme.spacing(1),
width: '350px',
},
},
list: {
width: '100%',
maxWidth: '350px',
maxHeight: '200px',
backgroundColor: theme.palette.background.paper,
overflow: 'auto'
},
inline: {
width: '100%'
}
}));
const NoteSchema = Yup.object().shape({
note: Yup.string()
.min(2, "Too Short!")
.required("Required")
});
export function ContactNotes ({ ticket }) {
const { id: ticketId, contactId } = ticket
const classes = useStyles()
const [newNote, setNewNote] = useState({ note: "" });
const [loading, setLoading] = useState(false)
const [showOnDeleteDialog, setShowOnDeleteDialog] = useState(false)
const [selectedNote, setSelectedNote] = useState({})
const [notes, setNotes] = useState([])
const { saveNote, deleteNote, listNotes } = useTicketNotes()
useEffect(() => {
async function openAndFetchData () {
handleResetState()
await loadNotes()
}
openAndFetchData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleResetState = () => {
setNewNote({ note: "" })
setLoading(false)
}
const handleChangeComment = (e) => {
setNewNote({ note: e.target.value })
}
const handleSave = async values => {
setLoading(true)
try {
await saveNote({
...values,
ticketId,
contactId
})
await loadNotes()
setNewNote({ note: '' })
toast.success('Observação adicionada com sucesso!')
} catch (e) {
toast.error(e)
}
setLoading(false)
}
const handleOpenDialogDelete = (item) => {
setSelectedNote(item)
setShowOnDeleteDialog(true)
}
const handleDelete = async () => {
setLoading(true)
try {
await deleteNote(selectedNote.id)
await loadNotes()
setSelectedNote({})
toast.success('Observação excluída com sucesso!')
} catch (e) {
toast.error(e)
}
setLoading(false)
}
const loadNotes = async () => {
setLoading(true)
try {
const notes = await listNotes({ ticketId, contactId })
setNotes(notes)
} catch (e) {
toast.error(e)
}
setLoading(false)
}
const renderNoteList = () => {
return notes.map((note) => {
return <ContactNotesDialogListItem
note={note}
key={note.id}
deleteItem={handleOpenDialogDelete}
/>
})
}
return (
<>
<ConfirmationModal
title="Excluir Registro"
open={showOnDeleteDialog}
onClose={setShowOnDeleteDialog}
onConfirm={handleDelete}
>
Deseja realmente excluir este registro?
</ConfirmationModal>
<Formik
initialValues={newNote}
enableReinitialize={true}
validationSchema={NoteSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSave(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, setErrors }) => (
<Form>
<Grid container spacing={2}>
<Grid xs={12} item>
<Field
as={TextField}
name="note"
rows={3}
label={i18n.t("ticketOptionsMenu.appointmentsModal.textarea")}
placeholder={i18n.t("ticketOptionsMenu.appointmentsModal.placeholder")}
multiline={true}
error={touched.note && Boolean(errors.note)}
helperText={touched.note && errors.note}
variant="outlined"
onChange={handleChangeComment}
fullWidth
/>
</Grid>
{ notes.length > 0 && (
<Grid xs={12} item>
<List className={classes.list}>
{ renderNoteList() }
</List>
</Grid>
) }
<Grid xs={12} item>
<Grid container spacing={2}>
<Grid xs={6} item>
<Button
onClick={() => {
setNewNote("");
setErrors({});
}}
color="primary"
variant="outlined"
fullWidth
>
Cancelar
</Button>
</Grid>
<Grid xs={6} item>
<ButtonWithSpinner loading={loading} color="primary" type="submit" variant="contained" autoFocus fullWidth>
Salvar
</ButtonWithSpinner>
</Grid>
</Grid>
</Grid>
</Grid>
</Form>
)}
</Formik>
</>
);
}
@@ -0,0 +1,206 @@
import React, { useState, useEffect } from 'react';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import TextField from '@material-ui/core/TextField';
import List from '@material-ui/core/List';
import { makeStyles } from '@material-ui/core/styles';
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import ContactNotesDialogListItem from '../ContactNotesDialogListItem';
import ConfirmationModal from '../ConfirmationModal';
import { toast } from "react-toastify";
import { i18n } from "../../translate/i18n";
import ButtonWithSpinner from '../ButtonWithSpinner';
import useTicketNotes from '../../hooks/useTicketNotes';
const useStyles = makeStyles((theme) => ({
root: {
'& .MuiTextField-root': {
margin: theme.spacing(1),
width: '350px',
},
},
list: {
width: '100%',
maxWidth: '350px',
maxHeight: '200px',
backgroundColor: theme.palette.background.paper,
},
inline: {
width: '100%'
}
}));
const NoteSchema = Yup.object().shape({
note: Yup.string()
.min(2, "Too Short!")
.required("Required")
});
export default function ContactNotesDialog ({ modalOpen, onClose, ticket }) {
const { id: ticketId, contactId } = ticket
const classes = useStyles()
const [open, setOpen] = useState(false);
const [newNote, setNewNote] = useState({ note: "" });
const [loading, setLoading] = useState(false)
const [showOnDeleteDialog, setShowOnDeleteDialog] = useState(false)
const [selectedNote, setSelectedNote] = useState({})
const [notes, setNotes] = useState([])
const { saveNote, deleteNote, listNotes } = useTicketNotes()
useEffect(() => {
async function openAndFetchData () {
if (modalOpen) {
setOpen(true)
handleResetState()
await loadNotes()
}
}
openAndFetchData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [modalOpen])
const handleResetState = () => {
setNewNote({ note: "" })
setLoading(false)
}
const handleChangeComment = (e) => {
setNewNote({ note: e.target.value })
}
const handleClose = () => {
setOpen(false);
onClose()
};
const handleSave = async values => {
setLoading(true)
try {
await saveNote({
...values,
ticketId,
contactId
})
await loadNotes()
setNewNote({ note: '' })
toast.success('Observação adicionada com sucesso!')
} catch (e) {
toast.error(e)
}
setLoading(false)
}
const handleOpenDialogDelete = (item) => {
setSelectedNote(item)
setShowOnDeleteDialog(true)
}
const handleDelete = async () => {
setLoading(true)
try {
await deleteNote(selectedNote.id)
await loadNotes()
setSelectedNote({})
toast.success('Observação excluída com sucesso!')
} catch (e) {
toast.error(e)
}
setLoading(false)
}
const loadNotes = async () => {
setLoading(true)
try {
const notes = await listNotes({ ticketId, contactId })
setNotes(notes)
} catch (e) {
toast.error(e)
}
setLoading(false)
}
const renderNoteList = () => {
return notes.map((note) => {
return <ContactNotesDialogListItem
note={note}
key={note.id}
deleteItem={handleOpenDialogDelete}
/>
})
}
return (
<>
<ConfirmationModal
title="Excluir Registro"
open={showOnDeleteDialog}
onClose={setShowOnDeleteDialog}
onConfirm={handleDelete}
>
Deseja realmente excluir este registro?
</ConfirmationModal>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{ i18n.t("ticketOptionsMenu.appointmentsModal.title") }
</DialogTitle>
<Formik
initialValues={newNote}
enableReinitialize={true}
validationSchema={NoteSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSave(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors }) => (
<Form>
<DialogContent className={classes.root} dividers>
<Field
as={TextField}
name="note"
rows={3}
label={i18n.t("ticketOptionsMenu.appointmentsModal.textarea")}
placeholder={i18n.t("ticketOptionsMenu.appointmentsModal.placeholder")}
multiline={true}
error={touched.note && Boolean(errors.note)}
helperText={touched.note && errors.note}
variant="outlined"
onChange={handleChangeComment}
/>
<List className={classes.list}>
{ renderNoteList() }
</List>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Fechar
</Button>
<ButtonWithSpinner loading={loading} color="primary" type="submit" variant="contained" autoFocus>
Salvar
</ButtonWithSpinner>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</>
);
}
@@ -0,0 +1,64 @@
import React from 'react';
import PropTypes from 'prop-types';
import IconButton from '@material-ui/core/IconButton';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles';
import DeleteIcon from '@material-ui/icons/Delete';
import moment from 'moment';
const useStyles = makeStyles((theme) => ({
inline: {
width: '100%'
}
}));
export default function ContactNotesDialogListItem (props) {
const { note, deleteItem } = props;
const classes = useStyles();
const handleDelete = (item) => {
deleteItem(item);
}
return (
<ListItem alignItems="flex-start">
<ListItemAvatar>
<Avatar alt={note.user.name} src="/static/images/avatar/1.jpg" />
</ListItemAvatar>
<ListItemText
primary={
<>
<Typography
component="span"
variant="body2"
className={classes.inline}
color="textPrimary"
>
{note.note}
</Typography>
</>
}
secondary={
<>
{note.user.name}, {moment(note.createdAt).format('DD/MM/YY HH:mm')}
</>
}
/>
<ListItemSecondaryAction>
<IconButton onClick={() => handleDelete(note)} edge="end" aria-label="delete">
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
)
}
ContactNotesDialogListItem.propTypes = {
note: PropTypes.object.isRequired,
deleteItem: PropTypes.func.isRequired
}
@@ -0,0 +1,26 @@
import { makeStyles } from "@material-ui/styles";
import React from "react";
const useStyles = makeStyles(theme => ({
tag: {
padding: "1px 5px",
borderRadius: "3px",
fontSize: "0.8em",
fontWeight: "bold",
color: "#FFF",
marginRight: "5px",
whiteSpace: "nowrap"
}
}));
const ContactTag = ({ tag }) => {
const classes = useStyles();
return (
<div className={classes.tag} style={{ backgroundColor: tag.color, marginTop: "2px" }}>
{tag.name.toUpperCase()}
</div>
)
}
export default ContactTag;
@@ -0,0 +1,50 @@
import React from 'react'
import PropTypes from 'prop-types'
import MaskedInput from 'react-text-mask'
import createNumberMask from 'text-mask-addons/dist/createNumberMask'
const defaultMaskOptions = {
prefix: 'R$',
suffix: '',
includeThousandsSeparator: true,
thousandsSeparatorSymbol: '.',
allowDecimal: true,
decimalSymbol: ',',
decimalLimit: 2, // how many digits allowed after the decimal
integerLimit: 7, // limit length of integer numbers
allowNegative: false,
allowLeadingZeroes: false,
}
const CurrencyInput = ({ maskOptions, ...inputProps }) => {
const currencyMask = createNumberMask({
...defaultMaskOptions,
...maskOptions,
})
return <MaskedInput mask={currencyMask} {...inputProps} />
}
CurrencyInput.defaultProps = {
inputMode: 'numeric',
maskOptions: {},
}
CurrencyInput.propTypes = {
inputmode: PropTypes.string,
maskOptions: PropTypes.shape({
prefix: PropTypes.string,
suffix: PropTypes.string,
includeThousandsSeparator: PropTypes.bool,
thousandsSeparatorSymbol: PropTypes.string,
allowDecimal: PropTypes.bool,
decimalSymbol: PropTypes.string,
decimalLimit: PropTypes.string,
requireDecimal: PropTypes.bool,
allowNegative: PropTypes.bool,
allowLeadingZeroes: PropTypes.bool,
integerLimit: PropTypes.number,
}),
}
export default CurrencyInput
+70
View File
@@ -0,0 +1,70 @@
import React, { useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import { CssBaseline, IconButton } from "@material-ui/core";
import Brightness4Icon from "@material-ui/icons/Brightness4";
import Brightness7Icon from "@material-ui/icons/Brightness7";
const useStyles = makeStyles((theme) => ({
icons: {
color: "#fff",
},
switch: {
color: "#fff",
},
visible: {
display: "none",
},
btnHeader: {
color: "#fff",
},
}));
const DarkMode = (props) => {
const classes = useStyles();
const [theme, setTheme] = useState("light");
const themeToggle = () => {
theme === "light" ? setTheme("dark") : setTheme("light");
};
const handleClick = () => {
props.themeToggle();
themeToggle();
};
return (
<>
{theme === "light" ? (
<>
<CssBaseline />
<IconButton
className={classes.icons}
onClick={handleClick}
// ref={anchorEl}
aria-label="Dark Mode"
color="inherit"
>
<Brightness4Icon />
</IconButton>
</>
) : (
<>
<CssBaseline />
<IconButton
className={classes.icons}
onClick={handleClick}
// ref={anchorEl}
aria-label="Dark Mode"
color="inherit"
>
<Brightness7Icon />
</IconButton>
</>
)}
</>
);
};
export default DarkMode;
@@ -0,0 +1,53 @@
import React from "react";
import { Avatar, Card, CardHeader, Typography } from "@material-ui/core";
import Skeleton from "@material-ui/lab/Skeleton";
import { makeStyles } from "@material-ui/core/styles";
import { grey } from '@material-ui/core/colors';
const useStyles = makeStyles(theme => ({
cardAvatar: {
fontSize: '55px',
color: grey[500],
backgroundColor: '#ffffff',
width: theme.spacing(7),
height: theme.spacing(7)
},
cardTitle: {
fontSize: '18px',
color: theme.palette.text.primary
},
cardSubtitle: {
color: grey[600],
fontSize: '14px'
}
}));
export default function CardCounter(props) {
const { icon, title, value, loading } = props
const classes = useStyles();
return ( !loading ?
<Card>
<CardHeader
avatar={
<Avatar className={classes.cardAvatar}>
{icon}
</Avatar>
}
title={
<Typography variant="h6" component="h2" className={classes.cardTitle}>
{ title }
</Typography>
}
subheader={
<Typography variant="subtitle1" component="p" className={classes.cardSubtitle}>
{ value }
</Typography>
}
/>
</Card>
: <Skeleton variant="rect" height={80} />
)
}
@@ -0,0 +1,104 @@
import React from "react";
import Paper from "@material-ui/core/Paper";
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableContainer from '@material-ui/core/TableContainer';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Skeleton from "@material-ui/lab/Skeleton";
import { makeStyles } from "@material-ui/core/styles";
import { green, red } from '@material-ui/core/colors';
import CheckCircleIcon from '@material-ui/icons/CheckCircle';
import ErrorIcon from '@material-ui/icons/Error';
import moment from 'moment';
import Rating from '@material-ui/lab/Rating';
const useStyles = makeStyles(theme => ({
on: {
color: green[600],
fontSize: '20px'
},
off: {
color: red[600],
fontSize: '20px'
},
pointer: {
cursor: "pointer"
}
}));
export function RatingBox ({ rating }) {
const ratingTrunc = rating === null ? 0 : Math.trunc(rating);
return <Rating
defaultValue={ratingTrunc}
max={3}
readOnly
/>
}
export default function TableAttendantsStatus(props) {
const { loading, attendants } = props
const classes = useStyles();
function renderList () {
return attendants.map((a, k) => (
<TableRow key={k}>
<TableCell>{a.name}</TableCell>
<TableCell align="center" title="1 - Insatisfeito, 2 - Satisfeito, 3 - Muito Satisfeito" className={classes.pointer}>
<RatingBox rating={a.rating} />
</TableCell>
<TableCell align="center">{formatTime(a.avgSupportTime, 2)}</TableCell>
<TableCell align="center">
{ a.online ?
<CheckCircleIcon className={classes.on} />
: <ErrorIcon className={classes.off} />
}
</TableCell>
</TableRow>
))
}
function formatTime(minutes){
return moment().startOf('day').add(minutes, 'minutes').format('HH[h] mm[m]');
}
return ( !loading ?
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Nome</TableCell>
<TableCell align="center">Avaliações</TableCell>
<TableCell align="center">T.M. de Atendimento</TableCell>
<TableCell align="center">Status (Atual)</TableCell>
</TableRow>
</TableHead>
<TableBody>
{ renderList() }
{/* <TableRow>
<TableCell>Nome 4</TableCell>
<TableCell align="center">10</TableCell>
<TableCell align="center">10 minutos</TableCell>
<TableCell align="center">
<CheckCircleIcon className={classes.off} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Nome 5</TableCell>
<TableCell align="center">10</TableCell>
<TableCell align="center">10 minutos</TableCell>
<TableCell align="center">
<CheckCircleIcon className={classes.on} />
</TableCell>
</TableRow> */}
</TableBody>
</Table>
</TableContainer>
: <Skeleton variant="rect" height={150} />
)
}
+34
View File
@@ -0,0 +1,34 @@
import React, { useState, useEffect } from 'react';
import CoreDialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
function Dialog ({ title, modalOpen, onClose, children }) {
const [open, setOpen] = useState(false);
useEffect(() => {
setOpen(modalOpen)
}, [modalOpen])
const handleClose = () => {
setOpen(false);
onClose()
};
return (
<>
<CoreDialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{title}
</DialogTitle>
{children}
</CoreDialog>
</>
);
}
export default Dialog;
+350
View File
@@ -0,0 +1,350 @@
import React, { useState, useEffect, useContext } from "react";
import * as Yup from "yup";
import {
Formik,
Form,
Field,
FieldArray
} from "formik";
import { toast } from "react-toastify";
import {
Box,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
Grid,
makeStyles,
TextField
} from "@material-ui/core";
import IconButton from "@material-ui/core/IconButton";
import Typography from "@material-ui/core/Typography";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import AttachFileIcon from "@material-ui/icons/AttachFile";
import { green } from "@material-ui/core/colors";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
import { AuthContext } from "../../context/Auth/AuthContext";
const useStyles = makeStyles(theme => ({
root: {
display: "flex",
flexWrap: "wrap",
gap: 4
},
multFieldLine: {
display: "flex",
"& > *:not(:last-child)": {
marginRight: theme.spacing(1),
},
},
textField: {
marginRight: theme.spacing(1),
flex: 1,
},
extraAttr: {
display: "flex",
justifyContent: "center",
alignItems: "center",
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
formControl: {
margin: theme.spacing(1),
minWidth: 2000,
},
colorAdorment: {
width: 20,
height: 20,
},
}));
const FileListSchema = Yup.object().shape({
name: Yup.string()
.min(3, "nome muito curto")
.required("Obrigatório"),
message: Yup.string()
.required("Obrigatório")
});
const FilesModal = ({ open, onClose, fileListId, reload }) => {
const classes = useStyles();
const { user } = useContext(AuthContext);
const [ files, setFiles ] = useState([]);
const [selectedFileNames, setSelectedFileNames] = useState([]);
const initialState = {
name: "",
message: "",
options: [{ name: "", path:"", mediaType:"" }],
};
const [fileList, setFileList] = useState(initialState);
useEffect(() => {
try {
(async () => {
if (!fileListId) return;
const { data } = await api.get(`/files/${fileListId}`);
setFileList(data);
})()
} catch (err) {
toastError(err);
}
}, [fileListId, open]);
const handleClose = () => {
setFileList(initialState);
setFiles([]);
onClose();
};
const handleSaveFileList = async (values) => {
const uploadFiles = async (options, filesOptions, id) => {
const formData = new FormData();
formData.append("fileId", id);
formData.append("typeArch", "fileList")
filesOptions.forEach((fileOption, index) => {
if (fileOption.file) {
formData.append("files", fileOption.file);
formData.append("mediaType", fileOption.file.type)
formData.append("name", options[index].name);
formData.append("id", options[index].id);
}
});
try {
const { data } = await api.post(`/files/uploadList/${id}`, formData);
setFiles([]);
return data;
} catch (err) {
toastError(err);
}
return null;
}
const fileData = { ...values, userId: user.id };
try {
if (fileListId) {
const { data } = await api.put(`/files/${fileListId}`, fileData)
if (data.options.length > 0)
uploadFiles(data.options, values.options, fileListId)
} else {
const { data } = await api.post("/files", fileData);
if (data.options.length > 0)
uploadFiles(data.options, values.options, data.id)
}
toast.success(i18n.t("fileModal.success"));
if (typeof reload == 'function') {
reload();
}
} catch (err) {
toastError(err);
}
handleClose();
};
return (
<div className={classes.root}>
<Dialog
open={open}
onClose={handleClose}
maxWidth="md"
fullWidth
scroll="paper">
<DialogTitle id="form-dialog-title">
{(fileListId ? `${i18n.t("fileModal.title.edit")}` : `${i18n.t("fileModal.title.add")}`)}
</DialogTitle>
<Formik
initialValues={fileList}
enableReinitialize={true}
validationSchema={FileListSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveFileList(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting, values }) => (
<Form>
<DialogContent dividers>
<div className={classes.multFieldLine}>
<Field
as={TextField}
label={i18n.t("fileModal.form.name")}
name="name"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
fullWidth
/>
</div>
<br />
<div className={classes.multFieldLine}>
<Field
as={TextField}
label={i18n.t("fileModal.form.message")}
type="message"
multiline
minRows={5}
fullWidth
name="message"
error={
touched.message && Boolean(errors.message)
}
helperText={
touched.message && errors.message
}
variant="outlined"
margin="dense"
/>
</div>
<Typography
style={{ marginBottom: 8, marginTop: 12 }}
variant="subtitle1"
>
{i18n.t("fileModal.form.fileOptions")}
</Typography>
<FieldArray name="options">
{({ push, remove }) => (
<>
{values.options &&
values.options.length > 0 &&
values.options.map((info, index) => (
<div
className={classes.extraAttr}
key={`${index}-info`}
>
<Grid container spacing={0}>
<Grid xs={6} md={10} item>
<Field
as={TextField}
label={i18n.t("fileModal.form.extraName")}
name={`options[${index}].name`}
variant="outlined"
margin="dense"
multiline
fullWidth
minRows={2}
className={classes.textField}
/>
</Grid>
<Grid xs={2} md={2} item style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
<input
type="file"
onChange={(e) => {
const selectedFile = e.target.files[0];
const updatedOptions = [...values.options];
updatedOptions[index].file = selectedFile;
setFiles('options', updatedOptions);
// Atualize a lista selectedFileNames para o campo específico
const updatedFileNames = [...selectedFileNames];
updatedFileNames[index] = selectedFile ? selectedFile.name : '';
setSelectedFileNames(updatedFileNames);
}}
style={{ display: 'none' }}
name={`options[${index}].file`}
id={`file-upload-${index}`}
/>
<label htmlFor={`file-upload-${index}`}>
<IconButton component="span">
<AttachFileIcon />
</IconButton>
</label>
<IconButton
size="small"
onClick={() => remove(index)}
>
<DeleteOutlineIcon />
</IconButton>
</Grid>
<Grid xs={12} md={12} item>
{info.path? info.path : selectedFileNames[index]}
</Grid>
</Grid>
</div>
))}
<div className={classes.extraAttr}>
<Button
style={{ flex: 1, marginTop: 8 }}
variant="outlined"
color="primary"
onClick={() => {push({ name: "", path: ""});
setSelectedFileNames([...selectedFileNames, ""]);
}}
>
{`+ ${i18n.t("fileModal.buttons.fileOptions")}`}
</Button>
</div>
</>
)}
</FieldArray>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("fileModal.buttons.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{fileListId
? `${i18n.t("fileModal.buttons.okEdit")}`
: `${i18n.t("fileModal.buttons.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default FilesModal;
@@ -0,0 +1,38 @@
import React from 'react';
import { at } from 'lodash';
import { useField } from 'formik';
import {
Checkbox,
FormControl,
FormControlLabel,
FormHelperText
} from '@material-ui/core';
export default function CheckboxField(props) {
const { label, ...rest } = props;
const [field, meta, helper] = useField(props);
const { setValue } = helper;
function _renderHelperText() {
const [touched, error] = at(meta, 'touched', 'error');
if (touched && error) {
return <FormHelperText>{error}</FormHelperText>;
}
}
function _onChange(e) {
setValue(e.target.checked);
}
return (
<FormControl {...rest}>
<FormControlLabel
value={field.checked}
checked={field.checked}
control={<Checkbox {...field} onChange={_onChange} />}
label={label}
/>
{_renderHelperText()}
</FormControl>
);
}
@@ -0,0 +1,54 @@
import React, { useState, useEffect } from 'react';
import { useField } from 'formik';
import Grid from '@material-ui/core/Grid';
import {
MuiPickersUtilsProvider,
KeyboardDatePicker
} from '@material-ui/pickers';
import DateFnsUtils from '@date-io/date-fns';
export default function DatePickerField(props) {
const [field, meta, helper] = useField(props);
const { touched, error } = meta;
const { setValue } = helper;
const isError = touched && error && true;
const { value } = field;
const [selectedDate, setSelectedDate] = useState(null);
useEffect(() => {
if (value) {
const date = new Date(value);
setSelectedDate(date);
}
}, [value]);
function _onChange(date) {
if (date) {
setSelectedDate(date);
try {
const ISODateString = date.toISOString();
setValue(ISODateString);
} catch (error) {
setValue(date);
}
} else {
setValue(date);
}
}
return (
<Grid container>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDatePicker
{...field}
{...props}
value={selectedDate}
onChange={_onChange}
error={isError}
invalidDateMessage={isError && error}
helperText={isError && error}
/>
</MuiPickersUtilsProvider>
</Grid>
);
}
@@ -0,0 +1,26 @@
import React from 'react';
import { at } from 'lodash';
import { useField } from 'formik';
import { TextField } from '@material-ui/core';
export default function InputField(props) {
const { errorText, ...rest } = props;
const [field, meta] = useField(props);
function _renderHelperText() {
const [touched, error] = at(meta, 'touched', 'error');
if (touched && error) {
return error;
}
}
return (
<TextField
type="text"
error={meta.touched && meta.error && true}
helperText={_renderHelperText()}
{...field}
{...rest}
/>
);
}
@@ -0,0 +1,48 @@
import React from 'react';
import PropTypes from 'prop-types';
import { at } from 'lodash';
import { useField } from 'formik';
import {
InputLabel,
FormControl,
Select,
MenuItem,
FormHelperText
} from '@material-ui/core';
function SelectField(props) {
const { label, data, ...rest } = props;
const [field, meta] = useField(props);
const { value: selectedValue } = field;
const [touched, error] = at(meta, 'touched', 'error');
const isError = touched && error && true;
function _renderHelperText() {
if (isError) {
return <FormHelperText>{error}</FormHelperText>;
}
}
return (
<FormControl {...rest} error={isError}>
<InputLabel>{label}</InputLabel>
<Select {...field} value={selectedValue ? selectedValue : ''}>
{data.map((item, index) => (
<MenuItem key={index} value={item.value}>
{item.label}
</MenuItem>
))}
</Select>
{_renderHelperText()}
</FormControl>
);
}
SelectField.defaultProps = {
data: []
};
SelectField.propTypes = {
data: PropTypes.array.isRequired
};
export default SelectField;
@@ -0,0 +1,5 @@
import InputField from './InputField';
import CheckboxField from './CheckboxField';
import SelectField from './SelectField';
import DatePickerField from './DatePickerField';
export { InputField, CheckboxField, SelectField, DatePickerField };
@@ -0,0 +1,290 @@
import React, { useState, useEffect } from "react";
import {
makeStyles,
Paper,
Grid,
TextField,
Table,
TableHead,
TableBody,
TableCell,
TableRow,
IconButton
} from "@material-ui/core";
import { Formik, Form, Field } from 'formik';
import ButtonWithSpinner from "../ButtonWithSpinner";
import ConfirmationModal from "../ConfirmationModal";
import { Edit as EditIcon } from "@material-ui/icons";
import { toast } from "react-toastify";
import useHelps from "../../hooks/useHelps";
const useStyles = makeStyles(theme => ({
root: {
width: '100%'
},
mainPaper: {
width: '100%',
flex: 1,
padding: theme.spacing(2)
},
fullWidth: {
width: '100%'
},
tableContainer: {
width: '100%',
overflowX: "scroll",
...theme.scrollbarStyles
},
textfield: {
width: '100%'
},
textRight: {
textAlign: 'right'
},
row: {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2)
},
control: {
paddingRight: theme.spacing(1),
paddingLeft: theme.spacing(1)
},
buttonContainer: {
textAlign: 'right',
padding: theme.spacing(1)
}
}));
export function HelpManagerForm (props) {
const { onSubmit, onDelete, onCancel, initialValue, loading } = props;
const classes = useStyles()
const [record, setRecord] = useState(initialValue);
useEffect(() => {
setRecord(initialValue)
}, [initialValue])
const handleSubmit = async(data) => {
onSubmit(data)
}
return (
<Formik
enableReinitialize
className={classes.fullWidth}
initialValues={record}
onSubmit={(values, { resetForm }) =>
setTimeout(() => {
handleSubmit(values)
resetForm()
}, 500)
}
>
{(values) => (
<Form className={classes.fullWidth}>
<Grid spacing={2} justifyContent="flex-end" container>
<Grid xs={12} sm={6} md={3} item>
<Field
as={TextField}
label="Título"
name="title"
variant="outlined"
className={classes.fullWidth}
margin="dense"
/>
</Grid>
<Grid xs={12} sm={6} md={3} item>
<Field
as={TextField}
label="Código do Vídeo"
name="video"
variant="outlined"
className={classes.fullWidth}
margin="dense"
/>
</Grid>
<Grid xs={12} sm={12} md={6} item>
<Field
as={TextField}
label="Descrição"
name="description"
variant="outlined"
className={classes.fullWidth}
margin="dense"
/>
</Grid>
<Grid sm={3} md={1} item>
<ButtonWithSpinner className={classes.fullWidth} loading={loading} onClick={() => onCancel()} variant="contained">
Limpar
</ButtonWithSpinner>
</Grid>
{ record.id !== undefined ? (
<Grid sm={3} md={1} item>
<ButtonWithSpinner className={classes.fullWidth} loading={loading} onClick={() => onDelete(record)} variant="contained" color="secondary">
Excluir
</ButtonWithSpinner>
</Grid>
) : null}
<Grid sm={3} md={1} item>
<ButtonWithSpinner className={classes.fullWidth} loading={loading} type="submit" variant="contained" color="primary">
Salvar
</ButtonWithSpinner>
</Grid>
</Grid>
</Form>
)}
</Formik>
)
}
export function HelpsManagerGrid (props) {
const { records, onSelect } = props
const classes = useStyles()
return (
<Paper className={classes.tableContainer}>
<Table className={classes.fullWidth} size="small" aria-label="a dense table">
<TableHead>
<TableRow>
<TableCell align="center" style={{width: '1%'}}>#</TableCell>
<TableCell align="left">Título</TableCell>
<TableCell align="left">Descrição</TableCell>
<TableCell align="left">Vídeo</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map((row) => (
<TableRow key={row.id}>
<TableCell align="center" style={{width: '1%'}}>
<IconButton onClick={() => onSelect(row)} aria-label="delete">
<EditIcon />
</IconButton>
</TableCell>
<TableCell align="left">{row.title || '-'}</TableCell>
<TableCell align="left">{row.description || '-'}</TableCell>
<TableCell align="left">{row.video || '-'}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Paper>
)
}
export default function HelpsManager () {
const classes = useStyles()
const { list, save, update, remove } = useHelps()
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
const [loading, setLoading] = useState(false)
const [records, setRecords] = useState([])
const [record, setRecord] = useState({
title: '',
description: '',
video: ''
})
useEffect(() => {
async function fetchData () {
await loadHelps()
}
fetchData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const loadHelps = async () => {
setLoading(true)
try {
const helpList = await list()
setRecords(helpList)
} catch (e) {
toast.error('Não foi possível carregar a lista de registros')
}
setLoading(false)
}
const handleSubmit = async (data) => {
setLoading(true)
try {
if (data.id !== undefined) {
await update(data)
} else {
await save(data)
}
await loadHelps()
handleCancel()
toast.success('Operação realizada com sucesso!')
} catch (e) {
toast.error('Não foi possível realizar a operação. Verifique se já existe uma helpo com o mesmo nome ou se os campos foram preenchidos corretamente')
}
setLoading(false)
}
const handleDelete = async () => {
setLoading(true)
try {
await remove(record.id)
await loadHelps()
handleCancel()
toast.success('Operação realizada com sucesso!')
} catch (e) {
toast.error('Não foi possível realizar a operação')
}
setLoading(false)
}
const handleOpenDeleteDialog = () => {
setShowConfirmDialog(true)
}
const handleCancel = () => {
setRecord({
title: '',
description: '',
video: ''
})
}
const handleSelect = (data) => {
setRecord({
id: data.id,
title: data.title || '',
description: data.description || '',
video: data.video || ''
})
}
return (
<Paper className={classes.mainPaper} elevation={0}>
<Grid spacing={2} container>
<Grid xs={12} item>
<HelpManagerForm
initialValue={record}
onDelete={handleOpenDeleteDialog}
onSubmit={handleSubmit}
onCancel={handleCancel}
loading={loading}
/>
</Grid>
<Grid xs={12} item>
<HelpsManagerGrid
records={records}
onSelect={handleSelect}
/>
</Grid>
</Grid>
<ConfirmationModal
title="Exclusão de Registro"
open={showConfirmDialog}
onClose={() => setShowConfirmDialog(false)}
onConfirm={() => handleDelete()}
>
Deseja realmente excluir esse registro?
</ConfirmationModal>
</Paper>
)
}
@@ -0,0 +1,50 @@
import React, { useEffect } from 'react';
import toastError from "../../errors/toastError";
import { Button, Divider, Typography} from "@material-ui/core";
const LocationPreview = ({ image, link, description }) => {
useEffect(() => {}, [image, link, description]);
const handleLocation = async() => {
try {
window.open(link);
} catch (err) {
toastError(err);
}
}
return (
<>
<div style={{
minWidth: "250px",
}}>
<div>
<div style={{ float: "left" }}>
<img src={image} alt="loc" onClick={handleLocation} style={{ width: "100px" }} />
</div>
{ description && (
<div style={{ display: "flex", flexWrap: "wrap" }}>
<Typography style={{ marginTop: "12px", marginLeft: "15px", marginRight: "15px", float: "left" }} variant="subtitle1" color="primary" gutterBottom>
<div dangerouslySetInnerHTML={{ __html: description.replace('\\n', '<br />') }}></div>
</Typography>
</div>
)}
<div style={{ display: "block", content: "", clear: "both" }}></div>
<div>
<Divider />
<Button
fullWidth
color="primary"
onClick={handleLocation}
disabled={!link}
>Visualizar</Button>
</div>
</div>
</div>
</>
);
};
export default LocationPreview;
@@ -0,0 +1,31 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container";
const useStyles = makeStyles(theme => ({
mainContainer: {
flex: 1,
padding: theme.spacing(2),
height: `calc(100% - 48px)`,
},
contentWrapper: {
height: "100%",
overflowY: "hidden",
display: "flex",
flexDirection: "column",
},
}));
const MainContainer = ({ children }) => {
const classes = useStyles();
return (
<Container className={classes.mainContainer}>
<div className={classes.contentWrapper}>{children}</div>
</Container>
);
};
export default MainContainer;
@@ -0,0 +1,19 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
contactsHeader: {
display: "flex",
alignItems: "center",
padding: "0px 6px 6px 6px",
},
}));
const MainHeader = ({ children }) => {
const classes = useStyles();
return <div className={classes.contactsHeader}>{children}</div>;
};
export default MainHeader;
@@ -0,0 +1,21 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
MainHeaderButtonsWrapper: {
flex: "none",
marginLeft: "auto",
"& > *": {
margin: theme.spacing(1),
},
},
}));
const MainHeaderButtonsWrapper = ({ children }) => {
const classes = useStyles();
return <div className={classes.MainHeaderButtonsWrapper}>{children}</div>;
};
export default MainHeaderButtonsWrapper;
@@ -0,0 +1,186 @@
import React from "react";
import Markdown from "markdown-to-jsx";
const elements = [
"a",
"abbr",
"address",
"area",
"article",
"aside",
"audio",
"b",
"base",
"bdi",
"bdo",
"big",
"blockquote",
"body",
"br",
"button",
"canvas",
"caption",
"cite",
"code",
"col",
"colgroup",
"data",
"datalist",
"dd",
"del",
"details",
"dfn",
"dialog",
"div",
"dl",
"dt",
"em",
"embed",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"head",
"header",
"hgroup",
"hr",
"html",
"i",
"iframe",
"img",
"input",
"ins",
"kbd",
"keygen",
"label",
"legend",
"li",
"link",
"main",
"map",
"mark",
"marquee",
"menu",
"menuitem",
"meta",
"meter",
"nav",
"noscript",
"object",
"ol",
"optgroup",
"option",
"output",
"p",
"param",
"picture",
"pre",
"progress",
"q",
"rp",
"rt",
"ruby",
"s",
"samp",
"script",
"section",
"select",
"small",
"source",
"span",
"strong",
"style",
"sub",
"summary",
"sup",
"table",
"tbody",
"td",
"textarea",
"tfoot",
"th",
"thead",
"time",
"title",
"tr",
"track",
"u",
"ul",
"var",
"video",
"wbr",
// SVG
"circle",
"clipPath",
"defs",
"ellipse",
"foreignObject",
"g",
"image",
"line",
"linearGradient",
"marker",
"mask",
"path",
"pattern",
"polygon",
"polyline",
"radialGradient",
"rect",
"stop",
"svg",
"text",
"tspan",
];
const allowedElements = ["a", "b", "strong", "em", "u", "code", "del"];
const CustomLink = ({ children, ...props }) => (
<a {...props} target="_blank" rel="noopener noreferrer">
{children}
</a>
);
const MarkdownWrapper = ({ children }) => {
const boldRegex = /\*(.*?)\*/g;
const tildaRegex = /~(.*?)~/g;
if (children && boldRegex.test(children)) {
children = children.replace(boldRegex, "**$1**");
}
if (children && tildaRegex.test(children)) {
children = children.replace(tildaRegex, "~~$1~~");
}
const options = React.useMemo(() => {
const markdownOptions = {
disableParsingRawHTML: true,
forceInline: true,
overrides: {
a: { component: CustomLink },
},
};
elements.forEach(element => {
if (!allowedElements.includes(element)) {
markdownOptions.overrides[element] = el => el.children || null;
}
});
return markdownOptions;
}, []);
if (!children) return null;
return <Markdown options={options}>{children}</Markdown>;
};
export default MarkdownWrapper;
@@ -0,0 +1,48 @@
import React, { useState, useEffect } from "react";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
timerBox: {
display: "flex",
marginLeft: 10,
marginRight: 10,
alignItems: "center",
},
}));
const RecordingTimer = () => {
const classes = useStyles();
const initialState = {
minutes: 0,
seconds: 0,
};
const [timer, setTimer] = useState(initialState);
useEffect(() => {
const interval = setInterval(
() =>
setTimer(prevState => {
if (prevState.seconds === 59) {
return { ...prevState, minutes: prevState.minutes + 1, seconds: 0 };
}
return { ...prevState, seconds: prevState.seconds + 1 };
}),
1000
);
return () => {
clearInterval(interval);
};
}, []);
const addZero = n => {
return n < 10 ? "0" + n : n;
};
return (
<div className={classes.timerBox}>
<span>{`${addZero(timer.minutes)}:${addZero(timer.seconds)}`}</span>
</div>
);
};
export default RecordingTimer;
@@ -0,0 +1,513 @@
import React, { useState, useEffect, useContext, useRef } from "react";
import "emoji-mart/css/emoji-mart.css";
import { useParams } from "react-router-dom";
import { Picker } from "emoji-mart";
import MicRecorder from "mic-recorder-to-mp3";
import clsx from "clsx";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import InputBase from "@material-ui/core/InputBase";
import CircularProgress from "@material-ui/core/CircularProgress";
import { green } from "@material-ui/core/colors";
import AttachFileIcon from "@material-ui/icons/AttachFile";
import IconButton from "@material-ui/core/IconButton";
import MoodIcon from "@material-ui/icons/Mood";
import SendIcon from "@material-ui/icons/Send";
import CancelIcon from "@material-ui/icons/Cancel";
import ClearIcon from "@material-ui/icons/Clear";
import MicIcon from "@material-ui/icons/Mic";
import CheckCircleOutlineIcon from "@material-ui/icons/CheckCircleOutline";
import HighlightOffIcon from "@material-ui/icons/HighlightOff";
import { FormControlLabel, Switch } from "@material-ui/core";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import RecordingTimer from "./RecordingTimer";
import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext";
import { AuthContext } from "../../context/Auth/AuthContext";
import { useLocalStorage } from "../../hooks/useLocalStorage";
import toastError from "../../errors/toastError";
const Mp3Recorder = new MicRecorder({ bitRate: 128 });
const useStyles = makeStyles(theme => ({
mainWrapper: {
backgroundColor: theme.palette.bordabox, //DARK MODE PLW DESIGN//
display: "flex",
flexDirection: "column",
alignItems: "center",
borderTop: "1px solid rgba(0, 0, 0, 0.12)",
},
newMessageBox: {
background: "#eee",
width: "100%",
display: "flex",
padding: "7px",
alignItems: "center",
},
messageInputWrapper: {
padding: 6,
marginRight: 7,
background: "#fff",
display: "flex",
borderRadius: 20,
flex: 1,
},
messageInput: {
paddingLeft: 10,
flex: 1,
border: "none",
},
sendMessageIcons: {
color: "grey",
},
uploadInput: {
display: "none",
},
viewMediaInputWrapper: {
display: "flex",
padding: "10px 13px",
position: "relative",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: "#eee",
borderTop: "1px solid rgba(0, 0, 0, 0.12)",
},
emojiBox: {
position: "absolute",
bottom: 63,
width: 40,
borderTop: "1px solid #e8e8e8",
},
circleLoading: {
color: green[500],
opacity: "70%",
position: "absolute",
top: "20%",
left: "50%",
marginLeft: -12,
},
audioLoading: {
color: green[500],
opacity: "70%",
},
recorderWrapper: {
display: "flex",
alignItems: "center",
alignContent: "middle",
},
cancelAudioIcon: {
color: "red",
},
sendAudioIcon: {
color: "green",
},
replyginMsgWrapper: {
display: "flex",
width: "100%",
alignItems: "center",
justifyContent: "center",
paddingTop: 8,
paddingLeft: 73,
paddingRight: 7,
},
replyginMsgContainer: {
flex: 1,
marginRight: 5,
overflowY: "hidden",
backgroundColor: "rgba(0, 0, 0, 0.05)",
borderRadius: "7.5px",
display: "flex",
position: "relative",
},
replyginMsgBody: {
padding: 10,
height: "auto",
display: "block",
whiteSpace: "pre-wrap",
overflow: "hidden",
},
replyginContactMsgSideColor: {
flex: "none",
width: "4px",
backgroundColor: "#35cd96",
},
replyginSelfMsgSideColor: {
flex: "none",
width: "4px",
backgroundColor: "#6bcbef",
},
messageContactName: {
display: "flex",
color: "#6bcbef",
fontWeight: 500,
},
}));
const MessageInput = ({ ticketStatus }) => {
const classes = useStyles();
const { ticketId } = useParams();
const [medias, setMedias] = useState([]);
const [inputMessage, setInputMessage] = useState("");
const [showEmoji, setShowEmoji] = useState(false);
const [loading, setLoading] = useState(false);
const [recording, setRecording] = useState(false);
const inputRef = useRef();
const { setReplyingMessage, replyingMessage } = useContext(
ReplyMessageContext
);
const { user } = useContext(AuthContext);
const [signMessage, setSignMessage] = useLocalStorage("signOption", true);
useEffect(() => {
inputRef.current.focus();
}, [replyingMessage]);
useEffect(() => {
inputRef.current.focus();
return () => {
setInputMessage("");
setShowEmoji(false);
setMedias([]);
setReplyingMessage(null);
};
}, [ticketId, setReplyingMessage]);
const handleChangeInput = e => {
setInputMessage(e.target.value);
};
const handleAddEmoji = e => {
let emoji = e.native;
setInputMessage(prevState => prevState + emoji);
};
const handleChangeMedias = e => {
if (!e.target.files) {
return;
}
const selectedMedias = Array.from(e.target.files);
setMedias(selectedMedias);
};
const handleInputPaste = e => {
if (e.clipboardData.files[0]) {
setMedias([e.clipboardData.files[0]]);
}
};
const handleUploadMedia = async e => {
setLoading(true);
e.preventDefault();
const formData = new FormData();
formData.append("fromMe", true);
medias.forEach(media => {
formData.append("medias", media);
formData.append("body", media.name);
});
try {
await api.post(`/messages/${ticketId}`, formData);
} catch (err) {
toastError(err);
}
setLoading(false);
setMedias([]);
};
const handleSendMessage = async () => {
if (inputMessage.trim() === "") return;
setLoading(true);
const message = {
read: 1,
fromMe: true,
mediaUrl: "",
body: signMessage
? `*${user?.name}:*\n${inputMessage.trim()}`
: inputMessage.trim(),
quotedMsg: replyingMessage,
};
try {
await api.post(`/messages/${ticketId}`, message);
} catch (err) {
toastError(err);
}
setInputMessage("");
setShowEmoji(false);
setLoading(false);
setReplyingMessage(null);
};
const handleStartRecording = async () => {
setLoading(true);
try {
await navigator.mediaDevices.getUserMedia({ audio: true });
await Mp3Recorder.start();
setRecording(true);
setLoading(false);
} catch (err) {
toastError(err);
setLoading(false);
}
};
const handleUploadAudio = async () => {
setLoading(true);
try {
const [, blob] = await Mp3Recorder.stop().getMp3();
if (blob.size < 10000) {
setLoading(false);
setRecording(false);
return;
}
const formData = new FormData();
const filename = `${new Date().getTime()}.mp3`;
formData.append("medias", blob, filename);
formData.append("body", filename);
formData.append("fromMe", true);
await api.post(`/messages/${ticketId}`, formData);
} catch (err) {
toastError(err);
}
setRecording(false);
setLoading(false);
};
const handleCancelAudio = async () => {
try {
await Mp3Recorder.stop().getMp3();
setRecording(false);
} catch (err) {
toastError(err);
}
};
const renderReplyingMessage = message => {
return (
<div className={classes.replyginMsgWrapper}>
<div className={classes.replyginMsgContainer}>
<span
className={clsx(classes.replyginContactMsgSideColor, {
[classes.replyginSelfMsgSideColor]: !message.fromMe,
})}
></span>
<div className={classes.replyginMsgBody}>
{!message.fromMe && (
<span className={classes.messageContactName}>
{message.contact?.name}
</span>
)}
{message.body}
</div>
</div>
<IconButton
aria-label="showRecorder"
component="span"
disabled={loading || ticketStatus !== "open"}
onClick={() => setReplyingMessage(null)}
>
<ClearIcon className={classes.sendMessageIcons} />
</IconButton>
</div>
);
};
if (medias.length > 0)
return (
<Paper elevation={0} square className={classes.viewMediaInputWrapper}>
<IconButton
aria-label="cancel-upload"
component="span"
onClick={e => setMedias([])}
>
<CancelIcon className={classes.sendMessageIcons} />
</IconButton>
{loading ? (
<div>
<CircularProgress className={classes.circleLoading} />
</div>
) : (
<span>
{medias[0]?.name}
{/* <img src={media.preview} alt=""></img> */}
</span>
)}
<IconButton
aria-label="send-upload"
component="span"
onClick={handleUploadMedia}
disabled={loading}
>
<SendIcon className={classes.sendMessageIcons} />
</IconButton>
</Paper>
);
else {
return (
<Paper square elevation={0} className={classes.mainWrapper}>
{replyingMessage && renderReplyingMessage(replyingMessage)}
<div className={classes.newMessageBox}>
<IconButton
aria-label="emojiPicker"
component="span"
disabled={loading || recording || ticketStatus !== "open"}
onClick={e => setShowEmoji(prevState => !prevState)}
>
<MoodIcon className={classes.sendMessageIcons} />
</IconButton>
{showEmoji ? (
<div className={classes.emojiBox}>
<Picker
perLine={16}
showPreview={false}
showSkinTones={false}
onSelect={handleAddEmoji}
/>
</div>
) : null}
<input
multiple
type="file"
id="upload-button"
disabled={loading || recording || ticketStatus !== "open"}
className={classes.uploadInput}
onChange={handleChangeMedias}
/>
<label htmlFor="upload-button">
<IconButton
aria-label="upload"
component="span"
disabled={loading || recording || ticketStatus !== "open"}
>
<AttachFileIcon className={classes.sendMessageIcons} />
</IconButton>
</label>
<FormControlLabel
style={{ marginRight: 7, color: "gray" }}
label={i18n.t("messagesInput.signMessage")}
labelPlacement="start"
control={
<Switch
size="small"
checked={signMessage}
onChange={e => {
setSignMessage(e.target.checked);
}}
name="showAllTickets"
color="primary"
/>
}
/>
<div className={classes.messageInputWrapper}>
<InputBase
inputRef={input => {
input && input.focus();
input && (inputRef.current = input);
}}
className={classes.messageInput}
placeholder={
ticketStatus === "open"
? i18n.t("messagesInput.placeholderOpen")
: i18n.t("messagesInput.placeholderClosed")
}
multiline
maxRows={5}
value={inputMessage}
onChange={handleChangeInput}
disabled={recording || loading || ticketStatus !== "open"}
onPaste={e => {
ticketStatus === "open" && handleInputPaste(e);
}}
onKeyPress={e => {
if (loading || e.shiftKey) return;
else if (e.key === "Enter") {
handleSendMessage();
}
}}
/>
</div>
{inputMessage ? (
<IconButton
aria-label="sendMessage"
component="span"
onClick={handleSendMessage}
disabled={loading}
>
<SendIcon className={classes.sendMessageIcons} />
</IconButton>
) : recording ? (
<div className={classes.recorderWrapper}>
<IconButton
aria-label="cancelRecording"
component="span"
fontSize="large"
disabled={loading}
onClick={handleCancelAudio}
>
<HighlightOffIcon className={classes.cancelAudioIcon} />
</IconButton>
{loading ? (
<div>
<CircularProgress className={classes.audioLoading} />
</div>
) : (
<RecordingTimer />
)}
<IconButton
aria-label="sendRecordedAudio"
component="span"
onClick={handleUploadAudio}
disabled={loading}
>
<CheckCircleOutlineIcon className={classes.sendAudioIcon} />
</IconButton>
</div>
) : (
<IconButton
aria-label="showRecorder"
component="span"
disabled={loading || ticketStatus !== "open"}
onClick={handleStartRecording}
>
<MicIcon className={classes.sendMessageIcons} />
</IconButton>
)}
</div>
</Paper>
);
}
};
export default MessageInput;
@@ -0,0 +1,48 @@
import React, { useState, useEffect } from "react";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
timerBox: {
display: "flex",
marginLeft: 10,
marginRight: 10,
alignItems: "center",
},
}));
const RecordingTimer = () => {
const classes = useStyles();
const initialState = {
minutes: 0,
seconds: 0,
};
const [timer, setTimer] = useState(initialState);
useEffect(() => {
const interval = setInterval(
() =>
setTimer(prevState => {
if (prevState.seconds === 59) {
return { ...prevState, minutes: prevState.minutes + 1, seconds: 0 };
}
return { ...prevState, seconds: prevState.seconds + 1 };
}),
1000
);
return () => {
clearInterval(interval);
};
}, []);
const addZero = n => {
return n < 10 ? "0" + n : n;
};
return (
<div className={classes.timerBox}>
<span>{`${addZero(timer.minutes)}:${addZero(timer.seconds)}`}</span>
</div>
);
};
export default RecordingTimer;
@@ -0,0 +1,773 @@
import React, { useState, useEffect, useContext, useRef } from "react";
import withWidth, { isWidthUp } from "@material-ui/core/withWidth";
import "emoji-mart/css/emoji-mart.css";
import { Picker } from "emoji-mart";
import MicRecorder from "mic-recorder-to-mp3";
import clsx from "clsx";
import { isNil } from "lodash";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import InputBase from "@material-ui/core/InputBase";
import CircularProgress from "@material-ui/core/CircularProgress";
import { green } from "@material-ui/core/colors";
import AttachFileIcon from "@material-ui/icons/AttachFile";
import IconButton from "@material-ui/core/IconButton";
import MoodIcon from "@material-ui/icons/Mood";
import SendIcon from "@material-ui/icons/Send";
import CancelIcon from "@material-ui/icons/Cancel";
import ClearIcon from "@material-ui/icons/Clear";
import MicIcon from "@material-ui/icons/Mic";
import CheckCircleOutlineIcon from "@material-ui/icons/CheckCircleOutline";
import HighlightOffIcon from "@material-ui/icons/HighlightOff";
import { FormControlLabel, Switch } from "@material-ui/core";
import Autocomplete from "@material-ui/lab/Autocomplete";
import { isString, isEmpty, isObject, has } from "lodash";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import axios from "axios";
import RecordingTimer from "./RecordingTimer";
import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext";
import { AuthContext } from "../../context/Auth/AuthContext";
import { useLocalStorage } from "../../hooks/useLocalStorage";
import toastError from "../../errors/toastError";
import useQuickMessages from "../../hooks/useQuickMessages";
const Mp3Recorder = new MicRecorder({ bitRate: 128 });
const useStyles = makeStyles((theme) => ({
mainWrapper: {
backgroundColor: theme.palette.bordabox, //DARK MODE PLW DESIGN//
display: "flex",
flexDirection: "column",
alignItems: "center",
borderTop: "1px solid rgba(0, 0, 0, 0.12)",
},
newMessageBox: {
backgroundColor: theme.palette.newmessagebox, //DARK MODE PLW DESIGN//
width: "100%",
display: "flex",
padding: "7px",
alignItems: "center",
},
messageInputWrapper: {
padding: 6,
marginRight: 7,
backgroundColor: theme.palette.inputdigita, //DARK MODE PLW DESIGN//
display: "flex",
borderRadius: 20,
flex: 1,
},
messageInput: {
paddingLeft: 10,
flex: 1,
border: "none",
},
sendMessageIcons: {
color: "grey",
},
uploadInput: {
display: "none",
},
viewMediaInputWrapper: {
display: "flex",
padding: "10px 13px",
position: "relative",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: "#eee",
borderTop: "1px solid rgba(0, 0, 0, 0.12)",
},
emojiBox: {
position: "absolute",
bottom: 63,
width: 40,
borderTop: "1px solid #e8e8e8",
},
circleLoading: {
color: green[500],
opacity: "70%",
position: "absolute",
top: "20%",
left: "50%",
marginLeft: -12,
},
audioLoading: {
color: green[500],
opacity: "70%",
},
recorderWrapper: {
display: "flex",
alignItems: "center",
alignContent: "middle",
},
cancelAudioIcon: {
color: "red",
},
sendAudioIcon: {
color: "green",
},
replyginMsgWrapper: {
display: "flex",
width: "100%",
alignItems: "center",
justifyContent: "center",
paddingTop: 8,
paddingLeft: 73,
paddingRight: 7,
},
replyginMsgContainer: {
flex: 1,
marginRight: 5,
overflowY: "hidden",
backgroundColor: "rgba(0, 0, 0, 0.05)",
borderRadius: "7.5px",
display: "flex",
position: "relative",
},
replyginMsgBody: {
padding: 10,
height: "auto",
display: "block",
whiteSpace: "pre-wrap",
overflow: "hidden",
},
replyginContactMsgSideColor: {
flex: "none",
width: "4px",
backgroundColor: "#35cd96",
},
replyginSelfMsgSideColor: {
flex: "none",
width: "4px",
backgroundColor: "#6bcbef",
},
messageContactName: {
display: "flex",
color: "#6bcbef",
fontWeight: 500,
},
}));
const EmojiOptions = (props) => {
const { disabled, showEmoji, setShowEmoji, handleAddEmoji } = props;
const classes = useStyles();
return (
<>
<IconButton
aria-label="emojiPicker"
component="span"
disabled={disabled}
onClick={(e) => setShowEmoji((prevState) => !prevState)}
>
<MoodIcon className={classes.sendMessageIcons} />
</IconButton>
{showEmoji ? (
<div className={classes.emojiBox}>
<Picker
perLine={16}
showPreview={false}
showSkinTones={false}
onSelect={handleAddEmoji}
/>
</div>
) : null}
</>
);
};
const SignSwitch = (props) => {
const { width, setSignMessage, signMessage } = props;
if (isWidthUp("md", width)) {
return (
<FormControlLabel
style={{ marginRight: 7, color: "gray" }}
label={i18n.t("messagesInput.signMessage")}
labelPlacement="start"
control={
<Switch
size="small"
checked={signMessage}
onChange={(e) => {
setSignMessage(e.target.checked);
}}
name="showAllTickets"
color="primary"
/>
}
/>
);
}
return null;
};
const FileInput = (props) => {
const { handleChangeMedias, disableOption } = props;
const classes = useStyles();
return (
<>
<input
multiple
type="file"
id="upload-button"
disabled={disableOption()}
className={classes.uploadInput}
onChange={handleChangeMedias}
/>
<label htmlFor="upload-button">
<IconButton
aria-label="upload"
component="span"
disabled={disableOption()}
>
<AttachFileIcon className={classes.sendMessageIcons} />
</IconButton>
</label>
</>
);
};
const ActionButtons = (props) => {
const {
inputMessage,
loading,
recording,
ticketStatus,
handleSendMessage,
handleCancelAudio,
handleUploadAudio,
handleStartRecording,
} = props;
const classes = useStyles();
if (inputMessage) {
return (
<IconButton
aria-label="sendMessage"
component="span"
onClick={handleSendMessage}
disabled={loading}
>
<SendIcon className={classes.sendMessageIcons} />
</IconButton>
);
} else if (recording) {
return (
<div className={classes.recorderWrapper}>
<IconButton
aria-label="cancelRecording"
component="span"
fontSize="large"
disabled={loading}
onClick={handleCancelAudio}
>
<HighlightOffIcon className={classes.cancelAudioIcon} />
</IconButton>
{loading ? (
<div>
<CircularProgress className={classes.audioLoading} />
</div>
) : (
<RecordingTimer />
)}
<IconButton
aria-label="sendRecordedAudio"
component="span"
onClick={handleUploadAudio}
disabled={loading}
>
<CheckCircleOutlineIcon className={classes.sendAudioIcon} />
</IconButton>
</div>
);
} else {
return (
<IconButton
aria-label="showRecorder"
component="span"
disabled={loading || ticketStatus !== "open"}
onClick={handleStartRecording}
>
<MicIcon className={classes.sendMessageIcons} />
</IconButton>
);
}
};
const CustomInput = (props) => {
const {
loading,
inputRef,
ticketStatus,
inputMessage,
setInputMessage,
handleSendMessage,
handleInputPaste,
disableOption,
handleQuickAnswersClick,
} = props;
const classes = useStyles();
const [quickMessages, setQuickMessages] = useState([]);
const [options, setOptions] = useState([]);
const [popupOpen, setPopupOpen] = useState(false);
const { user } = useContext(AuthContext);
const { list: listQuickMessages } = useQuickMessages();
useEffect(() => {
async function fetchData() {
const companyId = localStorage.getItem("companyId");
const messages = await listQuickMessages({ companyId, userId: user.id });
const options = messages.map((m) => {
let truncatedMessage = m.message;
if (isString(truncatedMessage) && truncatedMessage.length > 35) {
truncatedMessage = m.message.substring(0, 35) + "...";
}
return {
value: m.message,
label: `/${m.shortcode} - ${truncatedMessage}`,
mediaPath: m.mediaPath,
};
});
setQuickMessages(options);
}
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (
isString(inputMessage) &&
!isEmpty(inputMessage) &&
inputMessage.length > 1
) {
const firstWord = inputMessage.charAt(0);
setPopupOpen(firstWord.indexOf("/") > -1);
const filteredOptions = quickMessages.filter(
(m) => m.label.indexOf(inputMessage) > -1
);
setOptions(filteredOptions);
} else {
setPopupOpen(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputMessage]);
const onKeyPress = (e) => {
if (loading || e.shiftKey) return;
else if (e.key === "Enter") {
handleSendMessage();
}
};
const onPaste = (e) => {
if (ticketStatus === "open") {
handleInputPaste(e);
}
};
const renderPlaceholder = () => {
if (ticketStatus === "open") {
return i18n.t("messagesInput.placeholderOpen");
}
return i18n.t("messagesInput.placeholderClosed");
};
const setInputRef = (input) => {
if (input) {
input.focus();
inputRef.current = input;
}
};
return (
<div className={classes.messageInputWrapper}>
<Autocomplete
freeSolo
open={popupOpen}
id="grouped-demo"
value={inputMessage}
options={options}
closeIcon={null}
getOptionLabel={(option) => {
if (isObject(option)) {
return option.label;
} else {
return option;
}
}}
onChange={(event, opt) => {
if (isObject(opt) && has(opt, "value") && isNil(opt.mediaPath)) {
setInputMessage(opt.value);
setTimeout(() => {
inputRef.current.scrollTop = inputRef.current.scrollHeight;
}, 200);
} else if (isObject(opt) && has(opt, "value") && !isNil(opt.mediaPath)) {
handleQuickAnswersClick(opt);
setTimeout(() => {
inputRef.current.scrollTop = inputRef.current.scrollHeight;
}, 200);
}
}}
onInputChange={(event, opt, reason) => {
if (reason === "input") {
setInputMessage(event.target.value);
}
}}
onPaste={onPaste}
onKeyPress={onKeyPress}
style={{ width: "100%" }}
renderInput={(params) => {
const { InputLabelProps, InputProps, ...rest } = params;
return (
<InputBase
{...params.InputProps}
{...rest}
disabled={disableOption()}
inputRef={setInputRef}
placeholder={renderPlaceholder()}
multiline
className={classes.messageInput}
maxRows={5}
/>
);
}}
/>
</div>
);
};
const MessageInputCustom = (props) => {
const { ticketStatus, ticketId } = props;
const classes = useStyles();
const [medias, setMedias] = useState([]);
const [inputMessage, setInputMessage] = useState("");
const [showEmoji, setShowEmoji] = useState(false);
const [loading, setLoading] = useState(false);
const [recording, setRecording] = useState(false);
const inputRef = useRef();
const { setReplyingMessage, replyingMessage } =
useContext(ReplyMessageContext);
const { user } = useContext(AuthContext);
const [signMessage, setSignMessage] = useLocalStorage("signOption", true);
useEffect(() => {
inputRef.current.focus();
}, [replyingMessage]);
useEffect(() => {
inputRef.current.focus();
return () => {
setInputMessage("");
setShowEmoji(false);
setMedias([]);
setReplyingMessage(null);
};
}, [ticketId, setReplyingMessage]);
// const handleChangeInput = e => {
// if (isObject(e) && has(e, 'value')) {
// setInputMessage(e.value);
// } else {
// setInputMessage(e.target.value)
// }
// };
const handleAddEmoji = (e) => {
let emoji = e.native;
setInputMessage((prevState) => prevState + emoji);
};
const handleChangeMedias = (e) => {
if (!e.target.files) {
return;
}
const selectedMedias = Array.from(e.target.files);
setMedias(selectedMedias);
};
const handleInputPaste = (e) => {
if (e.clipboardData.files[0]) {
setMedias([e.clipboardData.files[0]]);
}
};
const handleUploadQuickMessageMedia = async (blob, message) => {
setLoading(true);
try {
const extension = blob.type.split("/")[1];
const formData = new FormData();
const filename = `${new Date().getTime()}.${extension}`;
formData.append("medias", blob, filename);
formData.append("body", message);
formData.append("fromMe", true);
await api.post(`/messages/${ticketId}`, formData);
} catch (err) {
toastError(err);
setLoading(false);
}
setLoading(false);
};
const handleQuickAnswersClick = async (value) => {
if (value.mediaPath) {
try {
const { data } = await axios.get(value.mediaPath, {
responseType: "blob",
});
handleUploadQuickMessageMedia(data, value.value);
setInputMessage("");
return;
// handleChangeMedias(response)
} catch (err) {
toastError(err);
}
}
setInputMessage("");
setInputMessage(value.value);
};
const handleUploadMedia = async (e) => {
setLoading(true);
e.preventDefault();
const formData = new FormData();
formData.append("fromMe", true);
medias.forEach((media) => {
formData.append("medias", media);
formData.append("body", media.name);
});
try {
await api.post(`/messages/${ticketId}`, formData);
} catch (err) {
toastError(err);
}
setLoading(false);
setMedias([]);
};
const handleSendMessage = async () => {
if (inputMessage.trim() === "") return;
setLoading(true);
const message = {
read: 1,
fromMe: true,
mediaUrl: "",
body: signMessage
? `*${user?.name}:*\n${inputMessage.trim()}`
: inputMessage.trim(),
quotedMsg: replyingMessage,
};
try {
await api.post(`/messages/${ticketId}`, message);
} catch (err) {
toastError(err);
}
setInputMessage("");
setShowEmoji(false);
setLoading(false);
setReplyingMessage(null);
};
const handleStartRecording = async () => {
setLoading(true);
try {
await navigator.mediaDevices.getUserMedia({ audio: true });
await Mp3Recorder.start();
setRecording(true);
setLoading(false);
} catch (err) {
toastError(err);
setLoading(false);
}
};
const handleUploadAudio = async () => {
setLoading(true);
try {
const [, blob] = await Mp3Recorder.stop().getMp3();
if (blob.size < 10000) {
setLoading(false);
setRecording(false);
return;
}
const formData = new FormData();
const filename = `audio-record-site-${new Date().getTime()}.mp3`;
formData.append("medias", blob, filename);
formData.append("body", filename);
formData.append("fromMe", true);
await api.post(`/messages/${ticketId}`, formData);
} catch (err) {
toastError(err);
}
setRecording(false);
setLoading(false);
};
const handleCancelAudio = async () => {
try {
await Mp3Recorder.stop().getMp3();
setRecording(false);
} catch (err) {
toastError(err);
}
};
const disableOption = () => {
return loading || recording || ticketStatus !== "open";
};
const renderReplyingMessage = (message) => {
return (
<div className={classes.replyginMsgWrapper}>
<div className={classes.replyginMsgContainer}>
<span
className={clsx(classes.replyginContactMsgSideColor, {
[classes.replyginSelfMsgSideColor]: !message.fromMe,
})}
></span>
<div className={classes.replyginMsgBody}>
{!message.fromMe && (
<span className={classes.messageContactName}>
{message.contact?.name}
</span>
)}
{message.body}
</div>
</div>
<IconButton
aria-label="showRecorder"
component="span"
disabled={loading || ticketStatus !== "open"}
onClick={() => setReplyingMessage(null)}
>
<ClearIcon className={classes.sendMessageIcons} />
</IconButton>
</div>
);
};
if (medias.length > 0)
return (
<Paper elevation={0} square className={classes.viewMediaInputWrapper}>
<IconButton
aria-label="cancel-upload"
component="span"
onClick={(e) => setMedias([])}
>
<CancelIcon className={classes.sendMessageIcons} />
</IconButton>
{loading ? (
<div>
<CircularProgress className={classes.circleLoading} />
</div>
) : (
<span>
{medias[0]?.name}
{/* <img src={media.preview} alt=""></img> */}
</span>
)}
<IconButton
aria-label="send-upload"
component="span"
onClick={handleUploadMedia}
disabled={loading}
>
<SendIcon className={classes.sendMessageIcons} />
</IconButton>
</Paper>
);
else {
return (
<Paper square elevation={0} className={classes.mainWrapper}>
{replyingMessage && renderReplyingMessage(replyingMessage)}
<div className={classes.newMessageBox}>
<EmojiOptions
disabled={disableOption()}
handleAddEmoji={handleAddEmoji}
showEmoji={showEmoji}
setShowEmoji={setShowEmoji}
/>
<FileInput
disableOption={disableOption}
handleChangeMedias={handleChangeMedias}
/>
<SignSwitch
width={props.width}
setSignMessage={setSignMessage}
signMessage={signMessage}
/>
<CustomInput
loading={loading}
inputRef={inputRef}
ticketStatus={ticketStatus}
inputMessage={inputMessage}
setInputMessage={setInputMessage}
// handleChangeInput={handleChangeInput}
handleSendMessage={handleSendMessage}
handleInputPaste={handleInputPaste}
disableOption={disableOption}
handleQuickAnswersClick={handleQuickAnswersClick}
/>
<ActionButtons
inputMessage={inputMessage}
loading={loading}
recording={recording}
ticketStatus={ticketStatus}
handleSendMessage={handleSendMessage}
handleCancelAudio={handleCancelAudio}
handleUploadAudio={handleUploadAudio}
handleStartRecording={handleStartRecording}
/>
</div>
</Paper>
);
}
};
export default withWidth()(MessageInputCustom);
@@ -0,0 +1,71 @@
import React, { useState, useContext } from "react";
import MenuItem from "@material-ui/core/MenuItem";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import ConfirmationModal from "../ConfirmationModal";
import { Menu } from "@material-ui/core";
import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext";
import toastError from "../../errors/toastError";
const MessageOptionsMenu = ({ message, menuOpen, handleClose, anchorEl }) => {
const { setReplyingMessage } = useContext(ReplyMessageContext);
const [confirmationOpen, setConfirmationOpen] = useState(false);
const handleDeleteMessage = async () => {
try {
await api.delete(`/messages/${message.id}`);
} catch (err) {
toastError(err);
}
};
const hanldeReplyMessage = () => {
setReplyingMessage(message);
handleClose();
};
const handleOpenConfirmationModal = e => {
setConfirmationOpen(true);
handleClose();
};
return (
<>
<ConfirmationModal
title={i18n.t("messageOptionsMenu.confirmationModal.title")}
open={confirmationOpen}
onClose={setConfirmationOpen}
onConfirm={handleDeleteMessage}
>
{i18n.t("messageOptionsMenu.confirmationModal.message")}
</ConfirmationModal>
<Menu
anchorEl={anchorEl}
getContentAnchorEl={null}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
open={menuOpen}
onClose={handleClose}
>
{message.fromMe && (
<MenuItem onClick={handleOpenConfirmationModal}>
{i18n.t("messageOptionsMenu.delete")}
</MenuItem>
)}
<MenuItem onClick={hanldeReplyMessage}>
{i18n.t("messageOptionsMenu.reply")}
</MenuItem>
</Menu>
</>
);
};
export default MessageOptionsMenu;
@@ -0,0 +1,66 @@
import React from "react";
import { Chip, makeStyles } from "@material-ui/core";
import { i18n } from "../../translate/i18n";
import OutlinedDiv from "../OutlinedDiv";
const useStyles = makeStyles(theme => ({
chip: {
margin: theme.spacing(0.5),
cursor: "pointer"
}
}));
const MessageVariablesPicker = ({ onClick, disabled }) => {
const classes = useStyles();
const handleClick = (e, value) => {
e.preventDefault();
if (disabled) return;
onClick(value);
};
const msgVars = [
{
name: i18n.t("messageVariablesPicker.vars.contactFirstName"),
value: "{{firstName}}"
},
{
name: i18n.t("messageVariablesPicker.vars.contactName"),
value: "{{name}} "
},
{
name: i18n.t("messageVariablesPicker.vars.greeting"),
value: "{{ms}} "
},
{
name: i18n.t("messageVariablesPicker.vars.protocolNumber"),
value: "{{protocol}} "
},
{
name: i18n.t("messageVariablesPicker.vars.hour"),
value: "{{hora}} "
},
];
return (
<OutlinedDiv
margin="dense"
fullWidth
label={i18n.t("messageVariablesPicker.label")}
disabled={disabled}
>
{msgVars.map(msgVar => (
<Chip
key={msgVar.value}
onMouseDown={e => handleClick(e, msgVar.value)}
label={msgVar.name}
size="small"
className={classes.chip}
color="primary"
/>
))}
</OutlinedDiv>
);
};
export default MessageVariablesPicker;
@@ -0,0 +1,830 @@
import React, { useState, useEffect, useReducer, useRef, useContext } from "react";
import { isSameDay, parseISO, format } from "date-fns";
import clsx from "clsx";
import { green } from "@material-ui/core/colors";
import {
Button,
CircularProgress,
Divider,
IconButton,
makeStyles,
} from "@material-ui/core";
import {
AccessTime,
Block,
Done,
DoneAll,
ExpandMore,
GetApp,
} from "@material-ui/icons";
import MarkdownWrapper from "../MarkdownWrapper";
import ModalImageCors from "../ModalImageCors";
import MessageOptionsMenu from "../MessageOptionsMenu";
import whatsBackground from "../../assets/wa-background.png";
import LocationPreview from "../LocationPreview";
import whatsBackgroundDark from "../../assets/wa-background-dark.png"; //DARK MODE PLW DESIGN//
import api from "../../services/api";
import toastError from "../../errors/toastError";
import { SocketContext } from "../../context/Socket/SocketContext";
const useStyles = makeStyles((theme) => ({
messagesListWrapper: {
overflow: "hidden",
position: "relative",
display: "flex",
flexDirection: "column",
flexGrow: 1,
width: "100%",
minWidth: 300,
minHeight: 200,
},
messagesList: {
backgroundImage: theme.mode === 'light' ? `url(${whatsBackground})` : `url(${whatsBackgroundDark})`, //DARK MODE PLW DESIGN//
display: "flex",
flexDirection: "column",
flexGrow: 1,
padding: "20px 20px 20px 20px",
overflowY: "scroll",
...theme.scrollbarStyles,
},
circleLoading: {
color: green[500],
position: "absolute",
opacity: "70%",
top: 0,
left: "50%",
marginTop: 12,
},
messageLeft: {
marginRight: 20,
marginTop: 2,
minWidth: 100,
maxWidth: 600,
height: "auto",
display: "block",
position: "relative",
"&:hover #messageActionsButton": {
display: "flex",
position: "absolute",
top: 0,
right: 0,
},
whiteSpace: "pre-wrap",
backgroundColor: "#ffffff",
color: "#303030",
alignSelf: "flex-start",
borderTopLeftRadius: 0,
borderTopRightRadius: 8,
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
paddingLeft: 5,
paddingRight: 5,
paddingTop: 5,
paddingBottom: 0,
boxShadow: "0 1px 1px #b3b3b3",
},
quotedContainerLeft: {
margin: "-3px -80px 6px -6px",
overflow: "hidden",
backgroundColor: "#f0f0f0",
borderRadius: "7.5px",
display: "flex",
position: "relative",
},
quotedMsg: {
padding: 10,
maxWidth: 300,
height: "auto",
display: "block",
whiteSpace: "pre-wrap",
overflow: "hidden",
},
quotedSideColorLeft: {
flex: "none",
width: "4px",
backgroundColor: "#6bcbef",
},
messageRight: {
marginLeft: 20,
marginTop: 2,
minWidth: 100,
maxWidth: 600,
height: "auto",
display: "block",
position: "relative",
"&:hover #messageActionsButton": {
display: "flex",
position: "absolute",
top: 0,
right: 0,
},
whiteSpace: "pre-wrap",
backgroundColor: "#dcf8c6",
color: "#303030",
alignSelf: "flex-end",
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomLeftRadius: 8,
borderBottomRightRadius: 0,
paddingLeft: 5,
paddingRight: 5,
paddingTop: 5,
paddingBottom: 0,
boxShadow: "0 1px 1px #b3b3b3",
},
quotedContainerRight: {
margin: "-3px -80px 6px -6px",
overflowY: "hidden",
backgroundColor: "#cfe9ba",
borderRadius: "7.5px",
display: "flex",
position: "relative",
},
quotedMsgRight: {
padding: 10,
maxWidth: 300,
height: "auto",
whiteSpace: "pre-wrap",
},
quotedSideColorRight: {
flex: "none",
width: "4px",
backgroundColor: "#35cd96",
},
messageActionsButton: {
display: "none",
position: "relative",
color: "#999",
zIndex: 1,
backgroundColor: "inherit",
opacity: "90%",
"&:hover, &.Mui-focusVisible": { backgroundColor: "inherit" },
},
messageContactName: {
display: "flex",
color: "#6bcbef",
fontWeight: 500,
},
textContentItem: {
overflowWrap: "break-word",
padding: "3px 80px 6px 6px",
},
textContentItemEdited: {
overflowWrap: "break-word",
padding: "3px 120px 6px 6px",
},
textContentItemDeleted: {
fontStyle: "italic",
color: "rgba(0, 0, 0, 0.36)",
overflowWrap: "break-word",
padding: "3px 80px 6px 6px",
},
messageMedia: {
objectFit: "cover",
width: 250,
height: 200,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
},
timestamp: {
fontSize: 11,
position: "absolute",
bottom: 0,
right: 5,
color: "#999",
},
dailyTimestamp: {
alignItems: "center",
textAlign: "center",
alignSelf: "center",
width: "110px",
backgroundColor: "#e1f3fb",
margin: "10px",
borderRadius: "10px",
boxShadow: "0 1px 1px #b3b3b3",
},
dailyTimestampText: {
color: "#808888",
padding: 8,
alignSelf: "center",
marginLeft: "0px",
},
ackIcons: {
fontSize: 18,
verticalAlign: "middle",
marginLeft: 4,
},
deletedIcon: {
fontSize: 18,
verticalAlign: "middle",
marginRight: 4,
},
ackDoneAllIcon: {
color: green[500],
fontSize: 18,
verticalAlign: "middle",
marginLeft: 4,
},
downloadMedia: {
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "inherit",
padding: 10,
},
}));
const reducer = (state, action) => {
if (action.type === "LOAD_MESSAGES") {
const messages = action.payload;
const newMessages = [];
messages.forEach((message) => {
const messageIndex = state.findIndex((m) => m.id === message.id);
if (messageIndex !== -1) {
state[messageIndex] = message;
} else {
newMessages.push(message);
}
});
return [...newMessages, ...state];
}
if (action.type === "ADD_MESSAGE") {
const newMessage = action.payload;
const messageIndex = state.findIndex((m) => m.id === newMessage.id);
if (messageIndex !== -1) {
state[messageIndex] = newMessage;
} else {
state.push(newMessage);
}
return [...state];
}
if (action.type === "UPDATE_MESSAGE") {
const messageToUpdate = action.payload;
const messageIndex = state.findIndex((m) => m.id === messageToUpdate.id);
if (messageIndex !== -1) {
state[messageIndex] = messageToUpdate;
}
return [...state];
}
if (action.type === "RESET") {
return [];
}
};
const MessagesList = ({ ticket, ticketId, isGroup }) => {
const classes = useStyles();
const [messagesList, dispatch] = useReducer(reducer, []);
const [pageNumber, setPageNumber] = useState(1);
const [hasMore, setHasMore] = useState(false);
const [loading, setLoading] = useState(false);
const lastMessageRef = useRef();
const [selectedMessage, setSelectedMessage] = useState({});
const [anchorEl, setAnchorEl] = useState(null);
const messageOptionsMenuOpen = Boolean(anchorEl);
const currentTicketId = useRef(ticketId);
const socketManager = useContext(SocketContext);
useEffect(() => {
dispatch({ type: "RESET" });
setPageNumber(1);
currentTicketId.current = ticketId;
}, [ticketId]);
useEffect(() => {
setLoading(true);
const delayDebounceFn = setTimeout(() => {
const fetchMessages = async () => {
if (ticketId === undefined) return;
try {
const { data } = await api.get("/messages/" + ticketId, {
params: { pageNumber },
});
if (currentTicketId.current === ticketId) {
dispatch({ type: "LOAD_MESSAGES", payload: data.messages });
setHasMore(data.hasMore);
setLoading(false);
}
if (pageNumber === 1 && data.messages.length > 1) {
scrollToBottom();
}
} catch (err) {
setLoading(false);
toastError(err);
}
};
fetchMessages();
}, 500);
return () => {
clearTimeout(delayDebounceFn);
};
}, [pageNumber, ticketId]);
useEffect(() => {
const companyId = localStorage.getItem("companyId");
const socket = socketManager.getSocket(companyId);
socket.on("ready", () => socket.emit("joinChatBox", `${ticket.id}`));
socket.on(`company-${companyId}-appMessage`, (data) => {
if (data.action === "create" && data.message.ticketId === currentTicketId.current) {
dispatch({ type: "ADD_MESSAGE", payload: data.message });
scrollToBottom();
}
if (data.action === "update" && data.message.ticketId === currentTicketId.current) {
dispatch({ type: "UPDATE_MESSAGE", payload: data.message });
}
});
return () => {
socket.disconnect();
};
}, [ticketId, ticket, socketManager]);
const loadMore = () => {
setPageNumber((prevPageNumber) => prevPageNumber + 1);
};
const scrollToBottom = () => {
if (lastMessageRef.current) {
lastMessageRef.current.scrollIntoView({});
}
};
const handleScroll = (e) => {
if (!hasMore) return;
const { scrollTop } = e.currentTarget;
if (scrollTop === 0) {
document.getElementById("messagesList").scrollTop = 1;
}
if (loading) {
return;
}
if (scrollTop < 50) {
loadMore();
}
};
const handleOpenMessageOptionsMenu = (e, message) => {
setAnchorEl(e.currentTarget);
setSelectedMessage(message);
};
const handleCloseMessageOptionsMenu = (e) => {
setAnchorEl(null);
};
const checkMessageMedia = (message) => {
if (message.mediaType === "locationMessage" && message.body.split('|').length >= 2) {
let locationParts = message.body.split('|')
let imageLocation = locationParts[0]
let linkLocation = locationParts[1]
let descriptionLocation = null
if (locationParts.length > 2)
descriptionLocation = message.body.split('|')[2]
return <LocationPreview image={imageLocation} link={linkLocation} description={descriptionLocation} />
}
/* else if (message.mediaType === "vcard") {
let array = message.body.split("\n");
let obj = [];
let contact = "";
for (let index = 0; index < array.length; index++) {
const v = array[index];
let values = v.split(":");
for (let ind = 0; ind < values.length; ind++) {
if (values[ind].indexOf("+") !== -1) {
obj.push({ number: values[ind] });
}
if (values[ind].indexOf("FN") !== -1) {
contact = values[ind + 1];
}
}
}
return <VcardPreview contact={contact} numbers={obj[0].number} />
} */
/*else if (message.mediaType === "multi_vcard") {
console.log("multi_vcard")
console.log(message)
if(message.body !== null && message.body !== "") {
let newBody = JSON.parse(message.body)
return (
<>
{
newBody.map(v => (
<VcardPreview contact={v.name} numbers={v.number} />
))
}
</>
)
} else return (<></>)
}*/
else if (message.mediaType === "image") {
return <ModalImageCors imageUrl={message.mediaUrl} />;
} else if (message.mediaType === "audio") {
return (
<audio controls>
<source src={message.mediaUrl} type="audio/ogg"></source>
</audio>
);
} else if (message.mediaType === "video") {
return (
<video
className={classes.messageMedia}
src={message.mediaUrl}
controls
/>
);
} else {
return (
<>
<div className={classes.downloadMedia}>
<Button
startIcon={<GetApp />}
color="primary"
variant="outlined"
target="_blank"
href={message.mediaUrl}
>
Download
</Button>
</div>
<Divider />
</>
);
}
};
const renderMessageAck = (message) => {
if (message.ack === 1) {
return <AccessTime fontSize="small" className={classes.ackIcons} />;
}
if (message.ack === 2) {
return <Done fontSize="small" className={classes.ackIcons} />;
}
if (message.ack === 3) {
return <DoneAll fontSize="small" className={classes.ackIcons} />;
}
if (message.ack === 4 || message.ack === 5) {
return <DoneAll fontSize="small" className={classes.ackDoneAllIcon} />;
}
};
const renderDailyTimestamps = (message, index) => {
if (index === 0) {
return (
<span
className={classes.dailyTimestamp}
key={`timestamp-${message.id}`}
>
<div className={classes.dailyTimestampText}>
{format(parseISO(messagesList[index].createdAt), "dd/MM/yyyy")}
</div>
</span>
);
}
if (index < messagesList.length - 1) {
let messageDay = parseISO(messagesList[index].createdAt);
let previousMessageDay = parseISO(messagesList[index - 1].createdAt);
if (!isSameDay(messageDay, previousMessageDay)) {
return (
<span
className={classes.dailyTimestamp}
key={`timestamp-${message.id}`}
>
<div className={classes.dailyTimestampText}>
{format(parseISO(messagesList[index].createdAt), "dd/MM/yyyy")}
</div>
</span>
);
}
}
if (index === messagesList.length - 1) {
return (
<div
key={`ref-${message.createdAt}`}
ref={lastMessageRef}
style={{ float: "left", clear: "both" }}
/>
);
}
};
const renderNumberTicket = (message, index) => {
if (index < messagesList.length && index > 0) {
let messageTicket = message.ticketId;
let connectionName = message.ticket?.whatsapp?.name;
let previousMessageTicket = messagesList[index - 1].ticketId;
if (messageTicket !== previousMessageTicket) {
return (
<center>
<div className={classes.ticketNunberClosed}>
Conversa encerrada: {format(parseISO(messagesList[index - 1].createdAt), "dd/MM/yyyy HH:mm:ss")}
</div>
<div className={classes.ticketNunberOpen}>
Conversa iniciada: {format(parseISO(message.createdAt), "dd/MM/yyyy HH:mm:ss")}
</div>
</center>
);
}
}
};
const renderMessageDivider = (message, index) => {
if (index < messagesList.length && index > 0) {
let messageUser = messagesList[index].fromMe;
let previousMessageUser = messagesList[index - 1].fromMe;
if (messageUser !== previousMessageUser) {
return (
<span style={{ marginTop: 16 }} key={`divider-${message.id}`}></span>
);
}
}
};
const renderQuotedMessage = (message) => {
return (
<div
className={clsx(classes.quotedContainerLeft, {
[classes.quotedContainerRight]: message.fromMe,
})}
>
<span
className={clsx(classes.quotedSideColorLeft, {
[classes.quotedSideColorRight]: message.quotedMsg?.fromMe,
})}
></span>
<div className={classes.quotedMsg}>
{!message.quotedMsg?.fromMe && (
<span className={classes.messageContactName}>
{message.quotedMsg?.contact?.name}
</span>
)}
{message.quotedMsg.mediaType === "audio"
&& (
<div className={classes.downloadMedia}>
<audio controls>
<source src={message.quotedMsg.mediaUrl} type="audio/ogg"></source>
</audio>
</div>
)
}
{message.quotedMsg.mediaType === "video"
&& (
<video
className={classes.messageMedia}
src={message.quotedMsg.mediaUrl}
controls
/>
)
}
{message.quotedMsg.mediaType === "application"
&& (
<div className={classes.downloadMedia}>
<Button
startIcon={<GetApp />}
color="primary"
variant="outlined"
target="_blank"
href={message.quotedMsg.mediaUrl}
>
Download
</Button>
</div>
)
}
{message.quotedMsg.mediaType === "image"
&& (
<ModalImageCors imageUrl={message.quotedMsg.mediaUrl} />)
|| message.quotedMsg?.body}
</div>
</div>
);
};
const renderMessages = () => {
if (messagesList.length > 0) {
const viewMessagesList = messagesList.map((message, index) => {
if (message.mediaType === "call_log") {
return (
<React.Fragment key={message.id}>
{renderDailyTimestamps(message, index)}
{renderNumberTicket(message, index)}
{renderMessageDivider(message, index)}
<div className={classes.messageCenter}>
<IconButton
variant="contained"
size="small"
id="messageActionsButton"
disabled={message.isDeleted}
className={classes.messageActionsButton}
onClick={(e) => handleOpenMessageOptionsMenu(e, message)}
>
<ExpandMore />
</IconButton>
{isGroup && (
<span className={classes.messageContactName}>
{message.contact?.name}
</span>
)}
<div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 17" width="20" height="17">
<path fill="#df3333" d="M18.2 12.1c-1.5-1.8-5-2.7-8.2-2.7s-6.7 1-8.2 2.7c-.7.8-.3 2.3.2 2.8.2.2.3.3.5.3 1.4 0 3.6-.7 3.6-.7.5-.2.8-.5.8-1v-1.3c.7-1.2 5.4-1.2 6.4-.1l.1.1v1.3c0 .2.1.4.2.6.1.2.3.3.5.4 0 0 2.2.7 3.6.7.2 0 1.4-2 .5-3.1zM5.4 3.2l4.7 4.6 5.8-5.7-.9-.8L10.1 6 6.4 2.3h2.5V1H4.1v4.8h1.3V3.2z"></path>
</svg> <span>Chamada de voz/vídeo perdida às {format(parseISO(message.createdAt), "HH:mm")}</span>
</div>
</div>
</React.Fragment>
);
}
if (!message.fromMe) {
return (
<React.Fragment key={message.id}>
{renderDailyTimestamps(message, index)}
{renderNumberTicket(message, index)}
{renderMessageDivider(message, index)}
<div className={classes.messageLeft}>
<IconButton
variant="contained"
size="small"
id="messageActionsButton"
disabled={message.isDeleted}
className={classes.messageActionsButton}
onClick={(e) => handleOpenMessageOptionsMenu(e, message)}
>
<ExpandMore />
</IconButton>
{isGroup && (
<span className={classes.messageContactName}>
{message.contact?.name}
</span>
)}
{/* aviso de mensagem apagado pelo contato */}
{message.isDeleted && (
<div>
<span className={"message-deleted"}
>Essa mensagem foi apagada pelo contato &nbsp;
<Block
color="error"
fontSize="small"
className={classes.deletedIcon}
/>
</span>
</div>
)}
{(message.mediaUrl || message.mediaType === "locationMessage" || message.mediaType === "vcard"
//|| message.mediaType === "multi_vcard"
) && checkMessageMedia(message)}
<div className={classes.textContentItem}>
{message.quotedMsg && renderQuotedMessage(message)}
<MarkdownWrapper>{message.mediaType === "locationMessage" ? null : message.body}</MarkdownWrapper>
<span className={classes.timestamp}>
{message.isEdited && <span>Editada </span>}
{format(parseISO(message.createdAt), "HH:mm")}
</span>
</div>
</div>
</React.Fragment>
);
} else {
return (
<React.Fragment key={message.id}>
{renderDailyTimestamps(message, index)}
{renderNumberTicket(message, index)}
{renderMessageDivider(message, index)}
<div className={classes.messageRight}>
<IconButton
variant="contained"
size="small"
id="messageActionsButton"
disabled={message.isDeleted}
className={classes.messageActionsButton}
onClick={(e) => handleOpenMessageOptionsMenu(e, message)}
>
<ExpandMore />
</IconButton>
{(message.mediaUrl || message.mediaType === "locationMessage" || message.mediaType === "vcard"
//|| message.mediaType === "multi_vcard"
) && checkMessageMedia(message)}
<div
className={clsx(classes.textContentItem, {
[classes.textContentItemDeleted]: message.isDeleted,
[classes.textContentItemEdited]: message.isEdited,
})}
>
{message.isDeleted && (
<Block
color="disabled"
fontSize="small"
className={classes.deletedIcon}
/>
)}
{message.quotedMsg && renderQuotedMessage(message)}
<MarkdownWrapper>{message.body}</MarkdownWrapper>
<span className={classes.timestamp}>
{message.isEdited && <span>Editada </span>}
{format(parseISO(message.createdAt), "HH:mm")}
{renderMessageAck(message)}
</span>
</div>
</div>
</React.Fragment>
);
}
});
return viewMessagesList;
} else {
return <div>Diga olá para seu novo contato!</div>;
}
};
return (
<div className={classes.messagesListWrapper}>
<MessageOptionsMenu
message={selectedMessage}
anchorEl={anchorEl}
menuOpen={messageOptionsMenuOpen}
handleClose={handleCloseMessageOptionsMenu}
/>
<div
id="messagesList"
className={classes.messagesList}
onScroll={handleScroll}
>
{messagesList.length > 0 ? renderMessages() : []}
</div>
{loading && (
<div>
<CircularProgress className={classes.circleLoading} />
</div>
)}
</div>
);
};
export default MessagesList;
@@ -0,0 +1,50 @@
import React, { useState, useEffect } from "react";
import { makeStyles } from "@material-ui/core/styles";
import ModalImage from "react-modal-image";
import api from "../../services/api";
const useStyles = makeStyles(theme => ({
messageMedia: {
objectFit: "cover",
width: 250,
height: 200,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
},
}));
const ModalImageCors = ({ imageUrl }) => {
const classes = useStyles();
const [fetching, setFetching] = useState(true);
const [blobUrl, setBlobUrl] = useState("");
useEffect(() => {
if (!imageUrl) return;
const fetchImage = async () => {
const { data, headers } = await api.get(imageUrl, {
responseType: "blob",
});
const url = window.URL.createObjectURL(
new Blob([data], { type: headers["content-type"] })
);
setBlobUrl(url);
setFetching(false);
};
fetchImage();
}, [imageUrl]);
return (
<ModalImage
className={classes.messageMedia}
smallSrcSet={fetching ? imageUrl : blobUrl}
medium={fetching ? imageUrl : blobUrl}
large={fetching ? imageUrl : blobUrl}
alt="image"
/>
);
};
export default ModalImageCors;
+263
View File
@@ -0,0 +1,263 @@
import React, { useState, useEffect, useContext } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import CircularProgress from "@material-ui/core/CircularProgress";
import Select from "@material-ui/core/Select";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
import QueueSelectCustom from "../QueueSelectCustom";
import { AuthContext } from "../../context/Auth/AuthContext";
import { Can } from "../Can";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
flexWrap: "wrap",
},
multFieldLine: {
display: "flex",
"& > *:not(:last-child)": {
marginRight: theme.spacing(1),
},
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
}));
const UserSchema = Yup.object().shape({
name: Yup.string()
.min(2, "Too Short!")
.max(50, "Too Long!")
.required("Required"),
password: Yup.string().min(5, "Too Short!").max(50, "Too Long!"),
email: Yup.string().email("Invalid email").required("Required"),
});
const ModalUsers = ({ open, onClose, userId, companyId }) => {
const classes = useStyles();
const initialState = {
name: "",
email: "",
password: "",
profile: "user",
};
const { user: loggedInUser } = useContext(AuthContext);
const [user, setUser] = useState(initialState);
const [selectedQueueIds, setSelectedQueueIds] = useState([]);
useEffect(() => {
const fetchUser = async () => {
if (!userId) return;
if (open) {
try {
const { data } = await api.get(`/users/${userId}`);
setUser((prevState) => {
return { ...prevState, ...data };
});
const userQueueIds = data.queues?.map((queue) => queue.id);
setSelectedQueueIds(userQueueIds);
} catch (err) {
toastError(err);
}
}
};
fetchUser();
}, [userId, open]);
const handleClose = () => {
onClose();
setUser(initialState);
};
const handleSaveUser = async (values) => {
const userData = { ...values, companyId, queueIds: selectedQueueIds };
try {
if (userId) {
await api.put(`/users/${userId}`, userData);
} else {
await api.post("/users", userData);
}
toast.success(i18n.t("userModal.success"));
} catch (err) {
toastError(err);
}
handleClose();
};
return (
<div className={classes.root}>
<Dialog
open={open}
onClose={handleClose}
maxWidth="xs"
fullWidth
scroll="paper"
>
<DialogTitle id="form-dialog-title">
{userId
? `${i18n.t("userModal.title.edit")}`
: `${i18n.t("userModal.title.add")}`}
</DialogTitle>
<Formik
initialValues={user}
enableReinitialize={true}
validationSchema={UserSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveUser(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting }) => (
<Form>
<DialogContent dividers>
<div className={classes.multFieldLine}>
<Field
as={TextField}
label={i18n.t("userModal.form.name")}
autoFocus
name="name"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
fullWidth
/>
<Field
as={TextField}
label={i18n.t("userModal.form.password")}
type="password"
name="password"
error={touched.password && Boolean(errors.password)}
helperText={touched.password && errors.password}
variant="outlined"
margin="dense"
fullWidth
/>
</div>
<div className={classes.multFieldLine}>
<Field
as={TextField}
label={i18n.t("userModal.form.email")}
name="email"
error={touched.email && Boolean(errors.email)}
helperText={touched.email && errors.email}
variant="outlined"
margin="dense"
fullWidth
/>
<FormControl
variant="outlined"
className={classes.formControl}
margin="dense"
>
<Can
role={loggedInUser.profile}
perform="user-modal:editProfile"
yes={() => (
<>
<InputLabel id="profile-selection-input-label">
{i18n.t("userModal.form.profile")}
</InputLabel>
<Field
as={Select}
label={i18n.t("userModal.form.profile")}
name="profile"
labelId="profile-selection-label"
id="profile-selection"
required
>
<MenuItem value="admin">Admin</MenuItem>
<MenuItem value="user">User</MenuItem>
</Field>
</>
)}
/>
</FormControl>
</div>
<Can
role={loggedInUser.profile}
perform="user-modal:editQueues"
yes={() => (
<QueueSelectCustom
companyId={companyId}
selectedQueueIds={selectedQueueIds}
onChange={(values) => setSelectedQueueIds(values)}
/>
)}
/>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("userModal.buttons.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{userId
? `${i18n.t("userModal.buttons.okEdit")}`
: `${i18n.t("userModal.buttons.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default ModalUsers;
@@ -0,0 +1,415 @@
import React, { useState, useEffect, useContext } from "react";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import Autocomplete, {
createFilterOptions,
} from "@material-ui/lab/Autocomplete";
import CircularProgress from "@material-ui/core/CircularProgress";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import ButtonWithSpinner from "../ButtonWithSpinner";
import ContactModal from "../ContactModal";
import toastError from "../../errors/toastError";
import { makeStyles } from "@material-ui/core/styles";
import { AuthContext } from "../../context/Auth/AuthContext";
import { WhatsApp } from "@material-ui/icons";
import { Grid, ListItemText, MenuItem, Select } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import { toast } from "react-toastify";
//import ShowTicketOpen from "../ShowTicketOpenModal";
const useStyles = makeStyles((theme) => ({
online: {
fontSize: 11,
color: "#25d366"
},
offline: {
fontSize: 11,
color: "#e1306c"
}
}));
const filter = createFilterOptions({
trim: true,
});
const NewTicketModal = ({ modalOpen, onClose, initialContact }) => {
const classes = useStyles();
const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false);
const [searchParam, setSearchParam] = useState("");
const [selectedContact, setSelectedContact] = useState(null);
const [selectedQueue, setSelectedQueue] = useState("");
const [selectedWhatsapp, setSelectedWhatsapp] = useState("");
const [newContact, setNewContact] = useState({});
const [whatsapps, setWhatsapps] = useState([]);
const [contactModalOpen, setContactModalOpen] = useState(false);
const { user } = useContext(AuthContext);
const { companyId, whatsappId } = user;
const [ openAlert, setOpenAlert ] = useState(false);
const [ userTicketOpen, setUserTicketOpen] = useState("");
const [ queueTicketOpen, setQueueTicketOpen] = useState("");
useEffect(() => {
if (initialContact?.id !== undefined) {
setOptions([initialContact]);
setSelectedContact(initialContact);
}
}, [initialContact]);
useEffect(() => {
setLoading(true);
const delayDebounceFn = setTimeout(() => {
const fetchContacts = async () => {
api
.get(`/whatsapp`, { params: { companyId, session: 0 } })
.then(({ data }) => setWhatsapps(data));
};
if (whatsappId !== null && whatsappId!== undefined) {
setSelectedWhatsapp(whatsappId)
}
if (user.queues.length === 1) {
setSelectedQueue(user.queues[0].id)
}
fetchContacts();
setLoading(false);
}, 500);
return () => clearTimeout(delayDebounceFn);
}, [])
useEffect(() => {
if (!modalOpen || searchParam.length < 3) {
setLoading(false);
return;
}
setLoading(true);
const delayDebounceFn = setTimeout(() => {
const fetchContacts = async () => {
try {
const { data } = await api.get("contacts", {
params: { searchParam },
});
setOptions(data.contacts);
setLoading(false);
} catch (err) {
setLoading(false);
toastError(err);
}
};
fetchContacts();
}, 500);
return () => clearTimeout(delayDebounceFn);
}, [searchParam, modalOpen]);
// const IconChannel = (channel) => {
// switch (channel) {
// case "facebook":
// return <Facebook style={{ color: "#3b5998", verticalAlign: "middle" }} />;
// case "instagram":
// return <Instagram style={{ color: "#e1306c", verticalAlign: "middle" }} />;
// case "whatsapp":
// return <WhatsApp style={{ color: "#25d366", verticalAlign: "middle" }} />
// default:
// return "error";
// }
// };
const handleClose = () => {
onClose();
setSearchParam("");
setOpenAlert(false);
setUserTicketOpen("");
setQueueTicketOpen("");
setSelectedContact(null);
};
const handleCloseAlert = () => {
setOpenAlert(false);
setLoading(false);
setOpenAlert(false);
setUserTicketOpen("");
setQueueTicketOpen("");
};
const handleSaveTicket = async contactId => {
if (!contactId) return;
if (selectedQueue === "" && user.profile !== 'admin') {
toast.error("Selecione uma fila");
return;
}
setLoading(true);
try {
const queueId = selectedQueue !== "" ? selectedQueue : null;
const whatsappId = selectedWhatsapp !== "" ? selectedWhatsapp : null;
const { data: ticket } = await api.post("/tickets", {
contactId: contactId,
queueId,
whatsappId,
userId: user.id,
status: "open",
});
onClose(ticket);
} catch (err) {
const ticket = JSON.parse(err.response.data.error);
if (ticket.userId !== user?.id) {
setOpenAlert(true);
setUserTicketOpen(ticket.user.name);
setQueueTicketOpen(ticket.queue.name);
} else {
setOpenAlert(false);
setUserTicketOpen("");
setQueueTicketOpen("");
setLoading(false);
onClose(ticket);
}
}
setLoading(false);
};
const handleSelectOption = (e, newValue) => {
if (newValue?.number) {
setSelectedContact(newValue);
} else if (newValue?.name) {
setNewContact({ name: newValue.name });
setContactModalOpen(true);
}
};
const handleCloseContactModal = () => {
setContactModalOpen(false);
};
const handleAddNewContactTicket = contact => {
handleSaveTicket(contact.id);
};
const createAddContactOption = (filterOptions, params) => {
const filtered = filter(filterOptions, params);
if (params.inputValue !== "" && !loading && searchParam.length >= 3) {
filtered.push({
name: `${params.inputValue}`,
});
}
return filtered;
};
const renderOption = option => {
if (option.number) {
return <>
{/* {IconChannel(option.channel)} */}
<Typography component="span" style={{ fontSize: 14, marginLeft: "10px", display: "inline-flex", alignItems: "center", lineHeight: "2" }}>
{option.name} - {option.number}
</Typography>
</>
} else {
return `${i18n.t("newTicketModal.add")} ${option.name}`;
}
};
const renderOptionLabel = option => {
if (option.number) {
return `${option.name} - ${option.number}`;
} else {
return `${option.name}`;
}
};
const renderContactAutocomplete = () => {
if (initialContact === undefined || initialContact.id === undefined) {
return (
<Grid xs={12} item>
<Autocomplete
fullWidth
options={options}
loading={loading}
clearOnBlur
autoHighlight
freeSolo
clearOnEscape
getOptionLabel={renderOptionLabel}
renderOption={renderOption}
filterOptions={createAddContactOption}
onChange={(e, newValue) => handleSelectOption(e, newValue)}
renderInput={params => (
<TextField
{...params}
label={i18n.t("newTicketModal.fieldLabel")}
variant="outlined"
autoFocus
onChange={e => setSearchParam(e.target.value)}
onKeyPress={e => {
if (loading || !selectedContact) return;
else if (e.key === "Enter") {
handleSaveTicket(selectedContact.id);
}
}}
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
</Grid>
)
}
return null;
}
return (
<>
<ContactModal
open={contactModalOpen}
initialValues={newContact}
onClose={handleCloseContactModal}
onSave={handleAddNewContactTicket}
></ContactModal>
<Dialog open={modalOpen} onClose={handleClose}>
<DialogTitle id="form-dialog-title">
{i18n.t("newTicketModal.title")}
</DialogTitle>
<DialogContent dividers>
<Grid style={{ width: 300 }} container spacing={2}>
{/* CONTATO */}
{renderContactAutocomplete()}
{/* FILA */}
<Grid xs={12} item>
<Select
required
fullWidth
displayEmpty
variant="outlined"
value={selectedQueue}
onChange={(e) => {
setSelectedQueue(e.target.value)
}}
MenuProps={{
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
getContentAnchorEl: null,
}}
renderValue={() => {
if (selectedQueue === "") {
return "Selecione uma fila"
}
const queue = user.queues.find(q => q.id === selectedQueue)
return queue.name
}}
>
{user.queues?.length > 0 &&
user.queues.map((queue, key) => (
<MenuItem dense key={key} value={queue.id}>
<ListItemText primary={queue.name} />
</MenuItem>
))
}
</Select>
</Grid>
{/* CONEXAO */}
<Grid xs={12} item>
<Select
required
fullWidth
displayEmpty
variant="outlined"
value={selectedWhatsapp}
onChange={(e) => {
setSelectedWhatsapp(e.target.value)
}}
MenuProps={{
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
getContentAnchorEl: null,
}}
renderValue={() => {
if (selectedWhatsapp === "") {
return "Selecione uma Conexão"
}
const whatsapp = whatsapps.find(w => w.id === selectedWhatsapp)
return whatsapp.name
}}
>
{whatsapps?.length > 0 &&
whatsapps.map((whatsapp, key) => (
<MenuItem dense key={key} value={whatsapp.id}>
<ListItemText
primary={
<>
{/* {IconChannel(whatsapp.channel)} */}
<Typography component="span" style={{ fontSize: 14, marginLeft: "10px", display: "inline-flex", alignItems: "center", lineHeight: "2" }}>
{whatsapp.name} &nbsp; <p className={(whatsapp.status) === 'CONNECTED' ? classes.online : classes.offline} >({whatsapp.status})</p>
</Typography>
</>
}
/>
</MenuItem>
))}
</Select>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={loading}
variant="outlined"
>
{i18n.t("newTicketModal.buttons.cancel")}
</Button>
<ButtonWithSpinner
variant="contained"
type="button"
disabled={!selectedContact}
onClick={() => handleSaveTicket(selectedContact.id)}
color="primary"
loading={loading}
>
{i18n.t("newTicketModal.buttons.ok")}
</ButtonWithSpinner>
</DialogActions>
{/* <ShowTicketOpen
isOpen={openAlert}
handleClose={handleCloseAlert}
user={userTicketOpen}
queue={queueTicketOpen}
/> */}
</Dialog >
</>
);
};
export default NewTicketModal;
@@ -0,0 +1,270 @@
import React, { useState, useRef, useEffect, useContext } from "react";
import { useHistory } from "react-router-dom";
import { format } from "date-fns";
import { SocketContext } from "../../context/Socket/SocketContext";
import useSound from "use-sound";
import Popover from "@material-ui/core/Popover";
import IconButton from "@material-ui/core/IconButton";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import { makeStyles } from "@material-ui/core/styles";
import Badge from "@material-ui/core/Badge";
import ChatIcon from "@material-ui/icons/Chat";
import TicketListItem from "../TicketListItemCustom";
import useTickets from "../../hooks/useTickets";
import alertSound from "../../assets/sound.mp3";
import { AuthContext } from "../../context/Auth/AuthContext";
import { i18n } from "../../translate/i18n";
import toastError from "../../errors/toastError";
const useStyles = makeStyles(theme => ({
tabContainer: {
overflowY: "auto",
maxHeight: 350,
...theme.scrollbarStyles,
},
popoverPaper: {
width: "100%",
maxWidth: 350,
marginLeft: theme.spacing(2),
marginRight: theme.spacing(1),
[theme.breakpoints.down("sm")]: {
maxWidth: 270,
},
},
noShadow: {
boxShadow: "none !important",
},
}));
const NotificationsPopOver = (volume) => {
const classes = useStyles();
const history = useHistory();
const { user } = useContext(AuthContext);
const ticketIdUrl = +history.location.pathname.split("/")[2];
const ticketIdRef = useRef(ticketIdUrl);
const anchorEl = useRef();
const [isOpen, setIsOpen] = useState(false);
const [notifications, setNotifications] = useState([]);
const [showPendingTickets, setShowPendingTickets] = useState(false);
const [, setDesktopNotifications] = useState([]);
const { tickets } = useTickets({ withUnreadMessages: "true" });
const [play] = useSound(alertSound, volume);
const soundAlertRef = useRef();
const historyRef = useRef(history);
const socketManager = useContext(SocketContext);
useEffect(() => {
const fetchSettings = async () => {
try {
if (user.allTicket === "enable") {
setShowPendingTickets(true);
}
} catch (err) {
toastError(err);
}
}
fetchSettings();
}, []);
useEffect(() => {
soundAlertRef.current = play;
if (!("Notification" in window)) {
console.log("This browser doesn't support notifications");
} else {
Notification.requestPermission();
}
}, [play]);
useEffect(() => {
const processNotifications = () => {
if (showPendingTickets) {
setNotifications(tickets);
} else {
const newNotifications = tickets.filter(ticket => ticket.status !== "pending");
setNotifications(newNotifications);
}
}
processNotifications();
}, [tickets]);
useEffect(() => {
ticketIdRef.current = ticketIdUrl;
}, [ticketIdUrl]);
useEffect(() => {
const socket = socketManager.getSocket(user.companyId);
socket.on("ready", () => socket.emit("joinNotification"));
socket.on(`company-${user.companyId}-ticket`, data => {
if (data.action === "updateUnread" || data.action === "delete") {
setNotifications(prevState => {
const ticketIndex = prevState.findIndex(t => t.id === data.ticketId);
if (ticketIndex !== -1) {
prevState.splice(ticketIndex, 1);
return [...prevState];
}
return prevState;
});
setDesktopNotifications(prevState => {
const notfiticationIndex = prevState.findIndex(
n => n.tag === String(data.ticketId)
);
if (notfiticationIndex !== -1) {
prevState[notfiticationIndex].close();
prevState.splice(notfiticationIndex, 1);
return [...prevState];
}
return prevState;
});
}
});
socket.on(`company-${user.companyId}-appMessage`, data => {
if (
data.action === "create" && !data.message.fromMe &&
(data.ticket.status !== "pending" ) &&
(!data.message.read || data.ticket.status === "pending") &&
(data.ticket.userId === user?.id || !data.ticket.userId) &&
(user?.queues?.some(queue => (queue.id === data.ticket.queueId)) || !data.ticket.queueId)
) {
setNotifications(prevState => {
const ticketIndex = prevState.findIndex(t => t.id === data.ticket.id);
if (ticketIndex !== -1) {
prevState[ticketIndex] = data.ticket;
return [...prevState];
}
return [data.ticket, ...prevState];
});
const shouldNotNotificate =
(data.message.ticketId === ticketIdRef.current &&
document.visibilityState === "visible") ||
(data.ticket.userId && data.ticket.userId !== user?.id) ||
data.ticket.isGroup;
if (shouldNotNotificate) return;
handleNotifications(data);
}
});
return () => {
socket.disconnect();
};
}, [user, showPendingTickets, socketManager]);
const handleNotifications = data => {
const { message, contact, ticket } = data;
const options = {
body: `${message.body} - ${format(new Date(), "HH:mm")}`,
icon: contact.urlPicture,
tag: ticket.id,
renotify: true,
};
const notification = new Notification(
`${i18n.t("tickets.notification.message")} ${contact.name}`,
options
);
notification.onclick = e => {
e.preventDefault();
window.focus();
historyRef.current.push(`/tickets/${ticket.uuid}`);
// handleChangeTab(null, ticket.isGroup? "group" : "open");
};
setDesktopNotifications(prevState => {
const notfiticationIndex = prevState.findIndex(
n => n.tag === notification.tag
);
if (notfiticationIndex !== -1) {
prevState[notfiticationIndex] = notification;
return [...prevState];
}
return [notification, ...prevState];
});
soundAlertRef.current();
};
const handleClick = () => {
setIsOpen(prevState => !prevState);
};
const handleClickAway = () => {
setIsOpen(false);
};
const NotificationTicket = ({ children }) => {
return <div onClick={handleClickAway}>{children}</div>;
};
return (
<>
<IconButton
onClick={handleClick}
ref={anchorEl}
aria-label="Open Notifications"
color="inherit"
style={{color:"white"}}
>
<Badge overlap="rectangular" badgeContent={notifications.length} color="secondary">
<ChatIcon />
</Badge>
</IconButton>
<Popover
disableScrollLock
open={isOpen}
anchorEl={anchorEl.current}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
classes={{ paper: classes.popoverPaper }}
onClose={handleClickAway}
>
<List dense className={classes.tabContainer}>
{notifications.length === 0 ? (
<ListItem>
<ListItemText>{i18n.t("notifications.noTickets")}</ListItemText>
</ListItem>
) : (
notifications.map(ticket => (
<NotificationTicket key={ticket.id}>
<TicketListItem ticket={ticket} />
</NotificationTicket>
))
)}
</List>
</Popover>
</>
);
};
export default NotificationsPopOver;
@@ -0,0 +1,275 @@
import React, { useState, useRef, useEffect, useContext } from "react";
import { useHistory } from "react-router-dom";
import { format } from "date-fns";
import useSound from "use-sound";
import Popover from "@material-ui/core/Popover";
import IconButton from "@material-ui/core/IconButton";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import { makeStyles } from "@material-ui/core/styles";
import Badge from "@material-ui/core/Badge";
import ChatIcon from "@material-ui/icons/Chat";
import TicketListItem from "../TicketListItem";
import { i18n } from "../../translate/i18n";
import useTickets from "../../hooks/useTickets";
import alertSound from "../../assets/sound.mp3";
import { AuthContext } from "../../context/Auth/AuthContext";
import { socketConnection } from "../../services/socket";
const useStyles = makeStyles((theme) => ({
tabContainer: {
overflowY: "auto",
maxHeight: 350,
...theme.scrollbarStyles,
},
popoverPaper: {
width: "100%",
maxWidth: 350,
marginLeft: theme.spacing(2),
marginRight: theme.spacing(1),
[theme.breakpoints.down("sm")]: {
maxWidth: 270,
},
},
noShadow: {
boxShadow: "none !important",
},
icons: {
color: "#fff",
},
customBadge: {
backgroundColor: "#f44336",
color: "#fff",
},
}));
const NotificationsPopOver = ({ volume }) => {
const classes = useStyles();
const history = useHistory();
const { user } = useContext(AuthContext);
const ticketIdUrl = +history.location.pathname.split("/")[2];
const ticketIdRef = useRef(ticketIdUrl);
const anchorEl = useRef();
const [isOpen, setIsOpen] = useState(false);
const [notifications, setNotifications] = useState([]);
const { profile, queues } = user;
const [, setDesktopNotifications] = useState([]);
const { tickets } = useTickets({ withUnreadMessages: "true" });
const [play] = useSound(alertSound, { volume, });
const soundAlertRef = useRef();
const historyRef = useRef(history);
useEffect(() => {
soundAlertRef.current = play;
if (!("Notification" in window)) {
console.log("This browser doesn't support notifications");
} else {
Notification.requestPermission();
}
}, [play]);
useEffect(() => {
const queueIds = queues.map((q) => q.id);
const filteredTickets = tickets.filter(
(t) => queueIds.indexOf(t.queueId) > -1
);
if (profile === "user") {
setNotifications(filteredTickets);
} else {
setNotifications(tickets);
}
}, [tickets, queues, profile]);
useEffect(() => {
ticketIdRef.current = ticketIdUrl;
}, [ticketIdUrl]);
useEffect(() => {
const companyId = localStorage.getItem("companyId");
const socket = socketConnection({ companyId });
if (!socket) {
return () => {};
}
const queueIds = queues.map((q) => q.id);
socket.on("connect", () => socket.emit("joinNotification"));
socket.on(`company-${companyId}-ticket`, (data) => {
if (data.action === "updateUnread" || data.action === "delete") {
setNotifications((prevState) => {
const ticketIndex = prevState.findIndex(
(t) => t.id === data.ticketId
);
if (ticketIndex !== -1) {
prevState.splice(ticketIndex, 1);
return [...prevState];
}
return prevState;
});
setDesktopNotifications((prevState) => {
const notfiticationIndex = prevState.findIndex(
(n) => n.tag === String(data.ticketId)
);
if (notfiticationIndex !== -1) {
prevState[notfiticationIndex].close();
prevState.splice(notfiticationIndex, 1);
return [...prevState];
}
return prevState;
});
}
});
socket.on(`company-${companyId}-appMessage`, (data) => {
if (
data.action === "create" &&
!data.message.read &&
(data.ticket.userId === user?.id || !data.ticket.userId)
) {
if (
profile === "user" &&
(queueIds.indexOf(data.ticket.queue?.id) === -1 ||
data.ticket.queue === null)
) {
return;
}
setNotifications((prevState) => {
const ticketIndex = prevState.findIndex(
(t) => t.id === data.ticket.id
);
if (ticketIndex !== -1) {
prevState[ticketIndex] = data.ticket;
return [...prevState];
}
return [data.ticket, ...prevState];
});
const shouldNotNotificate =
(data.message.ticketId === ticketIdRef.current &&
document.visibilityState === "visible") ||
(data.ticket.userId && data.ticket.userId !== user?.id) ||
data.ticket.isGroup ||
data.ticket.isBot;
if (shouldNotNotificate) return;
handleNotifications(data);
}
});
return () => {
socket.disconnect();
};
}, [user, profile, queues]);
const handleNotifications = (data) => {
const { message, contact, ticket } = data;
const options = {
body: `${message.body} - ${format(new Date(), "HH:mm")}`,
icon: contact.profilePicUrl,
tag: ticket.id,
renotify: true,
};
const notification = new Notification(
`${i18n.t("tickets.notification.message")} ${contact.name}`,
options
);
notification.onclick = (e) => {
e.preventDefault();
window.focus();
historyRef.current.push(`/tickets/${ticket.uuid}`);
};
setDesktopNotifications((prevState) => {
const notfiticationIndex = prevState.findIndex(
(n) => n.tag === notification.tag
);
if (notfiticationIndex !== -1) {
prevState[notfiticationIndex] = notification;
return [...prevState];
}
return [notification, ...prevState];
});
soundAlertRef.current();
};
const handleClick = () => {
setIsOpen((prevState) => !prevState);
};
const handleClickAway = () => {
setIsOpen(false);
};
const NotificationTicket = ({ children }) => {
return <div onClick={handleClickAway}>{children}</div>;
};
return (
<>
<IconButton
className={classes.icons}
onClick={handleClick}
ref={anchorEl}
aria-label="Open Notifications"
variant="contained"
>
<Badge
overlap="rectangular"
badgeContent={notifications.length}
classes={{ badge: classes.customBadge }}
>
<ChatIcon />
</Badge>
</IconButton>
<Popover
disableScrollLock
open={isOpen}
anchorEl={anchorEl.current}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
classes={{ paper: classes.popoverPaper }}
onClose={handleClickAway}
>
<List dense className={classes.tabContainer}>
{notifications.length === 0 ? (
<ListItem>
<ListItemText>{i18n.t("notifications.noTickets")}</ListItemText>
</ListItem>
) : (
notifications.map((ticket) => (
<NotificationTicket key={ticket.id}>
<TicketListItem ticket={ticket} />
</NotificationTicket>
))
)}
</List>
</Popover>
</>
);
};
export default NotificationsPopOver;
@@ -0,0 +1,110 @@
import React, { useState, useRef } from "react";
import Popover from "@material-ui/core/Popover";
import IconButton from "@material-ui/core/IconButton";
import List from "@material-ui/core/List";
import { makeStyles } from "@material-ui/core/styles";
import VolumeUpIcon from "@material-ui/icons/VolumeUp";
import VolumeDownIcon from "@material-ui/icons/VolumeDown";
import { Grid, Slider } from "@material-ui/core";
const useStyles = makeStyles((theme) => ({
tabContainer: {
padding: theme.spacing(2),
},
popoverPaper: {
width: "100%",
maxWidth: 350,
marginLeft: theme.spacing(2),
marginRight: theme.spacing(1),
[theme.breakpoints.down("sm")]: {
maxWidth: 270,
},
},
noShadow: {
boxShadow: "none !important",
},
icons: {
color: "#fff",
},
customBadge: {
backgroundColor: "#f44336",
color: "#fff",
},
}));
const NotificationsVolume = ({ volume, setVolume }) => {
const classes = useStyles();
const anchorEl = useRef();
const [isOpen, setIsOpen] = useState(false);
const handleClick = () => {
setIsOpen((prevState) => !prevState);
};
const handleClickAway = () => {
setIsOpen(false);
};
const handleVolumeChange = (value) => {
setVolume(value);
localStorage.setItem("volume", value);
};
return (
<>
<IconButton
className={classes.icons}
onClick={handleClick}
ref={anchorEl}
aria-label="Open Notifications"
// color="inherit"
// color="secondary"
>
<VolumeUpIcon color="inherit" />
</IconButton>
<Popover
disableScrollLock
open={isOpen}
anchorEl={anchorEl.current}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
classes={{ paper: classes.popoverPaper }}
onClose={handleClickAway}
>
<List dense className={classes.tabContainer}>
<Grid container spacing={2}>
<Grid item>
<VolumeDownIcon />
</Grid>
<Grid item xs>
<Slider
value={volume}
aria-labelledby="continuous-slider"
step={0.1}
min={0}
max={1}
onChange={(e, value) =>
handleVolumeChange(value)
}
/>
</Grid>
<Grid item>
<VolumeUpIcon />
</Grid>
</Grid>
</List>
</Popover>
</>
);
};
export default NotificationsVolume;
@@ -0,0 +1,9 @@
const OnlyForSuperUser = ({ user, yes, no }) => user.super ? yes() : no();
OnlyForSuperUser.defaultProps = {
user: {},
yes: () => null,
no: () => null,
};
export default OnlyForSuperUser;
@@ -0,0 +1,30 @@
import React from "react";
import TextField from "@material-ui/core/TextField";
const InputComponent = ({ inputRef, ...other }) => <div {...other} />;
const OutlinedDiv = ({
InputProps,
children,
InputLabelProps,
label,
...other
}) => {
return (
<TextField
{...other}
variant="outlined"
label={label}
multiline
InputLabelProps={{ shrink: true, ...InputLabelProps }}
InputProps={{
inputComponent: InputComponent,
...InputProps
}}
inputProps={{ children: children }}
/>
);
};
export default OutlinedDiv;
@@ -0,0 +1,559 @@
import React, { useState, useEffect } from "react";
import {
makeStyles,
Paper,
Grid,
TextField,
Table,
TableHead,
TableBody,
TableCell,
TableRow,
IconButton,
FormControl,
InputLabel,
MenuItem,
Select
} from "@material-ui/core";
import { Formik, Form, Field } from 'formik';
import ButtonWithSpinner from "../ButtonWithSpinner";
import ConfirmationModal from "../ConfirmationModal";
import { Edit as EditIcon } from "@material-ui/icons";
import { toast } from "react-toastify";
import usePlans from "../../hooks/usePlans";
import { i18n } from "../../translate/i18n";
const useStyles = makeStyles(theme => ({
root: {
width: '100%'
},
mainPaper: {
width: '100%',
flex: 1,
padding: theme.spacing(2)
},
fullWidth: {
width: '100%'
},
tableContainer: {
width: '100%',
overflowX: "scroll",
...theme.scrollbarStyles
},
textfield: {
width: '100%'
},
textRight: {
textAlign: 'right'
},
row: {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2)
},
control: {
paddingRight: theme.spacing(1),
paddingLeft: theme.spacing(1)
},
buttonContainer: {
textAlign: 'right',
padding: theme.spacing(1)
}
}));
export function PlanManagerForm(props) {
const { onSubmit, onDelete, onCancel, initialValue, loading } = props;
const classes = useStyles()
const [record, setRecord] = useState({
name: '',
users: 0,
connections: 0,
queues: 0,
value: 0,
useCampaigns: true,
useSchedules: true,
useInternalChat: true,
useExternalApi: true,
useKanban: true,
useOpenAi: true,
useIntegrations: true,
});
useEffect(() => {
setRecord(initialValue)
}, [initialValue])
const handleSubmit = async (data) => {
onSubmit(data)
}
return (
<Formik
enableReinitialize
className={classes.fullWidth}
initialValues={record}
onSubmit={(values, { resetForm }) =>
setTimeout(() => {
handleSubmit(values)
resetForm()
}, 500)
}
>
{(values) => (
<Form className={classes.fullWidth}>
<Grid spacing={1} justifyContent="flex-start" container>
{/* NOME */}
<Grid xs={12} sm={6} md={2} item>
<Field
as={TextField}
label={i18n.t("plans.form.name")}
name="name"
variant="outlined"
className={classes.fullWidth}
margin="dense"
/>
</Grid>
{/* USUARIOS */}
<Grid xs={12} sm={6} md={1} item>
<Field
as={TextField}
label={i18n.t("plans.form.users")}
name="users"
variant="outlined"
className={classes.fullWidth}
margin="dense"
type="number"
/>
</Grid>
{/* CONEXOES */}
<Grid xs={12} sm={6} md={1} item>
<Field
as={TextField}
label={i18n.t("plans.form.connections")}
name="connections"
variant="outlined"
className={classes.fullWidth}
margin="dense"
type="number"
/>
</Grid>
{/* FILAS */}
<Grid xs={12} sm={6} md={1} item>
<Field
as={TextField}
label="Filas"
name="queues"
variant="outlined"
className={classes.fullWidth}
margin="dense"
type="number"
/>
</Grid>
{/* VALOR */}
<Grid xs={12} sm={6} md={1} item>
<Field
as={TextField}
label="Valor"
name="value"
variant="outlined"
className={classes.fullWidth}
margin="dense"
type="text"
/>
</Grid>
{/* CAMPANHAS */}
<Grid xs={12} sm={6} md={2} item>
<FormControl margin="dense" variant="outlined" fullWidth>
<InputLabel htmlFor="useCampaigns-selection">{i18n.t("plans.form.campaigns")}</InputLabel>
<Field
as={Select}
id="useCampaigns-selection"
label={i18n.t("plans.form.campaigns")}
labelId="useCampaigns-selection-label"
name="useCampaigns"
margin="dense"
>
<MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
<MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
</Field>
</FormControl>
</Grid>
{/* AGENDAMENTOS */}
<Grid xs={12} sm={8} md={2} item>
<FormControl margin="dense" variant="outlined" fullWidth>
<InputLabel htmlFor="useSchedules-selection">{i18n.t("plans.form.schedules")}</InputLabel>
<Field
as={Select}
id="useSchedules-selection"
label={i18n.t("plans.form.schedules")}
labelId="useSchedules-selection-label"
name="useSchedules"
margin="dense"
>
<MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
<MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
</Field>
</FormControl>
</Grid>
{/* CHAT INTERNO */}
<Grid xs={12} sm={8} md={2} item>
<FormControl margin="dense" variant="outlined" fullWidth>
<InputLabel htmlFor="useInternalChat-selection">Chat Interno</InputLabel>
<Field
as={Select}
id="useInternalChat-selection"
label="Chat Interno"
labelId="useInternalChat-selection-label"
name="useInternalChat"
margin="dense"
>
<MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
<MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
</Field>
</FormControl>
</Grid>
{/* API Externa */}
<Grid xs={12} sm={8} md={4} item>
<FormControl margin="dense" variant="outlined" fullWidth>
<InputLabel htmlFor="useExternalApi-selection">API Externa</InputLabel>
<Field
as={Select}
id="useExternalApi-selection"
label="API Externa"
labelId="useExternalApi-selection-label"
name="useExternalApi"
margin="dense"
>
<MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
<MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
</Field>
</FormControl>
</Grid>
{/* KANBAN */}
<Grid xs={12} sm={8} md={2} item>
<FormControl margin="dense" variant="outlined" fullWidth>
<InputLabel htmlFor="useKanban-selection">Kanban</InputLabel>
<Field
as={Select}
id="useKanban-selection"
label="Kanban"
labelId="useKanban-selection-label"
name="useKanban"
margin="dense"
>
<MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
<MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
</Field>
</FormControl>
</Grid>
{/* OPENAI */}
<Grid xs={12} sm={8} md={2} item>
<FormControl margin="dense" variant="outlined" fullWidth>
<InputLabel htmlFor="useOpenAi-selection">Open.Ai</InputLabel>
<Field
as={Select}
id="useOpenAi-selection"
label="Talk.Ai"
labelId="useOpenAi-selection-label"
name="useOpenAi"
margin="dense"
>
<MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
<MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
</Field>
</FormControl>
</Grid>
{/* INTEGRACOES */}
<Grid xs={12} sm={8} md={2} item>
<FormControl margin="dense" variant="outlined" fullWidth>
<InputLabel htmlFor="useIntegrations-selection">Integrações</InputLabel>
<Field
as={Select}
id="useIntegrations-selection"
label="Integrações"
labelId="useIntegrations-selection-label"
name="useIntegrations"
margin="dense"
>
<MenuItem value={true}>{i18n.t("plans.form.enabled")}</MenuItem>
<MenuItem value={false}>{i18n.t("plans.form.disabled")}</MenuItem>
</Field>
</FormControl>
</Grid>
</Grid>
<Grid spacing={2} justifyContent="flex-end" container>
<Grid sm={3} md={2} item>
<ButtonWithSpinner className={classes.fullWidth} loading={loading} onClick={() => onCancel()} variant="contained">
{i18n.t("plans.form.clear")}
</ButtonWithSpinner>
</Grid>
{record.id !== undefined ? (
<Grid sm={3} md={2} item>
<ButtonWithSpinner className={classes.fullWidth} loading={loading} onClick={() => onDelete(record)} variant="contained" color="secondary">
{i18n.t("plans.form.delete")}
</ButtonWithSpinner>
</Grid>
) : null}
<Grid sm={3} md={2} item>
<ButtonWithSpinner className={classes.fullWidth} loading={loading} type="submit" variant="contained" color="primary">
{i18n.t("plans.form.save")}
</ButtonWithSpinner>
</Grid>
</Grid>
</Form>
)}
</Formik>
)
}
export function PlansManagerGrid(props) {
const { records, onSelect } = props
const classes = useStyles()
const renderCampaigns = (row) => {
return row.useCampaigns === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
};
const renderSchedules = (row) => {
return row.useSchedules === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
};
const renderInternalChat = (row) => {
return row.useInternalChat === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
};
const renderExternalApi = (row) => {
return row.useExternalApi === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
};
const renderKanban = (row) => {
return row.useKanban === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
};
const renderOpenAi = (row) => {
return row.useOpenAi === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
};
const renderIntegrations = (row) => {
return row.useIntegrations === false ? `${i18n.t("plans.form.no")}` : `${i18n.t("plans.form.yes")}`;
};
return (
<Paper className={classes.tableContainer}>
<Table
className={classes.fullWidth}
// size="small"
padding="none"
aria-label="a dense table"
>
<TableHead>
<TableRow>
<TableCell align="center" style={{ width: '1%' }}>#</TableCell>
<TableCell align="left">{i18n.t("plans.form.name")}</TableCell>
<TableCell align="center">{i18n.t("plans.form.users")}</TableCell>
<TableCell align="center">{i18n.t("plans.form.connections")}</TableCell>
<TableCell align="center">Filas</TableCell>
<TableCell align="center">Valor</TableCell>
<TableCell align="center">{i18n.t("plans.form.campaigns")}</TableCell>
<TableCell align="center">{i18n.t("plans.form.schedules")}</TableCell>
<TableCell align="center">Chat Interno</TableCell>
<TableCell align="center">API Externa</TableCell>
<TableCell align="center">Kanban</TableCell>
<TableCell align="center">Open.Ai</TableCell>
<TableCell align="center">Integrações</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map((row) => (
<TableRow key={row.id}>
<TableCell align="center" style={{ width: '1%' }}>
<IconButton onClick={() => onSelect(row)} aria-label="delete">
<EditIcon />
</IconButton>
</TableCell>
<TableCell align="left">{row.name || '-'}</TableCell>
<TableCell align="center">{row.users || '-'}</TableCell>
<TableCell align="center">{row.connections || '-'}</TableCell>
<TableCell align="center">{row.queues || '-'}</TableCell>
<TableCell align="center">{i18n.t("plans.form.money")} {row.value ? row.value.toLocaleString('pt-br', { minimumFractionDigits: 2 }) : '00.00'}</TableCell>
<TableCell align="center">{renderCampaigns(row)}</TableCell>
<TableCell align="center">{renderSchedules(row)}</TableCell>
<TableCell align="center">{renderInternalChat(row)}</TableCell>
<TableCell align="center">{renderExternalApi(row)}</TableCell>
<TableCell align="center">{renderKanban(row)}</TableCell>
<TableCell align="center">{renderOpenAi(row)}</TableCell>
<TableCell align="center">{renderIntegrations(row)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Paper>
)
}
export default function PlansManager() {
const classes = useStyles()
const { list, save, update, remove } = usePlans()
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
const [loading, setLoading] = useState(false)
const [records, setRecords] = useState([])
const [record, setRecord] = useState({
name: '',
users: 0,
connections: 0,
queues: 0,
value: 0,
useCampaigns: true,
useSchedules: true,
useInternalChat: true,
useExternalApi: true,
useKanban: true,
useOpenAi: true,
useIntegrations: true,
})
useEffect(() => {
async function fetchData() {
await loadPlans()
}
fetchData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [record])
const loadPlans = async () => {
setLoading(true)
try {
const planList = await list()
setRecords(planList)
} catch (e) {
toast.error('Não foi possível carregar a lista de registros')
}
setLoading(false)
}
const handleSubmit = async (data) => {
setLoading(true)
console.log(data)
try {
if (data.id !== undefined) {
await update(data)
} else {
await save(data)
}
await loadPlans()
handleCancel()
toast.success('Operação realizada com sucesso!')
} catch (e) {
toast.error('Não foi possível realizar a operação. Verifique se já existe uma plano com o mesmo nome ou se os campos foram preenchidos corretamente')
}
setLoading(false)
}
const handleDelete = async () => {
setLoading(true)
try {
await remove(record.id)
await loadPlans()
handleCancel()
toast.success('Operação realizada com sucesso!')
} catch (e) {
toast.error('Não foi possível realizar a operação')
}
setLoading(false)
}
const handleOpenDeleteDialog = () => {
setShowConfirmDialog(true)
}
const handleCancel = () => {
setRecord({
id: undefined,
name: '',
users: 0,
connections: 0,
queues: 0,
value: 0,
useCampaigns: true,
useSchedules: true,
useInternalChat: true,
useExternalApi: true,
useKanban: true,
useOpenAi: true,
useIntegrations: true
})
}
const handleSelect = (data) => {
let useCampaigns = data.useCampaigns === false ? false : true
let useSchedules = data.useSchedules === false ? false : true
let useInternalChat = data.useInternalChat === false ? false : true
let useExternalApi = data.useExternalApi === false ? false : true
let useKanban = data.useKanban === false ? false : true
let useOpenAi = data.useOpenAi === false ? false : true
let useIntegrations = data.useIntegrations === false ? false : true
setRecord({
id: data.id,
name: data.name || '',
users: data.users || 0,
connections: data.connections || 0,
queues: data.queues || 0,
value: data.value?.toLocaleString('pt-br', { minimumFractionDigits: 0 }) || 0,
useCampaigns,
useSchedules,
useInternalChat,
useExternalApi,
useKanban,
useOpenAi,
useIntegrations
})
}
return (
<Paper className={classes.mainPaper} elevation={0}>
<Grid spacing={2} container>
<Grid xs={12} item>
<PlanManagerForm
initialValue={record}
onDelete={handleOpenDeleteDialog}
onSubmit={handleSubmit}
onCancel={handleCancel}
loading={loading}
/>
</Grid>
<Grid xs={12} item>
<PlansManagerGrid
records={records}
onSelect={handleSelect}
/>
</Grid>
</Grid>
<ConfirmationModal
title="Exclusão de Registro"
open={showConfirmDialog}
onClose={() => setShowConfirmDialog(false)}
onConfirm={() => handleDelete()}
>
Deseja realmente excluir esse registro?
</ConfirmationModal>
</Paper>
)
}
@@ -0,0 +1,366 @@
import React, { useState, useEffect } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import CircularProgress from "@material-ui/core/CircularProgress";
import { i18n } from "../../translate/i18n";
import { MenuItem, FormControl, InputLabel, Select } from "@material-ui/core";
import { Visibility, VisibilityOff } from "@material-ui/icons";
import { InputAdornment, IconButton } from "@material-ui/core";
import QueueSelectSingle from "../../components/QueueSelectSingle";
import api from "../../services/api";
import toastError from "../../errors/toastError";
const useStyles = makeStyles(theme => ({
root: {
display: "flex",
flexWrap: "wrap",
},
multFieldLine: {
display: "flex",
"& > *:not(:last-child)": {
marginRight: theme.spacing(1),
},
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
colorAdorment: {
width: 20,
height: 20,
},
}));
const PromptSchema = Yup.object().shape({
name: Yup.string().min(5, "Muito curto!").max(100, "Muito longo!").required("Obrigatório"),
prompt: Yup.string().min(50, "Muito curto!").required("Descreva o treinamento para Inteligência Artificial"),
voice: Yup.string().required("Informe o modo para Voz"),
max_tokens: Yup.number().required("Informe o número máximo de tokens"),
temperature: Yup.number().required("Informe a temperatura"),
apikey: Yup.string().required("Informe a API Key"),
queueId: Yup.number().required("Informe a fila"),
max_messages: Yup.number().required("Informe o número máximo de mensagens")
});
const PromptModal = ({ open, onClose, promptId }) => {
const classes = useStyles();
const [selectedVoice, setSelectedVoice] = useState("texto");
const [showApiKey, setShowApiKey] = useState(false);
const handleToggleApiKey = () => {
setShowApiKey(!showApiKey);
};
const initialState = {
name: "",
prompt: "",
voice: "texto",
voiceKey: "",
voiceRegion: "",
maxTokens: 100,
temperature: 1,
apiKey: "",
queueId: null,
maxMessages: 10
};
const [prompt, setPrompt] = useState(initialState);
useEffect(() => {
const fetchPrompt = async () => {
if (!promptId) {
setPrompt(initialState);
return;
}
try {
const { data } = await api.get(`/prompt/${promptId}`);
setPrompt(prevState => {
return { ...prevState, ...data };
});
setSelectedVoice(data.voice);
} catch (err) {
toastError(err);
}
};
fetchPrompt();
}, [promptId, open]);
const handleClose = () => {
setPrompt(initialState);
setSelectedVoice("texto");
onClose();
};
const handleChangeVoice = (e) => {
setSelectedVoice(e.target.value);
};
const handleSavePrompt = async values => {
const promptData = { ...values, voice: selectedVoice };
if (!values.queueId) {
toastError("Informe o setor");
return;
}
try {
if (promptId) {
await api.put(`/prompt/${promptId}`, promptData);
} else {
await api.post("/prompt", promptData);
}
toast.success(i18n.t("promptModal.success"));
} catch (err) {
toastError(err);
}
handleClose();
};
return (
<div className={classes.root}>
<Dialog
open={open}
onClose={handleClose}
maxWidth="md"
scroll="paper"
fullWidth
>
<DialogTitle id="form-dialog-title">
{promptId
? `${i18n.t("promptModal.title.edit")}`
: `${i18n.t("promptModal.title.add")}`}
</DialogTitle>
<Formik
initialValues={prompt}
enableReinitialize={true}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSavePrompt(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting, values }) => (
<Form style={{ width: "100%" }}>
<DialogContent dividers>
<Field
as={TextField}
label={i18n.t("promptModal.form.name")}
name="name"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
fullWidth
/>
<FormControl fullWidth margin="dense" variant="outlined">
<Field
as={TextField}
label={i18n.t("promptModal.form.apikey")}
name="apiKey"
type={showApiKey ? 'text' : 'password'}
error={touched.apiKey && Boolean(errors.apiKey)}
helperText={touched.apiKey && errors.apiKey}
variant="outlined"
margin="dense"
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleToggleApiKey}>
{showApiKey ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
</FormControl>
<Field
as={TextField}
label={i18n.t("promptModal.form.prompt")}
name="prompt"
error={touched.prompt && Boolean(errors.prompt)}
helperText={touched.prompt && errors.prompt}
variant="outlined"
margin="dense"
fullWidth
rows={10}
multiline={true}
/>
<QueueSelectSingle />
<div className={classes.multFieldLine}>
<FormControl fullWidth margin="dense" variant="outlined">
<InputLabel>{i18n.t("promptModal.form.voice")}</InputLabel>
<Select
id="type-select"
labelWidth={60}
name="voice"
value={selectedVoice}
onChange={handleChangeVoice}
multiple={false}
>
<MenuItem key={"texto"} value={"texto"}>
Texto
</MenuItem>
<MenuItem key={"pt-BR-FranciscaNeural"} value={"pt-BR-FranciscaNeural"}>
Francisa
</MenuItem>
<MenuItem key={"pt-BR-AntonioNeural"} value={"pt-BR-AntonioNeural"}>
Antônio
</MenuItem>
<MenuItem key={"pt-BR-BrendaNeural"} value={"pt-BR-BrendaNeural"}>
Brenda
</MenuItem>
<MenuItem key={"pt-BR-DonatoNeural"} value={"pt-BR-DonatoNeural"}>
Donato
</MenuItem>
<MenuItem key={"pt-BR-ElzaNeural"} value={"pt-BR-ElzaNeural"}>
Elza
</MenuItem>
<MenuItem key={"pt-BR-FabioNeural"} value={"pt-BR-FabioNeural"}>
Fábio
</MenuItem>
<MenuItem key={"pt-BR-GiovannaNeural"} value={"pt-BR-GiovannaNeural"}>
Giovanna
</MenuItem>
<MenuItem key={"pt-BR-HumbertoNeural"} value={"pt-BR-HumbertoNeural"}>
Humberto
</MenuItem>
<MenuItem key={"pt-BR-JulioNeural"} value={"pt-BR-JulioNeural"}>
Julio
</MenuItem>
<MenuItem key={"pt-BR-LeilaNeural"} value={"pt-BR-LeilaNeural"}>
Leila
</MenuItem>
<MenuItem key={"pt-BR-LeticiaNeural"} value={"pt-BR-LeticiaNeural"}>
Letícia
</MenuItem>
<MenuItem key={"pt-BR-ManuelaNeural"} value={"pt-BR-ManuelaNeural"}>
Manuela
</MenuItem>
<MenuItem key={"pt-BR-NicolauNeural"} value={"pt-BR-NicolauNeural"}>
Nicolau
</MenuItem>
<MenuItem key={"pt-BR-ValerioNeural"} value={"pt-BR-ValerioNeural"}>
Valério
</MenuItem>
<MenuItem key={"pt-BR-YaraNeural"} value={"pt-BR-YaraNeural"}>
Yara
</MenuItem>
</Select>
</FormControl>
<Field
as={TextField}
label={i18n.t("promptModal.form.voiceKey")}
name="voiceKey"
error={touched.voiceKey && Boolean(errors.voiceKey)}
helperText={touched.voiceKey && errors.voiceKey}
variant="outlined"
margin="dense"
fullWidth
/>
<Field
as={TextField}
label={i18n.t("promptModal.form.voiceRegion")}
name="voiceRegion"
error={touched.voiceRegion && Boolean(errors.voiceRegion)}
helperText={touched.voiceRegion && errors.voiceRegion}
variant="outlined"
margin="dense"
fullWidth
/>
</div>
<div className={classes.multFieldLine}>
<Field
as={TextField}
label={i18n.t("promptModal.form.temperature")}
name="temperature"
error={touched.temperature && Boolean(errors.temperature)}
helperText={touched.temperature && errors.temperature}
variant="outlined"
margin="dense"
fullWidth
/>
<Field
as={TextField}
label={i18n.t("promptModal.form.max_tokens")}
name="maxTokens"
error={touched.maxTokens && Boolean(errors.maxTokens)}
helperText={touched.maxTokens && errors.maxTokens}
variant="outlined"
margin="dense"
fullWidth
/>
<Field
as={TextField}
label={i18n.t("promptModal.form.max_messages")}
name="maxMessages"
error={touched.maxMessages && Boolean(errors.maxMessages)}
helperText={touched.maxMessages && errors.maxMessages}
variant="outlined"
margin="dense"
fullWidth
/>
</div>
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("promptModal.buttons.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{promptId
? `${i18n.t("promptModal.buttons.okEdit")}`
: `${i18n.t("promptModal.buttons.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default PromptModal;
@@ -0,0 +1,84 @@
import React, { useEffect, useState, useContext } from "react";
import QRCode from "qrcode.react";
import toastError from "../../errors/toastError";
import { Dialog, DialogContent, Paper, Typography, useTheme } from "@material-ui/core";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import { SocketContext } from "../../context/Socket/SocketContext";
const QrcodeModal = ({ open, onClose, whatsAppId }) => {
const [qrCode, setQrCode] = useState("");
const theme = useTheme();
const socketManager = useContext(SocketContext);
useEffect(() => {
const fetchSession = async () => {
if (!whatsAppId) return;
try {
const { data } = await api.get(`/whatsapp/${whatsAppId}`);
setQrCode(data.qrcode);
} catch (err) {
toastError(err);
}
};
fetchSession();
}, [whatsAppId]);
useEffect(() => {
if (!whatsAppId) return;
const companyId = localStorage.getItem("companyId");
const socket = socketManager.getSocket(companyId);
socket.on(`company-${companyId}-whatsappSession`, (data) => {
if (data.action === "update" && data.session.id === whatsAppId) {
setQrCode(data.session.qrcode);
}
if (data.action === "update" && data.session.qrcode === "") {
onClose();
}
});
return () => {
socket.disconnect();
};
}, [whatsAppId, onClose, socketManager]);
return (
<Dialog open={open} onClose={onClose} maxWidth="lg" scroll="paper">
<DialogContent>
<Paper elevation={0} style={{ display: "flex", alignItems: "center" }}>
<div style={{ marginRight: "20px" }}>
<Typography variant="h2" component="h2" color="textPrimary" gutterBottom style={{ fontFamily: "Montserrat", fontWeight: "bold", fontSize:"20px",}}>
Utilize o seu WhatsApp:
</Typography>
<Typography variant="body1" color="textPrimary" gutterBottom>
1 - Abra o WhatsApp no seu celular
</Typography>
<Typography variant="body1" color="textPrimary" gutterBottom>
2 - Toque em Mais opções no Android <svg class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path></svg> ou em Configurações <svg class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"></path></svg> no iPhone
</Typography>
<Typography variant="body1" color="textPrimary" gutterBottom>
3 - Toque em Dispositivos conectados e, em seguida, em Conectar dispositivos
</Typography>
<Typography variant="body1" color="textPrimary" gutterBottom>
4 - Aponte seu celular para essa tela para capturar o QR Code
</Typography>
</div>
<div>
{qrCode ? (
<QRCode value={qrCode} size={256} />
) : (
<span>Waiting for QR Code</span>
)}
</div>
</Paper>
</DialogContent>
</Dialog>
);
};
export default React.memo(QrcodeModal);
@@ -0,0 +1,505 @@
import React, { useState, useEffect } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
CircularProgress,
Select,
InputLabel,
MenuItem,
FormControl,
TextField,
Grid,
Paper,
} from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
flexWrap: "wrap",
gap: 4
},
textField: {
marginRight: theme.spacing(1),
flex: 1,
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
btnLeft: {
display: "flex",
marginRight: "auto",
marginLeft: 12,
},
colorAdorment: {
width: 20,
height: 20,
},
}));
const DialogflowSchema = Yup.object().shape({
name: Yup.string()
.min(2, "Too Short!")
.max(50, "Too Long!")
.required("Required"),
// projectName: Yup.string()
// .min(3, "Too Short!")
// .max(100, "Too Long!")
// .required(),
// jsonContent: Yup.string().min(3, "Too Short!").required(),
// language: Yup.string().min(2, "Too Short!").max(50, "Too Long!").required(),
});
const QueueIntegration = ({ open, onClose, integrationId }) => {
const classes = useStyles();
const initialState = {
type: "typebot",
name: "",
projectName: "",
jsonContent: "",
language: "",
urlN8N: "",
typebotDelayMessage: 1000,
typebotExpires: 1,
typebotKeywordFinish: "",
typebotKeywordRestart: "",
typebotRestartMessage: "",
typebotSlug: "",
typebotUnknownMessage: "",
};
const [integration, setIntegration] = useState(initialState);
useEffect(() => {
(async () => {
if (!integrationId) return;
try {
const { data } = await api.get(`/queueIntegration/${integrationId}`);
setIntegration((prevState) => {
return { ...prevState, ...data };
});
} catch (err) {
toastError(err);
}
})();
return () => {
setIntegration({
type: "dialogflow",
name: "",
projectName: "",
jsonContent: "",
language: "",
urlN8N: "",
typebotDelayMessage: 1000
});
};
}, [integrationId, open]);
const handleClose = () => {
onClose();
setIntegration(initialState);
};
const handleTestSession = async (event, values) => {
try {
const { projectName, jsonContent, language } = values;
await api.post(`/queueIntegration/testSession`, {
projectName,
jsonContent,
language,
});
toast.success(i18n.t("queueIntegrationModal.messages.testSuccess"));
} catch (err) {
toastError(err);
}
};
const handleSaveDialogflow = async (values) => {
try {
if (values.type === 'n8n' || values.type === 'webhook' || values.type === 'typebot') values.projectName = values.name
if (integrationId) {
await api.put(`/queueIntegration/${integrationId}`, values);
toast.success(i18n.t("queueIntegrationModal.messages.editSuccess"));
} else {
await api.post("/queueIntegration", values);
toast.success(i18n.t("queueIntegrationModal.messages.addSuccess"));
}
handleClose();
} catch (err) {
toastError(err);
}
};
return (
<div className={classes.root}>
<Dialog open={open} onClose={handleClose} fullWidth maxWidth="md" scroll="paper">
<DialogTitle>
{integrationId
? `${i18n.t("queueIntegrationModal.title.edit")}`
: `${i18n.t("queueIntegrationModal.title.add")}`}
</DialogTitle>
<Formik
initialValues={integration}
enableReinitialize={true}
validationSchema={DialogflowSchema}
onSubmit={(values, actions, event) => {
setTimeout(() => {
handleSaveDialogflow(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting, values }) => (
<Form>
<Paper square className={classes.mainPaper} elevation={1}>
<DialogContent dividers>
<Grid container spacing={1}>
<Grid item xs={12} md={6} xl={6}>
<FormControl
variant="outlined"
className={classes.formControl}
margin="dense"
fullWidth
>
<InputLabel id="type-selection-input-label">
{i18n.t("queueIntegrationModal.form.type")}
</InputLabel>
<Field
as={Select}
label={i18n.t("queueIntegrationModal.form.type")}
name="type"
labelId="profile-selection-label"
error={touched.type && Boolean(errors.type)}
helpertext={touched.type && errors.type}
id="type"
required
>
<MenuItem value="dialogflow">DialogFlow</MenuItem>
<MenuItem value="n8n">N8N</MenuItem>
<MenuItem value="webhook">WebHooks</MenuItem>
<MenuItem value="typebot">Typebot</MenuItem>
</Field>
</FormControl>
</Grid>
{values.type === "dialogflow" && (
<>
<Grid item xs={12} md={6} xl={6} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.name")}
autoFocus
name="name"
fullWidth
error={touched.name && Boolean(errors.name)}
helpertext={touched.name && errors.name}
variant="outlined"
margin="dense"
className={classes.textField}
/>
</Grid>
<Grid item xs={12} md={6} xl={6} >
<FormControl
variant="outlined"
className={classes.formControl}
margin="dense"
fullWidth
>
<InputLabel id="language-selection-input-label">
{i18n.t("queueIntegrationModal.form.language")}
</InputLabel>
<Field
as={Select}
label={i18n.t("queueIntegrationModal.form.language")}
name="language"
labelId="profile-selection-label"
fullWidth
error={touched.language && Boolean(errors.language)}
helpertext={touched.language && errors.language}
id="language-selection"
required
>
<MenuItem value="pt-BR">Portugues</MenuItem>
<MenuItem value="en">Inglês</MenuItem>
<MenuItem value="es">Español</MenuItem>
</Field>
</FormControl>
</Grid>
<Grid item xs={12} md={6} xl={6} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.projectName")}
name="projectName"
error={touched.projectName && Boolean(errors.projectName)}
helpertext={touched.projectName && errors.projectName}
fullWidth
variant="outlined"
margin="dense"
/>
</Grid>
<Grid item xs={12} md={12} xl={12} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.jsonContent")}
type="jsonContent"
multiline
//inputRef={greetingRef}
maxRows={5}
minRows={5}
fullWidth
name="jsonContent"
error={touched.jsonContent && Boolean(errors.jsonContent)}
helpertext={touched.jsonContent && errors.jsonContent}
variant="outlined"
margin="dense"
/>
</Grid>
</>
)}
{(values.type === "n8n" || values.type === "webhook") && (
<>
<Grid item xs={12} md={6} xl={6} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.name")}
autoFocus
required
name="name"
error={touched.name && Boolean(errors.name)}
helpertext={touched.name && errors.name}
variant="outlined"
margin="dense"
fullWidth
className={classes.textField}
/>
</Grid>
<Grid item xs={12} md={12} xl={12} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.urlN8N")}
name="urlN8N"
error={touched.urlN8N && Boolean(errors.urlN8N)}
helpertext={touched.urlN8N && errors.urlN8N}
variant="outlined"
margin="dense"
required
fullWidth
className={classes.textField}
/>
</Grid>
</>
)}
{(values.type === "typebot") && (
<>
<Grid item xs={12} md={6} xl={6} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.name")}
autoFocus
name="name"
error={touched.name && Boolean(errors.name)}
helpertext={touched.name && errors.name}
variant="outlined"
margin="dense"
required
fullWidth
className={classes.textField}
/>
</Grid>
<Grid item xs={12} md={12} xl={12} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.urlN8N")}
name="urlN8N"
error={touched.urlN8N && Boolean(errors.urlN8N)}
helpertext={touched.urlN8N && errors.urlN8N}
variant="outlined"
margin="dense"
required
fullWidth
className={classes.textField}
/>
</Grid>
<Grid item xs={12} md={6} xl={6} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.typebotSlug")}
name="typebotSlug"
error={touched.typebotSlug && Boolean(errors.typebotSlug)}
helpertext={touched.typebotSlug && errors.typebotSlug}
required
variant="outlined"
margin="dense"
fullWidth
className={classes.textField}
/>
</Grid>
<Grid item xs={12} md={6} xl={6} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.typebotExpires")}
name="typebotExpires"
error={touched.typebotExpires && Boolean(errors.typebotExpires)}
helpertext={touched.typebotExpires && errors.typebotExpires}
variant="outlined"
margin="dense"
fullWidth
className={classes.textField}
/>
</Grid>
<Grid item xs={12} md={6} xl={6} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.typebotDelayMessage")}
name="typebotDelayMessage"
error={touched.typebotDelayMessage && Boolean(errors.typebotDelayMessage)}
helpertext={touched.typebotDelayMessage && errors.typebotDelayMessage}
variant="outlined"
margin="dense"
fullWidth
className={classes.textField}
/>
</Grid>
<Grid item xs={12} md={6} xl={6} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.typebotKeywordFinish")}
name="typebotKeywordFinish"
error={touched.typebotKeywordFinish && Boolean(errors.typebotKeywordFinish)}
helpertext={touched.typebotKeywordFinish && errors.typebotKeywordFinish}
variant="outlined"
margin="dense"
fullWidth
className={classes.textField}
/>
</Grid>
<Grid item xs={12} md={6} xl={6} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.typebotKeywordRestart")}
name="typebotKeywordRestart"
error={touched.typebotKeywordRestart && Boolean(errors.typebotKeywordRestart)}
helpertext={touched.typebotKeywordRestart && errors.typebotKeywordRestart}
variant="outlined"
margin="dense"
fullWidth
className={classes.textField}
/>
</Grid>
<Grid item xs={12} md={6} xl={6} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.typebotUnknownMessage")}
name="typebotUnknownMessage"
error={touched.typebotUnknownMessage && Boolean(errors.typebotUnknownMessage)}
helpertext={touched.typebotUnknownMessage && errors.typebotUnknownMessage}
variant="outlined"
margin="dense"
fullWidth
className={classes.textField}
/>
</Grid>
<Grid item xs={12} md={12} xl={12} >
<Field
as={TextField}
label={i18n.t("queueIntegrationModal.form.typebotRestartMessage")}
name="typebotRestartMessage"
error={touched.typebotRestartMessage && Boolean(errors.typebotRestartMessage)}
helpertext={touched.typebotRestartMessage && errors.typebotRestartMessage}
variant="outlined"
margin="dense"
fullWidth
className={classes.textField}
/>
</Grid>
</>
)}
</Grid>
</DialogContent>
</Paper>
<DialogActions>
{values.type === "dialogflow" && (
<Button
//type="submit"
onClick={(e) => handleTestSession(e, values)}
color="inherit"
disabled={isSubmitting}
name="testSession"
variant="outlined"
className={classes.btnLeft}
>
{i18n.t("queueIntegrationModal.buttons.test")}
</Button>
)}
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("queueIntegrationModal.buttons.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{integrationId
? `${i18n.t("queueIntegrationModal.buttons.okEdit")}`
: `${i18n.t("queueIntegrationModal.buttons.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div >
);
};
export default QueueIntegration;
+470
View File
@@ -0,0 +1,470 @@
import React, { useState, useEffect, useRef } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import CircularProgress from "@material-ui/core/CircularProgress";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
import ColorPicker from "../ColorPicker";
import {
FormControl,
Grid,
IconButton,
InputAdornment,
InputLabel,
MenuItem,
Paper,
Select,
Tab,
Tabs,
} from "@material-ui/core";
import { Colorize } from "@material-ui/icons";
import { QueueOptions } from "../QueueOptions";
import SchedulesForm from "../SchedulesForm";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
flexWrap: "wrap",
},
textField: {
marginRight: theme.spacing(1),
flex: 1,
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
colorAdorment: {
width: 20,
height: 20,
},
}));
const QueueSchema = Yup.object().shape({
name: Yup.string()
.min(2, "Too Short!")
.max(50, "Too Long!")
.required("Required"),
color: Yup.string().min(3, "Too Short!").max(9, "Too Long!").required(),
greetingMessage: Yup.string(),
});
const QueueModal = ({ open, onClose, queueId }) => {
const classes = useStyles();
const initialState = {
name: "",
color: "",
greetingMessage: "",
outOfHoursMessage: "",
orderQueue: "",
integrationId: "",
promptId: ""
};
const [colorPickerModalOpen, setColorPickerModalOpen] = useState(false);
const [queue, setQueue] = useState(initialState);
const [tab, setTab] = useState(0);
const [schedulesEnabled, setSchedulesEnabled] = useState(false);
const greetingRef = useRef();
const [integrations, setIntegrations] = useState([]);
const [schedules, setSchedules] = useState([
{ weekday: "Segunda-feira", weekdayEn: "monday", startTime: "08:00", endTime: "18:00", },
{ weekday: "Terça-feira", weekdayEn: "tuesday", startTime: "08:00", endTime: "18:00", },
{ weekday: "Quarta-feira", weekdayEn: "wednesday", startTime: "08:00", endTime: "18:00", },
{ weekday: "Quinta-feira", weekdayEn: "thursday", startTime: "08:00", endTime: "18:00", },
{ weekday: "Sexta-feira", weekdayEn: "friday", startTime: "08:00", endTime: "18:00", },
{ weekday: "Sábado", weekdayEn: "saturday", startTime: "08:00", endTime: "12:00", },
{ weekday: "Domingo", weekdayEn: "sunday", startTime: "00:00", endTime: "00:00", },
]);
const [selectedPrompt, setSelectedPrompt] = useState(null);
const [prompts, setPrompts] = useState([]);
useEffect(() => {
(async () => {
try {
const { data } = await api.get("/prompt");
setPrompts(data.prompts);
} catch (err) {
toastError(err);
}
})();
}, []);
useEffect(() => {
api.get(`/settings`).then(({ data }) => {
if (Array.isArray(data)) {
const scheduleType = data.find((d) => d.key === "scheduleType");
if (scheduleType) {
setSchedulesEnabled(scheduleType.value === "queue");
}
}
});
}, []);
useEffect(() => {
(async () => {
try {
const { data } = await api.get("/queueIntegration");
setIntegrations(data.queueIntegrations);
} catch (err) {
toastError(err);
}
})();
}, []);
useEffect(() => {
(async () => {
if (!queueId) return;
try {
const { data } = await api.get(`/queue/${queueId}`);
setQueue((prevState) => {
return { ...prevState, ...data };
});
data.promptId ? setSelectedPrompt(data.promptId) : setSelectedPrompt(null);
setSchedules(data.schedules);
} catch (err) {
toastError(err);
}
})();
return () => {
setQueue({
name: "",
color: "",
greetingMessage: "",
outOfHoursMessage: "",
orderQueue: "",
integrationId: ""
});
};
}, [queueId, open]);
const handleClose = () => {
onClose();
setQueue(initialState);
};
const handleSaveQueue = async (values) => {
try {
if (queueId) {
await api.put(`/queue/${queueId}`, {
...values, schedules, promptId: selectedPrompt ? selectedPrompt : null
});
} else {
await api.post("/queue", {
...values, schedules, promptId: selectedPrompt ? selectedPrompt : null
});
}
toast.success("Queue saved successfully");
handleClose();
} catch (err) {
toastError(err);
}
};
const handleSaveSchedules = async (values) => {
toast.success("Clique em salvar para registar as alterações");
setSchedules(values);
setTab(0);
};
const handleChangePrompt = (e) => {
setSelectedPrompt(e.target.value);
};
return (
<div className={classes.root}>
<Dialog
maxWidth="md"
fullWidth={true}
open={open}
onClose={handleClose}
scroll="paper"
>
<DialogTitle>
{queueId
? `${i18n.t("queueModal.title.edit")}`
: `${i18n.t("queueModal.title.add")}`}
</DialogTitle>
<Tabs
value={tab}
indicatorColor="primary"
textColor="primary"
onChange={(_, v) => setTab(v)}
aria-label="disabled tabs example"
>
<Tab label="Dados da Fila" />
{schedulesEnabled && <Tab label="Horários de Atendimento" />}
</Tabs>
{tab === 0 && (
<Paper>
<Formik
initialValues={queue}
enableReinitialize={true}
validationSchema={QueueSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveQueue(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting, values }) => (
<Form>
<DialogContent dividers>
<Field
as={TextField}
label={i18n.t("queueModal.form.name")}
autoFocus
name="name"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
className={classes.textField}
/>
<Field
as={TextField}
label={i18n.t("queueModal.form.color")}
name="color"
id="color"
onFocus={() => {
setColorPickerModalOpen(true);
greetingRef.current.focus();
}}
error={touched.color && Boolean(errors.color)}
helperText={touched.color && errors.color}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<div
style={{ backgroundColor: values.color }}
className={classes.colorAdorment}
></div>
</InputAdornment>
),
endAdornment: (
<IconButton
size="small"
color="default"
onClick={() => setColorPickerModalOpen(true)}
>
<Colorize />
</IconButton>
),
}}
variant="outlined"
margin="dense"
className={classes.textField}
/>
<ColorPicker
open={colorPickerModalOpen}
handleClose={() => setColorPickerModalOpen(false)}
onChange={(color) => {
values.color = color;
setQueue(() => {
return { ...values, color };
});
}}
/>
<Field
as={TextField}
label={i18n.t("queueModal.form.orderQueue")}
name="orderQueue"
type="orderQueue"
error={touched.orderQueue && Boolean(errors.orderQueue)}
helperText={touched.orderQueue && errors.orderQueue}
variant="outlined"
margin="dense"
className={classes.textField1}
/>
<div>
<FormControl
variant="outlined"
margin="dense"
className={classes.FormControl}
fullWidth
>
<InputLabel id="integrationId-selection-label">
{i18n.t("queueModal.form.integrationId")}
</InputLabel>
<Field
as={Select}
label={i18n.t("queueModal.form.integrationId")}
name="integrationId"
id="integrationId"
placeholder={i18n.t("queueModal.form.integrationId")}
labelId="integrationId-selection-label"
value={values.integrationId || ""}
>
<MenuItem value={""} >{"Nenhum"}</MenuItem>
{integrations.map((integration) => (
<MenuItem key={integration.id} value={integration.id}>
{integration.name}
</MenuItem>
))}
</Field>
</FormControl>
<FormControl
margin="dense"
variant="outlined"
fullWidth
>
<InputLabel>
{i18n.t("whatsappModal.form.prompt")}
</InputLabel>
<Select
labelId="dialog-select-prompt-label"
id="dialog-select-prompt"
name="promptId"
value={selectedPrompt || ""}
onChange={handleChangePrompt}
label={i18n.t("whatsappModal.form.prompt")}
fullWidth
MenuProps={{
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
getContentAnchorEl: null,
}}
>
{prompts.map((prompt) => (
<MenuItem
key={prompt.id}
value={prompt.id}
>
{prompt.name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div style={{ marginTop: 5 }}>
<Field
as={TextField}
label={i18n.t("queueModal.form.greetingMessage")}
type="greetingMessage"
multiline
inputRef={greetingRef}
rows={5}
fullWidth
name="greetingMessage"
error={
touched.greetingMessage &&
Boolean(errors.greetingMessage)
}
helperText={
touched.greetingMessage && errors.greetingMessage
}
variant="outlined"
margin="dense"
/>
{schedulesEnabled && (
<Field
as={TextField}
label={i18n.t("queueModal.form.outOfHoursMessage")}
type="outOfHoursMessage"
multiline
inputRef={greetingRef}
rows={5}
fullWidth
name="outOfHoursMessage"
error={
touched.outOfHoursMessage &&
Boolean(errors.outOfHoursMessage)
}
helperText={
touched.outOfHoursMessage && errors.outOfHoursMessage
}
variant="outlined"
margin="dense"
/>
)}
</div>
<QueueOptions queueId={queueId} />
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("queueModal.buttons.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{queueId
? `${i18n.t("queueModal.buttons.okEdit")}`
: `${i18n.t("queueModal.buttons.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Paper>
)}
{tab === 1 && (
<Paper style={{ padding: 20 }}>
<SchedulesForm
loading={false}
onSubmit={handleSaveSchedules}
initialValues={schedules}
labelSaveButton="Adicionar"
/>
</Paper>
)}
</Dialog>
</div>
);
};
export default QueueModal;
@@ -0,0 +1,345 @@
import React, { useState, useEffect } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Stepper from "@material-ui/core/Stepper";
import Step from "@material-ui/core/Step";
import StepLabel from "@material-ui/core/StepLabel";
import Typography from "@material-ui/core/Typography";
import { Button, IconButton, StepContent, TextField } from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import SaveIcon from "@material-ui/icons/Save";
import EditIcon from "@material-ui/icons/Edit";
import api from "../../services/api";
import toastError from "../../errors/toastError";
const useStyles = makeStyles((theme) => ({
root: {
width: "100%",
//height: 400,
[theme.breakpoints.down("sm")]: {
maxHeight: "20vh",
},
},
button: {
marginRight: theme.spacing(1),
},
input: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
addButton: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
}));
export function QueueOptionStepper({ queueId, options, updateOptions }) {
const classes = useStyles();
const [activeOption, setActiveOption] = useState(-1);
const handleOption = (index) => async () => {
setActiveOption(index);
const option = options[index];
if (option !== undefined && option.id !== undefined) {
try {
const { data } = await api.request({
url: "/queue-options",
method: "GET",
params: { queueId, parentId: option.id },
});
const optionList = data.map((option) => {
return {
...option,
children: [],
edition: false,
};
});
option.children = optionList;
updateOptions();
} catch (e) {
toastError(e);
}
}
};
const handleSave = async (option) => {
try {
if (option.id) {
await api.request({
url: `/queue-options/${option.id}`,
method: "PUT",
data: option,
});
} else {
const { data } = await api.request({
url: `/queue-options`,
method: "POST",
data: option,
});
option.id = data.id;
}
option.edition = false;
updateOptions();
} catch (e) {
toastError(e);
}
};
const handleEdition = (index) => {
options[index].edition = !options[index].edition;
updateOptions();
};
const handleDeleteOption = async (index) => {
const option = options[index];
if (option !== undefined && option.id !== undefined) {
try {
await api.request({
url: `/queue-options/${option.id}`,
method: "DELETE",
});
} catch (e) {
toastError(e);
}
}
options.splice(index, 1);
options.forEach(async (option, order) => {
option.option = order + 1;
await handleSave(option);
});
updateOptions();
};
const handleOptionChangeTitle = (event, index) => {
options[index].title = event.target.value;
updateOptions();
};
const handleOptionChangeMessage = (event, index) => {
options[index].message = event.target.value;
updateOptions();
};
const renderTitle = (index) => {
const option = options[index];
if (option.edition) {
return (
<>
<TextField
value={option.title}
onChange={(event) => handleOptionChangeTitle(event, index)}
size="small"
className={classes.input}
placeholder="Título da opção"
/>
{option.edition && (
<>
<IconButton
color="primary"
variant="outlined"
size="small"
className={classes.button}
onClick={() => handleSave(option)}
>
<SaveIcon />
</IconButton>
<IconButton
variant="outlined"
color="secondary"
size="small"
className={classes.button}
onClick={() => handleDeleteOption(index)}
>
<DeleteOutlineIcon />
</IconButton>
</>
)}
</>
);
}
return (
<>
<Typography>
{option.title !== "" ? option.title : "Título não definido"}
<IconButton
variant="outlined"
size="small"
className={classes.button}
onClick={() => handleEdition(index)}
>
<EditIcon />
</IconButton>
</Typography>
</>
);
};
const renderMessage = (index) => {
const option = options[index];
if (option.edition) {
return (
<>
<TextField
style={{ width: "100%" }}
multiline
value={option.message}
onChange={(event) => handleOptionChangeMessage(event, index)}
size="small"
className={classes.input}
placeholder="Digite o texto da opção"
/>
</>
);
}
return (
<>
<Typography onClick={() => handleEdition(index)}>
{option.message}
</Typography>
</>
);
};
const handleAddOption = (index) => {
const optionNumber = options[index].children.length + 1;
options[index].children.push({
title: "",
message: "",
edition: false,
option: optionNumber,
queueId,
parentId: options[index].id,
children: [],
});
updateOptions();
};
const renderStep = (option, index) => {
return (
<Step key={index}>
<StepLabel style={{ cursor: "pointer" }} onClick={handleOption(index)}>
{renderTitle(index)}
</StepLabel>
<StepContent>
{renderMessage(index)}
{option.id !== undefined && (
<>
<Button
color="primary"
size="small"
onClick={() => handleAddOption(index)}
startIcon={<AddIcon />}
variant="outlined"
className={classes.addButton}
>
Adicionar
</Button>
</>
)}
<QueueOptionStepper
queueId={queueId}
options={option.children}
updateOptions={updateOptions}
/>
</StepContent>
</Step>
);
};
const renderStepper = () => {
return (
<Stepper
style={{ marginBottom: 0, paddingBottom: 0 }}
nonLinear
activeStep={activeOption}
orientation="vertical"
>
{options.map((option, index) => renderStep(option, index))}
</Stepper>
);
};
return renderStepper();
}
export function QueueOptions({ queueId }) {
const classes = useStyles();
const [options, setOptions] = useState([]);
useEffect(() => {
if (queueId) {
const fetchOptions = async () => {
try {
const { data } = await api.request({
url: "/queue-options",
method: "GET",
params: { queueId, parentId: -1 },
});
const optionList = data.map((option) => {
return {
...option,
children: [],
edition: false,
};
});
setOptions(optionList);
} catch (e) {
toastError(e);
}
};
fetchOptions();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const renderStepper = () => {
if (options.length > 0) {
return (
<QueueOptionStepper
queueId={queueId}
updateOptions={updateOptions}
options={options}
/>
);
}
};
const updateOptions = () => {
setOptions([...options]);
};
const addOption = () => {
const newOption = {
title: "",
message: "",
edition: false,
option: options.length + 1,
queueId,
parentId: null,
children: [],
};
setOptions([...options, newOption]);
};
return (
<div className={classes.root}>
<br />
<Typography>
Opções
<Button
color="primary"
size="small"
onClick={addOption}
startIcon={<AddIcon />}
style={{ marginLeft: 10 }}
variant="outlined"
>
Adicionar
</Button>
</Typography>
{renderStepper()}
</div>
);
}
@@ -0,0 +1,90 @@
import React, { useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import Chip from "@material-ui/core/Chip";
import toastError from "../../errors/toastError";
import api from "../../services/api";
import { i18n } from "../../translate/i18n";
const useStyles = makeStyles(theme => ({
chips: {
display: "flex",
flexWrap: "wrap",
},
chip: {
margin: 2,
},
}));
const QueueSelect = ({ selectedQueueIds, onChange }) => {
const classes = useStyles();
const [queues, setQueues] = useState([]);
useEffect(() => {
(async () => {
try {
const { data } = await api.get("/queue");
setQueues(data);
} catch (err) {
toastError(err);
}
})();
}, []);
const handleChange = e => {
onChange(e.target.value);
};
return (
<div style={{ marginTop: 6 }}>
<FormControl fullWidth margin="dense" variant="outlined">
<InputLabel>{i18n.t("queueSelect.inputLabel")}</InputLabel>
<Select
multiple
labelWidth={60}
value={selectedQueueIds}
onChange={handleChange}
MenuProps={{
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
getContentAnchorEl: null,
}}
renderValue={selected => (
<div className={classes.chips}>
{selected?.length > 0 &&
selected.map(id => {
const queue = queues.find(q => q.id === id);
return queue ? (
<Chip
key={id}
style={{ backgroundColor: queue.color }}
variant="outlined"
label={queue.name}
className={classes.chip}
/>
) : null;
})}
</div>
)}
>
{queues.map(queue => (
<MenuItem key={queue.id} value={queue.id}>
{queue.name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
);
};
export default QueueSelect;
@@ -0,0 +1,112 @@
import React, { useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import Chip from "@material-ui/core/Chip";
import toastError from "../../errors/toastError";
import api from "../../services/api";
import { i18n } from "../../translate/i18n";
const useStyles = makeStyles(theme => ({
chips: {
display: "flex",
flexWrap: "wrap",
},
chip: {
margin: 2,
},
}));
const QueueSelect = ({ selectedQueueIds, onChange, multiple = true, title = i18n.t("queueSelect.inputLabel") }) => {
const classes = useStyles();
const [queues, setQueues] = useState([]);
useEffect(() => {
fetchQueues();
}, []);
const fetchQueues = async () => {
try {
const { data } = await api.get("/queue");
setQueues(data);
} catch (err) {
toastError(err);
}
}
const handleChange = e => {
onChange(e.target.value);
};
return (
<div >
<FormControl fullWidth margin="dense" variant="outlined">
<InputLabel shrink={selectedQueueIds ? true : false} >{title}</InputLabel>
<Select
label={title}
multiple={multiple}
labelWidth={60}
value={selectedQueueIds}
onChange={handleChange}
MenuProps={{
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
getContentAnchorEl: null,
}}
renderValue={selected => {
return (
<div className={classes.chips}>
{selected?.length > 0 && multiple ? (
selected.map(id => {
const queue = queues.find(q => q.id === id);
return queue ? (
<Chip
key={id}
style={{ backgroundColor: queue.color }}
variant="outlined"
label={queue.name}
className={classes.chip}
/>
) : null;
})
) :
(
<Chip
key={selected}
variant="outlined"
style={{ backgroundColor: queues.find(q => q.id === selected)?.color }}
label={queues.find(q => q.id === selected)?.name}
className={classes.chip}
/>
)
}
</div>
)
}}
>
{!multiple && <MenuItem value={null}>Nenhum</MenuItem>}
{queues.map(queue => (
<MenuItem key={queue.id} value={queue.id}>
{queue.name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
);
};
export default QueueSelect;
@@ -0,0 +1,90 @@
import React, { useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import Chip from "@material-ui/core/Chip";
import toastError from "../../errors/toastError";
import api from "../../services/api";
import { i18n } from "../../translate/i18n";
const useStyles = makeStyles(theme => ({
chips: {
display: "flex",
flexWrap: "wrap",
},
chip: {
margin: 2,
},
}));
const QueueSelect = ({ selectedQueueIds, onChange }) => {
const classes = useStyles();
const [queues, setQueues] = useState([]);
useEffect(() => {
(async () => {
try {
const { data } = await api.get("/queue");
setQueues(data);
} catch (err) {
toastError(err);
}
})();
}, []);
const handleChange = e => {
onChange(e.target.value);
};
return (
<div style={{ marginTop: 6 }}>
<FormControl fullWidth margin="dense" variant="outlined">
<InputLabel>{i18n.t("queueSelect.inputLabel")}</InputLabel>
<Select
multiple
labelWidth={60}
value={selectedQueueIds}
onChange={handleChange}
MenuProps={{
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
getContentAnchorEl: null,
}}
renderValue={selected => (
<div className={classes.chips}>
{selected?.length > 0 &&
selected.map(id => {
const queue = queues.find(q => q.id === id);
return queue ? (
<Chip
key={id}
style={{ backgroundColor: queue.color }}
variant="outlined"
label={queue.name}
className={classes.chip}
/>
) : null;
})}
</div>
)}
>
{queues.map(queue => (
<MenuItem key={queue.id} value={queue.id}>
{queue.name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
);
};
export default QueueSelect;
@@ -0,0 +1,92 @@
import React, { useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import Chip from "@material-ui/core/Chip";
import toastError from "../../errors/toastError";
import api from "../../services/api";
import { i18n } from "../../translate/i18n";
const useStyles = makeStyles(theme => ({
chips: {
display: "flex",
flexWrap: "wrap",
},
chip: {
margin: 2,
},
}));
const QueueSelectCustom = ({ selectedQueueIds, companyId, onChange }) => {
const classes = useStyles();
const [queues, setQueues] = useState([]);
useEffect(() => {
(async () => {
try {
const { data } = await api.get("/queue", {
params: { companyId }
});
setQueues(data);
} catch (err) {
toastError(err);
}
})();
}, [companyId]);
const handleChange = e => {
onChange(e.target.value);
};
return (
<div style={{ marginTop: 6 }}>
<FormControl fullWidth margin="dense" variant="outlined">
<InputLabel>{i18n.t("queueSelect.inputLabel")}</InputLabel>
<Select
multiple
labelWidth={60}
value={selectedQueueIds}
onChange={handleChange}
MenuProps={{
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
getContentAnchorEl: null,
}}
renderValue={selected => (
<div className={classes.chips}>
{selected?.length > 0 &&
selected.map(id => {
const queue = queues.find(q => q.id === id);
return queue ? (
<Chip
key={id}
style={{ backgroundColor: queue.color }}
variant="outlined"
label={queue.name}
className={classes.chip}
/>
) : null;
})}
</div>
)}
>
{queues.map(queue => (
<MenuItem key={queue.id} value={queue.id}>
{queue.name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
);
};
export default QueueSelectCustom;
@@ -0,0 +1,66 @@
import React, { useEffect, useState } from "react";
import { Field } from "formik";
import { makeStyles } from "@material-ui/core/styles";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import toastError from "../../errors/toastError";
import api from "../../services/api";
import { i18n } from "../../translate/i18n";
import Typography from "@material-ui/core/Typography";
const useStyles = makeStyles(theme => ({
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
}));
const QueueSelectSingle = () => {
const classes = useStyles();
const [queues, setQueues] = useState([]);
useEffect(() => {
(async () => {
try {
const { data } = await api.get("/queue");
setQueues(data);
} catch (err) {
toastError(`QUEUESELETSINGLE >>> ${err}`);
}
})();
}, []);
return (
<div style={{ marginTop: 6 }}>
<FormControl
variant="outlined"
className={classes.FormControl}
margin="dense"
fullWidth
>
<div>
<Typography>
{i18n.t("queueSelect.inputLabel")}
</Typography>
<Field
as={Select}
label={i18n.t("queueSelect.inputLabel")}
name="queueId"
labelId="queue-selection-label"
id="queue-selection"
fullWidth
>
{queues.map(queue => (
<MenuItem key={queue.id} value={queue.id}>
{queue.name}
</MenuItem>
))}
</Field>
</div>
</FormControl>
</div>
);
};
export default QueueSelectSingle;
@@ -0,0 +1,332 @@
import React, { useContext, useState, useEffect, useRef } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import CircularProgress from "@material-ui/core/CircularProgress";
import AttachFileIcon from "@material-ui/icons/AttachFile";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import IconButton from "@material-ui/core/IconButton";
import { i18n } from "../../translate/i18n";
import { head } from "lodash";
import api from "../../services/api";
import toastError from "../../errors/toastError";
import { AuthContext } from "../../context/Auth/AuthContext";
import MessageVariablesPicker from "../MessageVariablesPicker";
import ButtonWithSpinner from "../ButtonWithSpinner";
import {
FormControl,
Grid,
InputLabel,
MenuItem,
Select,
} from "@material-ui/core";
import ConfirmationModal from "../ConfirmationModal";
const path = require('path');
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
flexWrap: "wrap",
},
multFieldLine: {
display: "flex",
"& > *:not(:last-child)": {
marginRight: theme.spacing(1),
},
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
colorAdorment: {
width: 20,
height: 20,
},
}));
const QuickeMessageSchema = Yup.object().shape({
shortcode: Yup.string().required("Obrigatório"),
// message: Yup.string().required("Obrigatório"),
});
const QuickMessageDialog = ({ open, onClose, quickemessageId, reload }) => {
const classes = useStyles();
const { user } = useContext(AuthContext);
const { profile } = user;
const messageInputRef = useRef();
const initialState = {
shortcode: "",
message: "",
geral: false,
status: true,
};
const [confirmationOpen, setConfirmationOpen] = useState(false);
const [quickemessage, setQuickemessage] = useState(initialState);
const [attachment, setAttachment] = useState(null);
const attachmentFile = useRef(null);
useEffect(() => {
try {
(async () => {
if (!quickemessageId) return;
const { data } = await api.get(`/quick-messages/${quickemessageId}`);
setQuickemessage((prevState) => {
return { ...prevState, ...data };
});
})();
} catch (err) {
toastError(err);
}
}, [quickemessageId, open]);
const handleClose = () => {
setQuickemessage(initialState);
setAttachment(null);
onClose();
};
const handleAttachmentFile = (e) => {
const file = head(e.target.files);
if (file) {
setAttachment(file);
}
};
const handleSaveQuickeMessage = async (values) => {
const quickemessageData = { ...values, isMedia: true, mediaPath: attachment ? String(attachment.name).replace(/ /g, "_") : values.mediaPath ? path.basename(values.mediaPath).replace(/ /g, "_") : null };
try {
if (quickemessageId) {
await api.put(`/quick-messages/${quickemessageId}`, quickemessageData);
if (attachment != null) {
const formData = new FormData();
formData.append("typeArch", "quickMessage");
formData.append("file", attachment);
await api.post(
`/quick-messages/${quickemessageId}/media-upload`,
formData
);
}
} else {
const { data } = await api.post("/quick-messages", quickemessageData);
if (attachment != null) {
const formData = new FormData();
formData.append("typeArch", "quickMessage");
formData.append("file", attachment);
await api.post(`/quick-messages/${data.id}/media-upload`, formData);
}
}
toast.success(i18n.t("quickMessages.toasts.success"));
if (typeof reload == "function") {
reload();
}
} catch (err) {
toastError(err);
}
handleClose();
};
const deleteMedia = async () => {
if (attachment) {
setAttachment(null);
attachmentFile.current.value = null;
}
if (quickemessage.mediaPath) {
await api.delete(`/quick-messages/${quickemessage.id}/media-upload`);
setQuickemessage((prev) => ({
...prev,
mediaPath: null,
}));
toast.success(i18n.t("quickMessages.toasts.deleted"));
if (typeof reload == "function") {
reload();
}
}
};
const handleClickMsgVar = async (msgVar, setValueFunc) => {
const el = messageInputRef.current;
const firstHalfText = el.value.substring(0, el.selectionStart);
const secondHalfText = el.value.substring(el.selectionEnd);
const newCursorPos = el.selectionStart + msgVar.length;
setValueFunc("message", `${firstHalfText}${msgVar}${secondHalfText}`);
await new Promise(r => setTimeout(r, 100));
messageInputRef.current.setSelectionRange(newCursorPos, newCursorPos);
};
return (
<div className={classes.root}>
<ConfirmationModal
title={i18n.t("quickMessages.confirmationModal.deleteTitle")}
open={confirmationOpen}
onClose={() => setConfirmationOpen(false)}
onConfirm={deleteMedia}
>
{i18n.t("quickMessages.confirmationModal.deleteMessage")}
</ConfirmationModal>
<Dialog
open={open}
onClose={handleClose}
maxWidth="xs"
fullWidth
scroll="paper"
>
<DialogTitle id="form-dialog-title">
{quickemessageId
? `${i18n.t("quickMessages.dialog.edit")}`
: `${i18n.t("quickMessages.dialog.add")}`}
</DialogTitle>
<div style={{ display: "none" }}>
<input
type="file"
ref={attachmentFile}
onChange={(e) => handleAttachmentFile(e)}
/>
</div>
<Formik
initialValues={quickemessage}
enableReinitialize={true}
validationSchema={QuickeMessageSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveQuickeMessage(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting, setFieldValue, values }) => (
<Form>
<DialogContent dividers>
<Grid spacing={2} container>
<Grid xs={12} item>
<Field
as={TextField}
autoFocus
label={i18n.t("quickMessages.dialog.shortcode")}
name="shortcode"
error={touched.shortcode && Boolean(errors.shortcode)}
helperText={touched.shortcode && errors.shortcode}
variant="outlined"
margin="dense"
fullWidth
/>
</Grid>
<Grid xs={12} item>
<Field
as={TextField}
label={i18n.t("quickMessages.dialog.message")}
name="message"
inputRef={messageInputRef}
error={touched.message && Boolean(errors.message)}
helperText={touched.message && errors.message}
variant="outlined"
margin="dense"
multiline={true}
rows={7}
fullWidth
// disabled={quickemessage.mediaPath || attachment ? true : false}
/>
</Grid>
<Grid item>
<MessageVariablesPicker
disabled={isSubmitting}
onClick={value => handleClickMsgVar(value, setFieldValue)}
/>
</Grid>
{(quickemessage.mediaPath || attachment) && (
<Grid xs={12} item>
<Button startIcon={<AttachFileIcon />}>
{attachment ? attachment.name : quickemessage.mediaName}
</Button>
<IconButton
onClick={() => setConfirmationOpen(true)}
color="secondary"
>
<DeleteOutlineIcon color="secondary" />
</IconButton>
</Grid>
)}
</Grid>
</DialogContent>
<DialogActions>
{!attachment && !quickemessage.mediaPath && (
<Button
color="primary"
onClick={() => attachmentFile.current.click()}
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("quickMessages.buttons.attach")}
</Button>
)}
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("quickMessages.buttons.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{quickemessageId
? `${i18n.t("quickMessages.buttons.edit")}`
: `${i18n.t("quickMessages.buttons.add")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default QuickMessageDialog;
@@ -0,0 +1,93 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import {
Table,
TableHead,
TableBody,
TableCell,
TableRow,
IconButton
} from '@material-ui/core';
import {
Edit as EditIcon,
DeleteOutline as DeleteOutlineIcon
} from "@material-ui/icons";
import TableRowSkeleton from "../../components/TableRowSkeleton";
function QuickMessagesTable(props) {
const { messages, showLoading, editMessage, deleteMessage, readOnly } = props
const [loading, setLoading] = useState(true)
const [rows, setRows] = useState([])
useEffect(() => {
if (Array.isArray(messages)) {
setRows(messages)
}
if (showLoading !== undefined) {
setLoading(showLoading)
}
}, [messages, showLoading])
const handleEdit = (message) => {
editMessage(message)
}
const handleDelete = (message) => {
deleteMessage(message)
}
const renderRows = () => {
return rows.map((message) => {
return (
<TableRow key={message.id}>
<TableCell align="center">{message.shortcode}</TableCell>
<TableCell align="left">{message.message}</TableCell>
{ !readOnly ? (
<TableCell align="center">
<IconButton
size="small"
onClick={() => handleEdit(message)}
>
<EditIcon />
</IconButton>
<IconButton
size="small"
onClick={() => handleDelete(message)}
>
<DeleteOutlineIcon />
</IconButton>
</TableCell>
) : null}
</TableRow>
)
})
}
return (
<Table size="small">
<TableHead>
<TableRow>
<TableCell align="center">Atalho</TableCell>
<TableCell align="left">Mensagem</TableCell>
{ !readOnly ? (
<TableCell align="center">Ações</TableCell>
) : null}
</TableRow>
</TableHead>
<TableBody>
{loading ? <TableRowSkeleton columns={readOnly ? 2 : 3} /> : renderRows()}
</TableBody>
</Table>
)
}
QuickMessagesTable.propTypes = {
messages: PropTypes.array.isRequired,
showLoading: PropTypes.bool,
editMessage: PropTypes.func.isRequired,
deleteMessage: PropTypes.func.isRequired
}
export default QuickMessagesTable;
@@ -0,0 +1,382 @@
import React, { useState, useEffect, useContext, useRef } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { useHistory } from "react-router-dom";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import CircularProgress from "@material-ui/core/CircularProgress";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
import { FormControl, Grid, IconButton } from "@material-ui/core";
import Autocomplete from "@material-ui/lab/Autocomplete";
import moment from "moment"
import { AuthContext } from "../../context/Auth/AuthContext";
import { isArray, capitalize } from "lodash";
import DeleteOutline from "@material-ui/icons/DeleteOutline";
import AttachFile from "@material-ui/icons/AttachFile";
import { head } from "lodash";
import ConfirmationModal from "../ConfirmationModal";
import MessageVariablesPicker from "../MessageVariablesPicker";
const useStyles = makeStyles(theme => ({
root: {
display: "flex",
flexWrap: "wrap",
},
multFieldLine: {
display: "flex",
"& > *:not(:last-child)": {
marginRight: theme.spacing(1),
},
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
}));
const ScheduleSchema = Yup.object().shape({
body: Yup.string()
.min(5, "Mensagem muito curta")
.required("Obrigatório"),
contactId: Yup.number().required("Obrigatório"),
sendAt: Yup.string().required("Obrigatório")
});
const ScheduleModal = ({ open, onClose, scheduleId, contactId, cleanContact, reload }) => {
const classes = useStyles();
const history = useHistory();
const { user } = useContext(AuthContext);
const initialState = {
body: "",
contactId: "",
sendAt: moment().add(1, 'hour').format('YYYY-MM-DDTHH:mm'),
sentAt: ""
};
const initialContact = {
id: "",
name: ""
}
const [schedule, setSchedule] = useState(initialState);
const [currentContact, setCurrentContact] = useState(initialContact);
const [contacts, setContacts] = useState([initialContact]);
const [attachment, setAttachment] = useState(null);
const attachmentFile = useRef(null);
const [confirmationOpen, setConfirmationOpen] = useState(false);
const messageInputRef = useRef();
useEffect(() => {
if (contactId && contacts.length) {
const contact = contacts.find(c => c.id === contactId);
if (contact) {
setCurrentContact(contact);
}
}
}, [contactId, contacts]);
useEffect(() => {
const { companyId } = user;
if (open) {
try {
(async () => {
const { data: contactList } = await api.get('/contacts/list', { params: { companyId: companyId } });
let customList = contactList.map((c) => ({ id: c.id, name: c.name }));
if (isArray(customList)) {
setContacts([{ id: "", name: "" }, ...customList]);
}
if (contactId) {
setSchedule(prevState => {
return { ...prevState, contactId }
});
}
if (!scheduleId) return;
const { data } = await api.get(`/schedules/${scheduleId}`);
setSchedule(prevState => {
return { ...prevState, ...data, sendAt: moment(data.sendAt).format('YYYY-MM-DDTHH:mm') };
});
setCurrentContact(data.contact);
})()
} catch (err) {
toastError(err);
}
}
}, [scheduleId, contactId, open, user]);
const handleClose = () => {
onClose();
setAttachment(null);
setSchedule(initialState);
};
const handleAttachmentFile = (e) => {
const file = head(e.target.files);
if (file) {
setAttachment(file);
}
};
const handleSaveSchedule = async values => {
const scheduleData = { ...values, userId: user.id };
try {
if (scheduleId) {
await api.put(`/schedules/${scheduleId}`, scheduleData);
if (attachment != null) {
const formData = new FormData();
formData.append("file", attachment);
await api.post(
`/schedules/${scheduleId}/media-upload`,
formData
);
}
} else {
const { data } = await api.post("/schedules", scheduleData);
if (attachment != null) {
const formData = new FormData();
formData.append("file", attachment);
await api.post(`/schedules/${data.id}/media-upload`, formData);
}
}
toast.success(i18n.t("scheduleModal.success"));
if (typeof reload == 'function') {
reload();
}
if (contactId) {
if (typeof cleanContact === 'function') {
cleanContact();
history.push('/schedules');
}
}
} catch (err) {
toastError(err);
}
setCurrentContact(initialContact);
setSchedule(initialState);
handleClose();
};
const handleClickMsgVar = async (msgVar, setValueFunc) => {
const el = messageInputRef.current;
const firstHalfText = el.value.substring(0, el.selectionStart);
const secondHalfText = el.value.substring(el.selectionEnd);
const newCursorPos = el.selectionStart + msgVar.length;
setValueFunc("body", `${firstHalfText}${msgVar}${secondHalfText}`);
await new Promise(r => setTimeout(r, 100));
messageInputRef.current.setSelectionRange(newCursorPos, newCursorPos);
};
const deleteMedia = async () => {
if (attachment) {
setAttachment(null);
attachmentFile.current.value = null;
}
if (schedule.mediaPath) {
await api.delete(`/schedules/${schedule.id}/media-upload`);
setSchedule((prev) => ({
...prev,
mediaPath: null,
}));
toast.success(i18n.t("scheduleModal.toasts.deleted"));
if (typeof reload == "function") {
console.log(reload);
console.log("1");
reload();
}
}
};
return (
<div className={classes.root}>
<ConfirmationModal
title={i18n.t("scheduleModal.confirmationModal.deleteTitle")}
open={confirmationOpen}
onClose={() => setConfirmationOpen(false)}
onConfirm={deleteMedia}
>
{i18n.t("scheduleModal.confirmationModal.deleteMessage")}
</ConfirmationModal>
<Dialog
open={open}
onClose={handleClose}
maxWidth="xs"
fullWidth
scroll="paper"
>
<DialogTitle id="form-dialog-title">
{schedule.status === 'ERRO' ? 'Erro de Envio' : `Mensagem ${capitalize(schedule.status)}`}
</DialogTitle>
<div style={{ display: "none" }}>
<input
type="file"
accept=".png,.jpg,.jpeg"
ref={attachmentFile}
onChange={(e) => handleAttachmentFile(e)}
/>
</div>
<Formik
initialValues={schedule}
enableReinitialize={true}
validationSchema={ScheduleSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveSchedule(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting, values, setFieldValue }) => (
<Form>
<DialogContent dividers>
<div className={classes.multFieldLine}>
<FormControl
variant="outlined"
fullWidth
>
<Autocomplete
fullWidth
value={currentContact}
options={contacts}
onChange={(e, contact) => {
const contactId = contact ? contact.id : '';
setSchedule({ ...schedule, contactId });
setCurrentContact(contact ? contact : initialContact);
}}
getOptionLabel={(option) => option.name}
getOptionSelected={(option, value) => {
return value.id === option.id
}}
renderInput={(params) => <TextField {...params} variant="outlined" placeholder="Contato" />}
/>
</FormControl>
</div>
<br />
<div className={classes.multFieldLine}>
<Field
as={TextField}
rows={9}
multiline={true}
label={i18n.t("scheduleModal.form.body")}
name="body"
inputRef={messageInputRef}
error={touched.body && Boolean(errors.body)}
helperText={touched.body && errors.body}
variant="outlined"
margin="dense"
fullWidth
/>
</div>
<Grid item>
<MessageVariablesPicker
disabled={isSubmitting}
onClick={value => handleClickMsgVar(value, setFieldValue)}
/>
</Grid>
<br />
<div className={classes.multFieldLine}>
<Field
as={TextField}
label={i18n.t("scheduleModal.form.sendAt")}
type="datetime-local"
name="sendAt"
InputLabelProps={{
shrink: true,
}}
error={touched.sendAt && Boolean(errors.sendAt)}
helperText={touched.sendAt && errors.sendAt}
variant="outlined"
fullWidth
/>
</div>
{(schedule.mediaPath || attachment) && (
<Grid xs={12} item>
<Button startIcon={<AttachFile />}>
{attachment ? attachment.name : schedule.mediaName}
</Button>
<IconButton
onClick={() => setConfirmationOpen(true)}
color="secondary"
>
<DeleteOutline color="secondary" />
</IconButton>
</Grid>
)}
</DialogContent>
<DialogActions>
{!attachment && !schedule.mediaPath && (
<Button
color="primary"
onClick={() => attachmentFile.current.click()}
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("quickMessages.buttons.attach")}
</Button>
)}
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("scheduleModal.buttons.cancel")}
</Button>
{(schedule.sentAt === null || schedule.sentAt === "") && (
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{scheduleId
? `${i18n.t("scheduleModal.buttons.okEdit")}`
: `${i18n.t("scheduleModal.buttons.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
)}
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default ScheduleModal;
@@ -0,0 +1,140 @@
import React, { useState, useEffect } from "react";
import { makeStyles, TextField, Grid, Container } from "@material-ui/core";
import { Formik, Form, FastField, FieldArray } from "formik";
import { isArray } from "lodash";
import NumberFormat from "react-number-format";
import ButtonWithSpinner from "../ButtonWithSpinner";
const useStyles = makeStyles((theme) => ({
root: {
width: "100%",
},
fullWidth: {
width: "100%",
},
textfield: {
width: "100%",
},
row: {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
},
control: {
paddingRight: theme.spacing(1),
paddingLeft: theme.spacing(1),
},
buttonContainer: {
textAlign: "right",
padding: theme.spacing(1),
},
}));
function SchedulesForm(props) {
const { initialValues, onSubmit, loading, labelSaveButton } = props;
const classes = useStyles();
const [schedules, setSchedules] = useState([
{ weekday: "Segunda-feira", weekdayEn: "monday", startTime: "", endTime: "", },
{ weekday: "Terça-feira", weekdayEn: "tuesday", startTime: "", endTime: "", },
{ weekday: "Quarta-feira", weekdayEn: "wednesday", startTime: "", endTime: "", },
{ weekday: "Quinta-feira", weekdayEn: "thursday", startTime: "", endTime: "", },
{ weekday: "Sexta-feira", weekdayEn: "friday", startTime: "", endTime: "" },
{ weekday: "Sábado", weekdayEn: "saturday", startTime: "", endTime: "" },
{ weekday: "Domingo", weekdayEn: "sunday", startTime: "", endTime: "" },
]);
useEffect(() => {
if (isArray(initialValues) && initialValues.length > 0) {
setSchedules(initialValues);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValues]);
const handleSubmit = (data) => {
onSubmit(data);
};
return (
<Formik
enableReinitialize
className={classes.fullWidth}
initialValues={{ schedules }}
onSubmit={({ schedules }) =>
setTimeout(() => {
handleSubmit(schedules);
}, 500)
}
>
{({ values }) => (
<Form className={classes.fullWidth}>
<FieldArray
name="schedules"
render={(arrayHelpers) => (
<Grid spacing={4} container>
{values.schedules.map((item, index) => {
return (
<Container>
<FastField
as={TextField}
label="Dia da Semana"
name={`schedules[${index}].weekday`}
disabled
variant="outlined"
style={{ marginRight: "3.2%", width: "30%" }}
margin="dense"
/>
<FastField
name={`schedules[${index}].startTime`}
>
{({ field }) => (
<NumberFormat
label="Hora de Inicial"
{...field}
variant="outlined"
margin="dense"
customInput={TextField}
format="##:##"
style={{ marginRight: "3.2%", width: "30%" }}
/>
)}
</FastField>
<FastField
name={`schedules[${index}].endTime`}
>
{({ field }) => (
<NumberFormat
label="Hora de Final"
{...field}
variant="outlined"
margin="dense"
customInput={TextField}
format="##:##"
style={{ marginRight: "3.2%", width: "30%" }}
/>
)}
</FastField>
</Container>
);
})}
</Grid>
)}
></FieldArray>
<div style={{ textAlign: "center", marginTop: "2%" }} className={classes.buttonContainer}>
<ButtonWithSpinner
loading={loading}
type="submit"
color="primary"
variant="contained"
>
{labelSaveButton ?? "Salvar"}
</ButtonWithSpinner>
</div>
</Form>
)}
</Formik>
);
}
export default SchedulesForm;
+720
View File
@@ -0,0 +1,720 @@
import React, { useEffect, useState } from "react";
import Grid from "@material-ui/core/Grid";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Select from "@material-ui/core/Select";
import FormHelperText from "@material-ui/core/FormHelperText";
import TextField from "@material-ui/core/TextField";
import Title from "../Title";
import Paper from "@material-ui/core/Paper";
import Typography from "@material-ui/core/Typography";
import useSettings from "../../hooks/useSettings";
import { ToastContainer, toast } from 'react-toastify';
import { makeStyles } from "@material-ui/core/styles";
import { grey, blue } from "@material-ui/core/colors";
import { Tabs, Tab } from "@material-ui/core";
//import 'react-toastify/dist/ReactToastify.css';
const useStyles = makeStyles((theme) => ({
container: {
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4),
},
fixedHeightPaper: {
padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column",
height: 240,
},
tab: {
backgroundColor: theme.palette.options, //DARK MODE PLW DESIGN//
borderRadius: 4,
width: "100%",
"& .MuiTab-wrapper": {
color: theme.palette.fontecor,
}, //DARK MODE PLW DESIGN//
"& .MuiTabs-flexContainer": {
justifyContent: "center"
}
},
paper: {
padding: theme.spacing(2),
display: "flex",
alignItems: "center",
marginBottom: 12,
width: "100%",
},
cardAvatar: {
fontSize: "55px",
color: grey[500],
backgroundColor: "#ffffff",
width: theme.spacing(7),
height: theme.spacing(7),
},
cardTitle: {
fontSize: "18px",
color: blue[700],
},
cardSubtitle: {
color: grey[600],
fontSize: "14px",
},
alignRight: {
textAlign: "right",
},
fullWidth: {
width: "100%",
},
selectContainer: {
width: "100%",
textAlign: "left",
},
}));
export default function Options(props) {
const { settings, scheduleTypeChanged } = props;
const classes = useStyles();
const [userRating, setUserRating] = useState("disabled");
const [scheduleType, setScheduleType] = useState("disabled");
const [callType, setCallType] = useState("enabled");
const [chatbotType, setChatbotType] = useState("");
const [CheckMsgIsGroup, setCheckMsgIsGroupType] = useState("enabled");
const [loadingUserRating, setLoadingUserRating] = useState(false);
const [loadingScheduleType, setLoadingScheduleType] = useState(false);
const [loadingCallType, setLoadingCallType] = useState(false);
const [loadingChatbotType, setLoadingChatbotType] = useState(false);
const [loadingCheckMsgIsGroup, setCheckMsgIsGroup] = useState(false);
//const [ipixcType, setIpIxcType] = useState("");
//const [loadingIpIxcType, setLoadingIpIxcType] = useState(false);
//const [tokenixcType, setTokenIxcType] = useState("");
//const [loadingTokenIxcType, setLoadingTokenIxcType] = useState(false);
//const [ipmkauthType, setIpMkauthType] = useState("");
//const [loadingIpMkauthType, setLoadingIpMkauthType] = useState(false);
//const [clientidmkauthType, setClientIdMkauthType] = useState("");
//const [loadingClientIdMkauthType, setLoadingClientIdMkauthType] = useState(false);
//const [clientsecretmkauthType, setClientSecrectMkauthType] = useState("");
//const [loadingClientSecrectMkauthType, setLoadingClientSecrectMkauthType] = useState(false);
const [asaasType, setAsaasType] = useState("");
const [loadingAsaasType, setLoadingAsaasType] = useState(false);
// recursos a mais da plw design
const [SendGreetingAccepted, setSendGreetingAccepted] = useState("disabled");
const [loadingSendGreetingAccepted, setLoadingSendGreetingAccepted] = useState(false);
const [SettingsTransfTicket, setSettingsTransfTicket] = useState("disabled");
const [loadingSettingsTransfTicket, setLoadingSettingsTransfTicket] = useState(false);
const [sendGreetingMessageOneQueues, setSendGreetingMessageOneQueues] = useState("disabled");
const [loadingSendGreetingMessageOneQueues, setLoadingSendGreetingMessageOneQueues] = useState(false);
const { update } = useSettings();
useEffect(() => {
if (Array.isArray(settings) && settings.length) {
const userRating = settings.find((s) => s.key === "userRating");
if (userRating) {
setUserRating(userRating.value);
}
const scheduleType = settings.find((s) => s.key === "scheduleType");
if (scheduleType) {
setScheduleType(scheduleType.value);
}
const callType = settings.find((s) => s.key === "call");
if (callType) {
setCallType(callType.value);
}
const CheckMsgIsGroup = settings.find((s) => s.key === "CheckMsgIsGroup");
if (CheckMsgIsGroup) {
setCheckMsgIsGroupType(CheckMsgIsGroup.value);
}
{/*PLW DESIGN SAUDAÇÃO*/}
const SendGreetingAccepted = settings.find((s) => s.key === "sendGreetingAccepted");
if (SendGreetingAccepted) {
setSendGreetingAccepted(SendGreetingAccepted.value);
}
{/*PLW DESIGN SAUDAÇÃO*/}
{/*TRANSFERIR TICKET*/}
const SettingsTransfTicket = settings.find((s) => s.key === "sendMsgTransfTicket");
if (SettingsTransfTicket) {
setSettingsTransfTicket(SettingsTransfTicket.value);
}
{/*TRANSFERIR TICKET*/}
const sendGreetingMessageOneQueues = settings.find((s) => s.key === "sendGreetingMessageOneQueues");
if (sendGreetingMessageOneQueues) {
setSendGreetingMessageOneQueues(sendGreetingMessageOneQueues.value)
}
const chatbotType = settings.find((s) => s.key === "chatBotType");
if (chatbotType) {
setChatbotType(chatbotType.value);
}
{/*const ipixcType = settings.find((s) => s.key === "ipixc");
if (ipixcType) {
setIpIxcType(ipixcType.value);
}*/}
{/*const tokenixcType = settings.find((s) => s.key === "tokenixc");
if (tokenixcType) {
setTokenIxcType(tokenixcType.value);
}*/}
{/*const ipmkauthType = settings.find((s) => s.key === "ipmkauth");
if (ipmkauthType) {
setIpMkauthType(ipmkauthType.value);
}*/}
{/* const clientidmkauthType = settings.find((s) => s.key === "clientidmkauth");
if (clientidmkauthType) {
setClientIdMkauthType(clientidmkauthType.value);
}*/}
{/*const clientsecretmkauthType = settings.find((s) => s.key === "clientsecretmkauth");
if (clientsecretmkauthType) {
setClientSecrectMkauthType(clientsecretmkauthType.value);
}*/}
const asaasType = settings.find((s) => s.key === "asaas");
if (asaasType) {
setAsaasType(asaasType.value);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings]);
async function handleChangeUserRating(value) {
setUserRating(value);
setLoadingUserRating(true);
await update({
key: "userRating",
value,
});
toast.success("Operação atualizada com sucesso.");
setLoadingUserRating(false);
}
async function handleSendGreetingMessageOneQueues(value) {
setSendGreetingMessageOneQueues(value);
setLoadingSendGreetingMessageOneQueues(true);
await update({
key: "sendGreetingMessageOneQueues",
value,
});
toast.success("Operação atualizada com sucesso.");
setLoadingSendGreetingMessageOneQueues(false);
}
async function handleScheduleType(value) {
setScheduleType(value);
setLoadingScheduleType(true);
await update({
key: "scheduleType",
value,
});
//toast.success("Oraçãpeo atualizada com sucesso.");
toast.success('Operação atualizada com sucesso.', {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: false,
draggable: true,
theme: "light",
});
setLoadingScheduleType(false);
if (typeof scheduleTypeChanged === "function") {
scheduleTypeChanged(value);
}
}
async function handleCallType(value) {
setCallType(value);
setLoadingCallType(true);
await update({
key: "call",
value,
});
toast.success("Operação atualizada com sucesso.");
setLoadingCallType(false);
}
async function handleChatbotType(value) {
setChatbotType(value);
setLoadingChatbotType(true);
await update({
key: "chatBotType",
value,
});
toast.success("Operação atualizada com sucesso.");
setLoadingChatbotType(false);
}
async function handleGroupType(value) {
setCheckMsgIsGroupType(value);
setCheckMsgIsGroup(true);
await update({
key: "CheckMsgIsGroup",
value,
});
toast.success("Operação atualizada com sucesso.");
setCheckMsgIsGroupType(false);
/* if (typeof scheduleTypeChanged === "function") {
scheduleTypeChanged(value);
} */
}
{/*NOVO CÓDIGO*/}
async function handleSendGreetingAccepted(value) {
setSendGreetingAccepted(value);
setLoadingSendGreetingAccepted(true);
await update({
key: "sendGreetingAccepted",
value,
});
toast.success("Operação atualizada com sucesso.");
setLoadingSendGreetingAccepted(false);
}
{/*NOVO CÓDIGO*/}
async function handleSettingsTransfTicket(value) {
setSettingsTransfTicket(value);
setLoadingSettingsTransfTicket(true);
await update({
key: "sendMsgTransfTicket",
value,
});
toast.success("Operação atualizada com sucesso.");
setLoadingSettingsTransfTicket(false);
}
{/*async function handleChangeIPIxc(value) {
setIpIxcType(value);
setLoadingIpIxcType(true);
await update({
key: "ipixc",
value,
});
toast.success("Operação atualizada com sucesso.");
setLoadingIpIxcType(false);
}
{/*async function handleChangeTokenIxc(value) {
setTokenIxcType(value);
setLoadingTokenIxcType(true);
await update({
key: "tokenixc",
value,
});
toast.success("Operação atualizada com sucesso.");
setLoadingTokenIxcType(false);
}
async function handleChangeIpMkauth(value) {
setIpMkauthType(value);
setLoadingIpMkauthType(true);
await update({
key: "ipmkauth",
value,
});
toast.success("Operação atualizada com sucesso.");
setLoadingIpMkauthType(false);
}
async function handleChangeClientIdMkauth(value) {
setClientIdMkauthType(value);
setLoadingClientIdMkauthType(true);
await update({
key: "clientidmkauth",
value,
});
toast.success("Operação atualizada com sucesso.");
setLoadingClientIdMkauthType(false);
}
async function handleChangeClientSecrectMkauth(value) {
setClientSecrectMkauthType(value);
setLoadingClientSecrectMkauthType(true);
await update({
key: "clientsecretmkauth",
value,
});
toast.success("Operação atualizada com sucesso.");
setLoadingClientSecrectMkauthType(false);
}*/}
async function handleChangeAsaas(value) {
setAsaasType(value);
setLoadingAsaasType(true);
await update({
key: "asaas",
value,
});
toast.success("Operação atualizada com sucesso.");
setLoadingAsaasType(false);
}
return (
<>
<Grid spacing={3} container>
{/* <Grid xs={12} item>
<Title>Configurações Gerais</Title>
</Grid> */}
<Grid xs={12} sm={6} md={4} item>
<FormControl className={classes.selectContainer}>
<InputLabel id="ratings-label">Avaliações</InputLabel>
<Select
labelId="ratings-label"
value={userRating}
onChange={async (e) => {
handleChangeUserRating(e.target.value);
}}
>
<MenuItem value={"disabled"}>Desabilitadas</MenuItem>
<MenuItem value={"enabled"}>Habilitadas</MenuItem>
</Select>
<FormHelperText>
{loadingUserRating && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
<Grid xs={12} sm={6} md={4} item>
<FormControl className={classes.selectContainer}>
<InputLabel id="schedule-type-label">
Gerenciamento de Expediente
</InputLabel>
<Select
labelId="schedule-type-label"
value={scheduleType}
onChange={async (e) => {
handleScheduleType(e.target.value);
}}
>
<MenuItem value={"disabled"}>Desabilitado</MenuItem>
<MenuItem value={"queue"}>Fila</MenuItem>
<MenuItem value={"company"}>Empresa</MenuItem>
</Select>
<FormHelperText>
{loadingScheduleType && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
<Grid xs={12} sm={6} md={4} item>
<FormControl className={classes.selectContainer}>
<InputLabel id="group-type-label">
Ignorar Mensagens de Grupos
</InputLabel>
<Select
labelId="group-type-label"
value={CheckMsgIsGroup}
onChange={async (e) => {
handleGroupType(e.target.value);
}}
>
<MenuItem value={"disabled"}>Desativado</MenuItem>
<MenuItem value={"enabled"}>Ativado</MenuItem>
</Select>
<FormHelperText>
{loadingScheduleType && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
<Grid xs={12} sm={6} md={4} item>
<FormControl className={classes.selectContainer}>
<InputLabel id="call-type-label">
Aceitar Chamada
</InputLabel>
<Select
labelId="call-type-label"
value={callType}
onChange={async (e) => {
handleCallType(e.target.value);
}}
>
<MenuItem value={"disabled"}>Não Aceitar</MenuItem>
<MenuItem value={"enabled"}>Aceitar</MenuItem>
</Select>
<FormHelperText>
{loadingCallType && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
<Grid xs={12} sm={6} md={4} item>
<FormControl className={classes.selectContainer}>
<InputLabel id="chatbot-type-label">
Tipo Chatbot
</InputLabel>
<Select
labelId="chatbot-type-label"
value={chatbotType}
onChange={async (e) => {
handleChatbotType(e.target.value);
}}
>
<MenuItem value={"text"}>Texto</MenuItem>
{/*<MenuItem value={"button"}>Botão</MenuItem>*/}
{/*<MenuItem value={"list"}>Lista</MenuItem>*/}
</Select>
<FormHelperText>
{loadingChatbotType && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
{/* ENVIAR SAUDAÇÃO AO ACEITAR O TICKET */}
<Grid xs={12} sm={6} md={4} item>
<FormControl className={classes.selectContainer}>
<InputLabel id="sendGreetingAccepted-label">Enviar saudação ao aceitar o ticket</InputLabel>
<Select
labelId="sendGreetingAccepted-label"
value={SendGreetingAccepted}
onChange={async (e) => {
handleSendGreetingAccepted(e.target.value);
}}
>
<MenuItem value={"disabled"}>Desabilitado</MenuItem>
<MenuItem value={"enabled"}>Habilitado</MenuItem>
</Select>
<FormHelperText>
{loadingSendGreetingAccepted && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
{/* ENVIAR SAUDAÇÃO AO ACEITAR O TICKET */}
{/* ENVIAR MENSAGEM DE TRANSFERENCIA DE SETOR/ATENDENTE */}
<Grid xs={12} sm={6} md={4} item>
<FormControl className={classes.selectContainer}>
<InputLabel id="sendMsgTransfTicket-label">Enviar mensagem de transferencia de Fila/agente</InputLabel>
<Select
labelId="sendMsgTransfTicket-label"
value={SettingsTransfTicket}
onChange={async (e) => {
handleSettingsTransfTicket(e.target.value);
}}
>
<MenuItem value={"disabled"}>Desabilitado</MenuItem>
<MenuItem value={"enabled"}>Habilitado</MenuItem>
</Select>
<FormHelperText>
{loadingSettingsTransfTicket && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
{/* ENVIAR SAUDAÇÃO QUANDO HOUVER SOMENTE 1 FILA */}
<Grid xs={12} sm={6} md={4} item>
<FormControl className={classes.selectContainer}>
<InputLabel id="sendGreetingMessageOneQueues-label">Enviar saudação quando houver somente 1 fila</InputLabel>
<Select
labelId="sendGreetingMessageOneQueues-label"
value={sendGreetingMessageOneQueues}
onChange={async (e) => {
handleSendGreetingMessageOneQueues(e.target.value);
}}
>
<MenuItem value={"disabled"}>Desabilitado</MenuItem>
<MenuItem value={"enabled"}>Habilitado</MenuItem>
</Select>
<FormHelperText>
{loadingSendGreetingMessageOneQueues && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
</Grid>
<Grid spacing={3} container>
<Tabs
indicatorColor="primary"
textColor="primary"
scrollButtons="on"
variant="scrollable"
className={classes.tab}
style={{
marginBottom: 20,
marginTop: 20
}}
>
<Tab
label="INTEGRAÇÕES" />
</Tabs>
</Grid>
{/*-----------------IXC DESATIVADO 4.6.5-----------------*/}
{/*<Grid spacing={3} container
style={{ marginBottom: 10 }}>
<Tabs
indicatorColor="primary"
textColor="primary"
scrollButtons="on"
variant="scrollable"
className={classes.tab}
>
<Tab
label="IXC" />
</Tabs>
<Grid xs={12} sm={6} md={6} item>
<FormControl className={classes.selectContainer}>
<TextField
id="ipixc"
name="ipixc"
margin="dense"
label="IP do IXC"
variant="outlined"
value={ipixcType}
onChange={async (e) => {
handleChangeIPIxc(e.target.value);
}}
>
</TextField>
<FormHelperText>
{loadingIpIxcType && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
<Grid xs={12} sm={6} md={6} item>
<FormControl className={classes.selectContainer}>
<TextField
id="tokenixc"
name="tokenixc"
margin="dense"
label="Token do IXC"
variant="outlined"
value={tokenixcType}
onChange={async (e) => {
handleChangeTokenIxc(e.target.value);
}}
>
</TextField>
<FormHelperText>
{loadingTokenIxcType && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
</Grid>*/}
{/*-----------------MK-AUTH DESATIVADO 4.6.5-----------------*/}
{/*<Grid spacing={3} container
style={{ marginBottom: 10 }}>
<Tabs
indicatorColor="primary"
textColor="primary"
scrollButtons="on"
variant="scrollable"
className={classes.tab}
>
<Tab label="MK-AUTH" />
</Tabs>
<Grid xs={12} sm={12} md={4} item>
<FormControl className={classes.selectContainer}>
<TextField
id="ipmkauth"
name="ipmkauth"
margin="dense"
label="Ip Mk-Auth"
variant="outlined"
value={ipmkauthType}
onChange={async (e) => {
handleChangeIpMkauth(e.target.value);
}}
>
</TextField>
<FormHelperText>
{loadingIpMkauthType && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
<Grid xs={12} sm={12} md={4} item>
<FormControl className={classes.selectContainer}>
<TextField
id="clientidmkauth"
name="clientidmkauth"
margin="dense"
label="Client Id"
variant="outlined"
value={clientidmkauthType}
onChange={async (e) => {
handleChangeClientIdMkauth(e.target.value);
}}
>
</TextField>
<FormHelperText>
{loadingClientIdMkauthType && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
<Grid xs={12} sm={12} md={4} item>
<FormControl className={classes.selectContainer}>
<TextField
id="clientsecretmkauth"
name="clientsecretmkauth"
margin="dense"
label="Client Secret"
variant="outlined"
value={clientsecretmkauthType}
onChange={async (e) => {
handleChangeClientSecrectMkauth(e.target.value);
}}
>
</TextField>
<FormHelperText>
{loadingClientSecrectMkauthType && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
</Grid>*/}
{/*-----------------ASAAS-----------------*/}
<Grid spacing={3} container
style={{ marginBottom: 10 }}>
<Tabs
indicatorColor="primary"
textColor="primary"
scrollButtons="on"
variant="scrollable"
className={classes.tab}
>
<Tab label="ASAAS" />
</Tabs>
<Grid xs={12} sm={12} md={12} item>
<FormControl className={classes.selectContainer}>
<TextField
id="asaas"
name="asaas"
margin="dense"
label="Token Asaas"
variant="outlined"
value={asaasType}
onChange={async (e) => {
handleChangeAsaas(e.target.value);
}}
>
</TextField>
<FormHelperText>
{loadingAsaasType && "Atualizando..."}
</FormHelperText>
</FormControl>
</Grid>
</Grid>
</>
);
}
@@ -0,0 +1,70 @@
import React, { useEffect, useRef } from "react";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Dialog from "@material-ui/core/Dialog";
import DialogContent from "@material-ui/core/DialogContent";
import CheckoutPage from "../CheckoutPage/";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
flexWrap: "wrap",
},
textField: {
marginRight: theme.spacing(1),
flex: 1,
},
extraAttr: {
display: "flex",
justifyContent: "center",
alignItems: "center",
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
}));
const ContactModal = ({ open, onClose, Invoice, contactId, initialValues, onSave }) => {
const classes = useStyles();
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
const handleClose = () => {
onClose();
};
return (
<div className={classes.root}>
<Dialog open={open} onClose={handleClose} maxWidth="md" scroll="paper">
<DialogContent dividers>
<CheckoutPage
Invoice={Invoice}
/>
</DialogContent>
</Dialog>
</div>
);
};
export default ContactModal;
@@ -0,0 +1,9 @@
import React from 'react'
function index() {
return (
<div>index</div>
)
}
export default index
+18
View File
@@ -0,0 +1,18 @@
import React from "react";
const TabPanel = ({ children, value, name, ...rest }) => {
if (value === name) {
return (
<div
role="tabpanel"
id={`simple-tabpanel-${name}`}
aria-labelledby={`simple-tab-${name}`}
{...rest}
>
<>{children}</>
</div>
);
} else return null;
};
export default TabPanel;
@@ -0,0 +1,52 @@
import React from "react";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import Skeleton from "@material-ui/lab/Skeleton";
import { makeStyles } from "@material-ui/core";
const useStyles = makeStyles(theme => ({
customTableCell: {
display: "flex",
alignItems: "center",
justifyContent: "center",
},
}));
const TableRowSkeleton = ({ avatar, columns }) => {
const classes = useStyles();
return (
<>
<TableRow>
{avatar && (
<>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton
animation="wave"
variant="circle"
width={40}
height={40}
/>
</TableCell>
<TableCell>
<Skeleton animation="wave" height={30} width={80} />
</TableCell>
</>
)}
{Array.from({ length: columns }, (_, index) => (
<TableCell align="center" key={index}>
<div className={classes.customTableCell}>
<Skeleton
align="center"
animation="wave"
height={30}
width={80}
/>
</div>
</TableCell>
))}
</TableRow>
</>
);
};
export default TableRowSkeleton;
+269
View File
@@ -0,0 +1,269 @@
import React, { useState, useEffect, useContext } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import CircularProgress from "@material-ui/core/CircularProgress";
import { Colorize } from "@material-ui/icons";
import { ColorBox } from 'material-ui-color';
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
import { AuthContext } from "../../context/Auth/AuthContext";
import { IconButton, InputAdornment } from "@material-ui/core";
import { FormControlLabel, Switch } from '@material-ui/core';
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import InputLabel from "@material-ui/core/InputLabel";
import Checkbox from '@material-ui/core/Checkbox';
const useStyles = makeStyles(theme => ({
root: {
display: "flex",
flexWrap: "wrap",
},
multFieldLine: {
display: "flex",
"& > *:not(:last-child)": {
marginRight: theme.spacing(1),
},
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
colorAdorment: {
width: 20,
height: 20,
},
}));
const TagSchema = Yup.object().shape({
name: Yup.string()
.min(3, "Mensagem muito curta")
.required("Obrigatório")
});
const TagModal = ({ open, onClose, tagId, reload }) => {
const classes = useStyles();
const { user } = useContext(AuthContext);
const [colorPickerModalOpen, setColorPickerModalOpen] = useState(false);
const initialState = {
name: "",
color: "",
kanban: 0
};
const [tag, setTag] = useState(initialState);
const [ kanban, setKanban] = useState(0);
useEffect(() => {
try {
(async () => {
if (!tagId) return;
const { data } = await api.get(`/tags/${tagId}`);
setKanban(data.kanban);
setTag(prevState => {
return { ...prevState, ...data };
});
})()
} catch (err) {
toastError(err);
}
}, [tagId, open]);
const handleClose = () => {
setTag(initialState);
setColorPickerModalOpen(false);
onClose();
};
const handleKanbanChange = (e) => {
setKanban( e.target.checked ? 1 : 0);
};
const handleSaveTag = async values => {
const tagData = { ...values, userId: user.id, kanban };
try {
if (tagId) {
await api.put(`/tags/${tagId}`, tagData);
} else {
await api.post("/tags", tagData);
}
toast.success(i18n.t("tagModal.success"));
if (typeof reload == 'function') {
reload();
}
} catch (err) {
toastError(err);
}
handleClose();
};
return (
<div className={classes.root}>
<Dialog
open={open}
onClose={handleClose}
maxWidth="xs"
fullWidth
scroll="paper"
>
<DialogTitle id="form-dialog-title">
{(tagId ? `${i18n.t("tagModal.title.edit")}` : `${i18n.t("tagModal.title.add")}`)}
</DialogTitle>
<Formik
initialValues={tag}
enableReinitialize={true}
validationSchema={TagSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveTag(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting, values }) => (
<Form>
<DialogContent dividers>
<div className={classes.multFieldLine}>
<Field
as={TextField}
label={i18n.t("tagModal.form.name")}
name="name"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
onChange={(e) => setTag(prev => ({ ...prev, name: e.target.value }))}
fullWidth
/>
</div>
<br />
<div className={classes.multFieldLine}>
<Field
as={TextField}
fullWidth
label={i18n.t("tagModal.form.color")}
name="color"
id="color"
error={touched.color && Boolean(errors.color)}
helperText={touched.color && errors.color}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<div
style={{ backgroundColor: values.color }}
className={classes.colorAdorment}
></div>
</InputAdornment>
),
endAdornment: (
<IconButton
size="small"
color="default"
onClick={() => setColorPickerModalOpen(!colorPickerModalOpen)}
>
<Colorize />
</IconButton>
),
}}
variant="outlined"
margin="dense"
/>
</div>
{(user.profile === "admin" || user.profile === "supervisor") && (
<>
<div className={classes.multFieldLine}>
<FormControlLabel
control={
<Checkbox
checked={kanban === 1}
onChange={handleKanbanChange}
value={kanban}
color="primary"
/>
}
label="Kanban"
labelPlacement="start"
/>
</div>
<br />
</>
)}
{colorPickerModalOpen && (
<div>
<ColorBox
disableAlpha={true}
hslGradient={false}
style={{ margin: '20px auto 0' }}
value={tag.color}
onChange={val => {
setTag(prev => ({ ...prev, color: `#${val.hex}` }));
}}
/>
</div>
)}
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("tagModal.buttons.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{tagId
? `${i18n.t("tagModal.buttons.okEdit")}`
: `${i18n.t("tagModal.buttons.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default TagModal;
@@ -0,0 +1,239 @@
import React, { useState, useEffect, useContext } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import CircularProgress from "@material-ui/core/CircularProgress";
import { Colorize } from "@material-ui/icons";
import { ColorBox } from 'material-ui-color';
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
import { AuthContext } from "../../context/Auth/AuthContext";
import { IconButton, InputAdornment } from "@material-ui/core";
const useStyles = makeStyles(theme => ({
root: {
display: "flex",
flexWrap: "wrap",
},
multFieldLine: {
display: "flex",
"& > *:not(:last-child)": {
marginRight: theme.spacing(1),
},
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
colorAdorment: {
width: 20,
height: 20,
},
}));
const TagSchema = Yup.object().shape({
name: Yup.string()
.min(3, "Mensagem muito curta")
.required("Obrigatório")
});
const TagModal = ({ open, onClose, tagId, reload }) => {
const classes = useStyles();
const { user } = useContext(AuthContext);
const [colorPickerModalOpen, setColorPickerModalOpen] = useState(false);
const initialState = {
name: "",
color: ""
};
const [tag, setTag] = useState(initialState);
useEffect(() => {
try {
(async () => {
if (!tagId) return;
const { data } = await api.get(`/tags/${tagId}`);
setTag(prevState => {
return { ...prevState, ...data };
});
})()
} catch (err) {
toastError(err);
}
}, [tagId, open]);
const handleClose = () => {
setTag(initialState);
setColorPickerModalOpen(false);
onClose();
};
const handleSaveTag = async values => {
const tagData = { ...values, userId: user.id };
try {
if (tagId) {
await api.put(`/tags/${tagId}`, tagData);
} else {
await api.post("/tags", tagData);
}
toast.success(i18n.t("tagModal.success"));
if (typeof reload == 'function') {
reload();
}
} catch (err) {
toastError(err);
}
handleClose();
};
return (
<div className={classes.root}>
<Dialog
open={open}
onClose={handleClose}
maxWidth="xs"
fullWidth
scroll="paper"
>
<DialogTitle id="form-dialog-title">
{ (tagId ? `${i18n.t("tagModal.title.edit")}` : `${i18n.t("tagModal.title.add")}`) }
</DialogTitle>
<Formik
initialValues={tag}
enableReinitialize={true}
validationSchema={TagSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
handleSaveTag(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting, values }) => (
<Form>
<DialogContent dividers>
<div className={classes.multFieldLine}>
<Field
as={TextField}
label={i18n.t("tagModal.form.name")}
name="name"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
onChange={(e) => setTag(prev => ({ ...prev, name: e.target.value }))}
fullWidth
/>
</div>
<br />
<div className={classes.multFieldLine}>
<Field
as={TextField}
fullWidth
label={i18n.t("tagModal.form.color")}
name="color"
id="color"
error={touched.color && Boolean(errors.color)}
helperText={touched.color && errors.color}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<div
style={{ backgroundColor: values.color }}
className={classes.colorAdorment}
></div>
</InputAdornment>
),
endAdornment: (
<IconButton
size="small"
color="default"
onClick={() => setColorPickerModalOpen(!colorPickerModalOpen)}
>
<Colorize />
</IconButton>
),
}}
variant="outlined"
margin="dense"
/>
</div>
{ colorPickerModalOpen && (
<div>
<ColorBox
disableAlpha={true}
hslGradient={false}
style={{margin: '20px auto 0'}}
value={tag.color}
onChange={val => {
setTag(prev => ({ ...prev, color: `#${val.hex}` }));
}}
/>
</div>
)}
</DialogContent>
<DialogActions>
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("tagModal.buttons.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{tagId
? `${i18n.t("tagModal.buttons.okEdit")}`
: `${i18n.t("tagModal.buttons.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default TagModal;
@@ -0,0 +1,120 @@
import { Chip, Paper, TextField } from "@material-ui/core";
import Autocomplete from "@material-ui/lab/Autocomplete";
import React, { useEffect, useRef, useState } from "react";
import { isArray, isString } from "lodash";
import toastError from "../../errors/toastError";
import api from "../../services/api";
export function TagsContainer({ ticket }) {
const [tags, setTags] = useState([]);
const [selecteds, setSelecteds] = useState([]);
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false
}
}, [])
useEffect(() => {
if (isMounted.current) {
loadTags().then(() => {
if (Array.isArray(ticket.tags)) {
setSelecteds(ticket.tags);
} else {
setSelecteds([]);
}
});
}
}, [ticket]);
const createTag = async (data) => {
try {
const { data: responseData } = await api.post(`/tags`, data);
return responseData;
} catch (err) {
toastError(err);
}
}
const loadTags = async () => {
try {
const { data } = await api.get(`/tags/list`);
setTags(data);
} catch (err) {
toastError(err);
}
}
const syncTags = async (data) => {
try {
const { data: responseData } = await api.post(`/tags/sync`, data);
return responseData;
} catch (err) {
toastError(err);
}
}
const onChange = async (value, reason) => {
let optionsChanged = []
if (reason === 'create-option') {
if (isArray(value)) {
for (let item of value) {
if (isString(item)) {
const newTag = await createTag({ name: item })
optionsChanged.push(newTag);
} else {
optionsChanged.push(item);
}
}
}
await loadTags();
} else {
optionsChanged = value;
}
setSelecteds(optionsChanged);
await syncTags({ ticketId: ticket.id, tags: optionsChanged });
}
return (
<Paper style={{ padding: 12 }}>
<Autocomplete
multiple
size="small"
options={tags}
value={selecteds}
freeSolo
onChange={(e, v, r) => onChange(v, r)}
getOptionLabel={(option) => option.name}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
variant="outlined"
style={{
background: option.color || '#eee',
color: "#FFF",
marginRight: 1,
fontWeight: 600,
borderRadius: 3,
fontSize: "0.8em",
whiteSpace: "nowrap"
}}
label={option.name.toUpperCase()}
{...getTagProps({ index })}
size="small"
/>
))
}
renderInput={(params) => (
<TextField {...params} variant="outlined" placeholder="Tags" />
)}
PaperComponent={({ children }) => (
<Paper style={{ width: 400, marginLeft: 12 }}>
{children}
</Paper>
)}
/>
</Paper>
)
}
@@ -0,0 +1,66 @@
import { Box, Chip, TextField } from "@material-ui/core";
import Autocomplete from "@material-ui/lab/Autocomplete";
import React, { useEffect, useState } from "react";
import toastError from "../../errors/toastError";
import api from "../../services/api";
export function TagsFilter({ onFiltered }) {
const [tags, setTags] = useState([]);
const [selecteds, setSelecteds] = useState([]);
useEffect(() => {
async function fetchData() {
await loadTags();
}
fetchData();
}, []);
const loadTags = async () => {
try {
const { data } = await api.get(`/tags/list`);
setTags(data);
} catch (err) {
toastError(err);
}
};
const onChange = async (value) => {
setSelecteds(value);
onFiltered(value);
};
return (
<Box style={{ padding: 10 }}>
<Autocomplete
multiple
size="small"
options={tags}
value={selecteds}
onChange={(e, v, r) => onChange(v)}
getOptionLabel={(option) => option.name}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
variant="outlined"
style={{
backgroundColor: option.color || "#eee",
textShadow: "1px 1px 1px #000",
color: "white",
}}
label={option.name}
{...getTagProps({ index })}
size="small"
/>
))
}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
placeholder="Filtro por Tags"
/>
)}
/>
</Box>
);
}
+197
View File
@@ -0,0 +1,197 @@
import React, { useState, useEffect, useContext } from "react";
import { useParams, useHistory } from "react-router-dom";
import { toast } from "react-toastify";
import clsx from "clsx";
import { Paper, makeStyles } from "@material-ui/core";
import ContactDrawer from "../ContactDrawer";
import MessageInput from "../MessageInputCustom/";
import TicketHeader from "../TicketHeader";
import TicketInfo from "../TicketInfo";
import TicketActionButtons from "../TicketActionButtonsCustom";
import MessagesList from "../MessagesList";
import api from "../../services/api";
import { ReplyMessageProvider } from "../../context/ReplyingMessage/ReplyingMessageContext";
import toastError from "../../errors/toastError";
import { AuthContext } from "../../context/Auth/AuthContext";
import { TagsContainer } from "../TagsContainer";
import { SocketContext } from "../../context/Socket/SocketContext";
const drawerWidth = 320;
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
height: "100%",
position: "relative",
overflow: "hidden",
},
mainWrapper: {
flex: 1,
height: "100%",
display: "flex",
flexDirection: "column",
overflow: "hidden",
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderLeft: "0",
marginRight: -drawerWidth,
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
mainWrapperShift: {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
marginRight: 0,
},
}));
const Ticket = () => {
const { ticketId } = useParams();
const history = useHistory();
const classes = useStyles();
const { user } = useContext(AuthContext);
const [drawerOpen, setDrawerOpen] = useState(false);
const [loading, setLoading] = useState(true);
const [contact, setContact] = useState({});
const [ticket, setTicket] = useState({});
const socketManager = useContext(SocketContext);
useEffect(() => {
setLoading(true);
const delayDebounceFn = setTimeout(() => {
const fetchTicket = async () => {
try {
const { data } = await api.get("/tickets/u/" + ticketId);
const { queueId } = data;
const { queues, profile } = user;
const queueAllowed = queues.find((q) => q.id === queueId);
if (queueAllowed === undefined && profile !== "admin") {
toast.error("Acesso não permitido");
history.push("/tickets");
return;
}
setContact(data.contact);
setTicket(data);
setLoading(false);
} catch (err) {
setLoading(false);
toastError(err);
}
};
fetchTicket();
}, 500);
return () => clearTimeout(delayDebounceFn);
}, [ticketId, user, history]);
useEffect(() => {
const companyId = localStorage.getItem("companyId");
const socket = socketManager.getSocket(companyId);
socket.on("ready", () => socket.emit("joinChatBox", `${ticket.id}`));
socket.on(`company-${companyId}-ticket`, (data) => {
if (data.action === "update" && data.ticket.id === ticket.id) {
setTicket(data.ticket);
}
if (data.action === "delete" && data.ticketId === ticket.id) {
// toast.success("Ticket deleted sucessfully.");
history.push("/tickets");
}
});
socket.on(`company-${companyId}-contact`, (data) => {
if (data.action === "update") {
setContact((prevState) => {
if (prevState.id === data.contact?.id) {
return { ...prevState, ...data.contact };
}
return prevState;
});
}
});
return () => {
socket.disconnect();
};
}, [ticketId, ticket, history, socketManager]);
const handleDrawerOpen = () => {
setDrawerOpen(true);
};
const handleDrawerClose = () => {
setDrawerOpen(false);
};
const renderTicketInfo = () => {
if (ticket.user !== undefined) {
return (
<TicketInfo
contact={contact}
ticket={ticket}
onClick={handleDrawerOpen}
/>
);
}
};
const renderMessagesList = () => {
return (
<>
<MessagesList
ticket={ticket}
ticketId={ticket.id}
isGroup={ticket.isGroup}
></MessagesList>
<MessageInput ticketId={ticket.id} ticketStatus={ticket.status} />
</>
);
};
return (
<div className={classes.root} id="drawer-container">
<Paper
variant="outlined"
elevation={0}
className={clsx(classes.mainWrapper, {
[classes.mainWrapperShift]: drawerOpen,
})}
>
<TicketHeader loading={loading}>
{renderTicketInfo()}
<TicketActionButtons ticket={ticket} />
</TicketHeader>
<Paper>
<TagsContainer ticket={ticket} />
</Paper>
<ReplyMessageProvider>{renderMessagesList()}</ReplyMessageProvider>
</Paper>
<ContactDrawer
open={drawerOpen}
handleDrawerClose={handleDrawerClose}
contact={contact}
loading={loading}
ticket={ticket}
/>
</div>
);
};
export default Ticket;
@@ -0,0 +1,121 @@
import React, { useContext, useState } from "react";
import { useHistory } from "react-router-dom";
import { makeStyles } from "@material-ui/core/styles";
import { IconButton } from "@material-ui/core";
import { MoreVert, Replay } from "@material-ui/icons";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import TicketOptionsMenu from "../TicketOptionsMenu";
import ButtonWithSpinner from "../ButtonWithSpinner";
import toastError from "../../errors/toastError";
import { AuthContext } from "../../context/Auth/AuthContext";
const useStyles = makeStyles(theme => ({
actionButtons: {
marginRight: 6,
flex: "none",
alignSelf: "center",
marginLeft: "auto",
"& > *": {
marginRight: theme.spacing(1),
marginLeft: theme.spacing(1),
},
},
}));
const TicketActionButtons = ({ ticket }) => {
const classes = useStyles();
const history = useHistory();
const [anchorEl, setAnchorEl] = useState(null);
const [loading, setLoading] = useState(false);
const ticketOptionsMenuOpen = Boolean(anchorEl);
const { user } = useContext(AuthContext);
const handleOpenTicketOptionsMenu = e => {
setAnchorEl(e.currentTarget);
};
const handleCloseTicketOptionsMenu = e => {
setAnchorEl(null);
};
const handleUpdateTicketStatus = async (e, status, userId) => {
setLoading(true);
try {
await api.put(`/tickets/${ticket.id}`, {
status: status,
userId: userId || null,
});
setLoading(false);
if (status === "open") {
history.push(`/tickets/${ticket.id}`);
} else {
history.push("/tickets");
}
} catch (err) {
setLoading(false);
toastError(err);
}
};
return (
<div className={classes.actionButtons}>
{ticket.status === "closed" && (
<ButtonWithSpinner
loading={loading}
startIcon={<Replay />}
size="small"
onClick={e => handleUpdateTicketStatus(e, "open", user?.id)}
>
{i18n.t("messagesList.header.buttons.reopen")}
</ButtonWithSpinner>
)}
{ticket.status === "open" && (
<>
<ButtonWithSpinner
loading={loading}
startIcon={<Replay />}
size="small"
onClick={e => handleUpdateTicketStatus(e, "pending", null)}
>
{i18n.t("messagesList.header.buttons.return")}
</ButtonWithSpinner>
<ButtonWithSpinner
loading={loading}
size="small"
variant="contained"
color="primary"
onClick={e => handleUpdateTicketStatus(e, "closed", user?.id)}
>
{i18n.t("messagesList.header.buttons.resolve")}
</ButtonWithSpinner>
<IconButton onClick={handleOpenTicketOptionsMenu}>
<MoreVert />
</IconButton>
<TicketOptionsMenu
ticket={ticket}
anchorEl={anchorEl}
menuOpen={ticketOptionsMenuOpen}
handleClose={handleCloseTicketOptionsMenu}
/>
</>
)}
{ticket.status === "pending" && (
<ButtonWithSpinner
loading={loading}
size="small"
variant="contained"
color="primary"
onClick={e => handleUpdateTicketStatus(e, "open", user?.id)}
>
{i18n.t("messagesList.header.buttons.accept")}
</ButtonWithSpinner>
)}
</div>
);
};
export default TicketActionButtons;
@@ -0,0 +1,149 @@
import React, { useContext, useState } from "react";
import { useHistory } from "react-router-dom";
import { makeStyles, createTheme, ThemeProvider } from "@material-ui/core/styles";
import { IconButton } from "@material-ui/core";
import { MoreVert, Replay } from "@material-ui/icons";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import TicketOptionsMenu from "../TicketOptionsMenu";
import ButtonWithSpinner from "../ButtonWithSpinner";
import toastError from "../../errors/toastError";
import { AuthContext } from "../../context/Auth/AuthContext";
import { TicketsContext } from "../../context/Tickets/TicketsContext";
import CheckCircleIcon from '@material-ui/icons/CheckCircle';
import UndoRoundedIcon from '@material-ui/icons/UndoRounded';
import Tooltip from '@material-ui/core/Tooltip';
import { green } from '@material-ui/core/colors';
const useStyles = makeStyles(theme => ({
actionButtons: {
marginRight: 6,
flex: "none",
alignSelf: "center",
marginLeft: "auto",
"& > *": {
margin: theme.spacing(0.5),
},
},
}));
const TicketActionButtonsCustom = ({ ticket }) => {
const classes = useStyles();
const history = useHistory();
const [anchorEl, setAnchorEl] = useState(null);
const [loading, setLoading] = useState(false);
const ticketOptionsMenuOpen = Boolean(anchorEl);
const { user } = useContext(AuthContext);
const { setCurrentTicket } = useContext(TicketsContext);
const customTheme = createTheme({
palette: {
primary: green,
}
});
const handleOpenTicketOptionsMenu = e => {
setAnchorEl(e.currentTarget);
};
const handleCloseTicketOptionsMenu = e => {
setAnchorEl(null);
};
const handleUpdateTicketStatus = async (e, status, userId) => {
setLoading(true);
try {
await api.put(`/tickets/${ticket.id}`, {
status: status,
userId: userId || null,
useIntegration: status === "closed" ? false : ticket.useIntegration,
promptId: status === "closed" ? false : ticket.promptId,
integrationId: status === "closed" ? false : ticket.integrationId
});
setLoading(false);
if (status === "open") {
setCurrentTicket({ ...ticket, code: "#open" });
} else {
setCurrentTicket({ id: null, code: null })
history.push("/tickets");
}
} catch (err) {
setLoading(false);
toastError(err);
}
};
return (
<div className={classes.actionButtons}>
{ticket.status === "closed" && (
<ButtonWithSpinner
loading={loading}
startIcon={<Replay />}
size="small"
onClick={e => handleUpdateTicketStatus(e, "open", user?.id)}
>
{i18n.t("messagesList.header.buttons.reopen")}
</ButtonWithSpinner>
)}
{ticket.status === "open" && (
<>
<Tooltip title={i18n.t("messagesList.header.buttons.return")}>
<IconButton onClick={e => handleUpdateTicketStatus(e, "pending", null)}>
<UndoRoundedIcon />
</IconButton>
</Tooltip>
<ThemeProvider theme={customTheme}>
<Tooltip title={i18n.t("messagesList.header.buttons.resolve")}>
<IconButton onClick={e => handleUpdateTicketStatus(e, "closed", user?.id)} color="primary">
<CheckCircleIcon />
</IconButton>
</Tooltip>
</ThemeProvider>
{/* <ButtonWithSpinner
loading={loading}
startIcon={<Replay />}
size="small"
onClick={e => handleUpdateTicketStatus(e, "pending", null)}
>
{i18n.t("messagesList.header.buttons.return")}
</ButtonWithSpinner>
<ButtonWithSpinner
loading={loading}
size="small"
variant="contained"
color="primary"
onClick={e => handleUpdateTicketStatus(e, "closed", user?.id)}
>
{i18n.t("messagesList.header.buttons.resolve")}
</ButtonWithSpinner> */}
<IconButton onClick={handleOpenTicketOptionsMenu}>
<MoreVert />
</IconButton>
<TicketOptionsMenu
ticket={ticket}
anchorEl={anchorEl}
menuOpen={ticketOptionsMenuOpen}
handleClose={handleCloseTicketOptionsMenu}
/>
</>
)}
{ticket.status === "pending" && (
<ButtonWithSpinner
loading={loading}
size="small"
variant="contained"
color="primary"
onClick={e => handleUpdateTicketStatus(e, "open", user?.id)}
>
{i18n.t("messagesList.header.buttons.accept")}
</ButtonWithSpinner>
)}
</div>
);
};
export default TicketActionButtonsCustom;
@@ -0,0 +1,10 @@
import { styled } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
const TicketAdvancedLayout = styled(Paper)({
height: `calc(100% - 48px)`,
display: "grid",
gridTemplateRows: "56px 1fr"
})
export default TicketAdvancedLayout;
@@ -0,0 +1,36 @@
import React from "react";
import { Card } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import TicketHeaderSkeleton from "../TicketHeaderSkeleton";
const useStyles = makeStyles(theme => ({
ticketHeader: {
display: "flex",
backgroundColor: theme.palette.tabHeaderBackground,
flex: "none",
borderBottom: "1px solid rgba(0, 0, 0, 0.12)",
[theme.breakpoints.down("sm")]: {
flexWrap: "wrap"
}
},
}));
const TicketHeader = ({ loading, children }) => {
const classes = useStyles();
return (
<>
{loading ? (
<TicketHeaderSkeleton />
) : (
<Card square className={classes.ticketHeader}>
{children}
</Card>
)}
</>
);
};
export default TicketHeader;
@@ -0,0 +1,36 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import { Avatar, Card, CardHeader } from "@material-ui/core";
import Skeleton from "@material-ui/lab/Skeleton";
const useStyles = makeStyles(theme => ({
ticketHeader: {
display: "flex",
backgroundColor: "#eee",
flex: "none",
borderBottom: "1px solid rgba(0, 0, 0, 0.12)",
},
}));
const TicketHeaderSkeleton = () => {
const classes = useStyles();
return (
<Card square className={classes.ticketHeader}>
<CardHeader
titleTypographyProps={{ noWrap: true }}
subheaderTypographyProps={{ noWrap: true }}
avatar={
<Skeleton animation="wave" variant="circle">
<Avatar alt="contact_image" />
</Skeleton>
}
title={<Skeleton animation="wave" width={80} />}
subheader={<Skeleton animation="wave" width={140} />}
/>
</Card>
);
};
export default TicketHeaderSkeleton;
@@ -0,0 +1,46 @@
import React, { useState, useEffect } from "react";
import { Avatar, CardHeader } from "@material-ui/core";
import { i18n } from "../../translate/i18n";
const TicketInfo = ({ contact, ticket, onClick }) => {
const { user } = ticket
const [userName, setUserName] = useState('')
const [contactName, setContactName] = useState('')
useEffect(() => {
if (contact) {
setContactName(contact.name);
if(document.body.offsetWidth < 600) {
if (contact.name.length > 10) {
const truncadName = contact.name.substring(0, 10) + '...';
setContactName(truncadName);
}
}
}
if (user && contact) {
setUserName(`${i18n.t("messagesList.header.assignedTo")} ${user.name}`);
if(document.body.offsetWidth < 600) {
setUserName(`${user.name}`);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<CardHeader
onClick={onClick}
style={{ cursor: "pointer" }}
titleTypographyProps={{ noWrap: true }}
subheaderTypographyProps={{ noWrap: true }}
avatar={<Avatar src={contact.profilePicUrl} alt="contact_image" />}
title={`${contactName} #${ticket.id}`}
subheader={ticket.user && `${userName}`}
/>
);
};
export default TicketInfo;

Some files were not shown because too many files have changed in this diff Show More