Initial commit
This commit is contained in:
@@ -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;
|
||||
@@ -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
|
||||
@@ -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} />
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
<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;
|
||||
@@ -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} <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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user