index.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. import React, { useState, useEffect, useRef, useContext } from "react";
  2. import * as Yup from "yup";
  3. import { Formik, Form, Field } from "formik";
  4. import { toast } from "react-toastify";
  5. import { head } from "lodash";
  6. import { makeStyles } from "@material-ui/core/styles";
  7. import { green } from "@material-ui/core/colors";
  8. import Button from "@material-ui/core/Button";
  9. import IconButton from "@material-ui/core/IconButton";
  10. import TextField from "@material-ui/core/TextField";
  11. import Dialog from "@material-ui/core/Dialog";
  12. import DialogActions from "@material-ui/core/DialogActions";
  13. import DialogContent from "@material-ui/core/DialogContent";
  14. import DialogTitle from "@material-ui/core/DialogTitle";
  15. import CircularProgress from "@material-ui/core/CircularProgress";
  16. import AttachFileIcon from "@material-ui/icons/AttachFile";
  17. import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
  18. import { i18n } from "../../translate/i18n";
  19. import moment from "moment";
  20. import api from "../../services/api";
  21. import toastError from "../../errors/toastError";
  22. import {
  23. Box,
  24. FormControl,
  25. Grid,
  26. InputLabel,
  27. MenuItem,
  28. Select,
  29. Tab,
  30. Tabs,
  31. } from "@material-ui/core";
  32. import { AuthContext } from "../../context/Auth/AuthContext";
  33. import ConfirmationModal from "../ConfirmationModal";
  34. const useStyles = makeStyles((theme) => ({
  35. root: {
  36. display: "flex",
  37. flexWrap: "wrap",
  38. backgroundColor: "#fff"
  39. },
  40. tabmsg: {
  41. backgroundColor: theme.palette.campaigntab,
  42. },
  43. textField: {
  44. marginRight: theme.spacing(1),
  45. flex: 1,
  46. },
  47. extraAttr: {
  48. display: "flex",
  49. justifyContent: "center",
  50. alignItems: "center",
  51. },
  52. btnWrapper: {
  53. position: "relative",
  54. },
  55. buttonProgress: {
  56. color: green[500],
  57. position: "absolute",
  58. top: "50%",
  59. left: "50%",
  60. marginTop: -12,
  61. marginLeft: -12,
  62. },
  63. }));
  64. const CampaignSchema = Yup.object().shape({
  65. name: Yup.string()
  66. .min(2, i18n.t("campaigns.dialog.form.nameShort"))
  67. .max(50, i18n.t("campaigns.dialog.form.nameLong"))
  68. .required(i18n.t("campaigns.dialog.form.nameRequired")),
  69. });
  70. const CampaignModal = ({
  71. open,
  72. onClose,
  73. campaignId,
  74. initialValues,
  75. onSave,
  76. resetPagination,
  77. }) => {
  78. const classes = useStyles();
  79. const isMounted = useRef(true);
  80. const { user } = useContext(AuthContext);
  81. const { companyId } = user;
  82. const [file, setFile] = useState(null);
  83. const initialState = {
  84. name: "",
  85. message1: "",
  86. message2: "",
  87. message3: "",
  88. message4: "",
  89. message5: "",
  90. status: "INATIVA", // INATIVA, PROGRAMADA, EM_ANDAMENTO, CANCELADA, FINALIZADA,
  91. scheduledAt: "",
  92. whatsappId: "",
  93. contactListId: "",
  94. tagListId: "Nenhuma",
  95. companyId,
  96. };
  97. const [campaign, setCampaign] = useState(initialState);
  98. const [whatsapps, setWhatsapps] = useState([]);
  99. const [contactLists, setContactLists] = useState([]);
  100. const [messageTab, setMessageTab] = useState(0);
  101. const [attachment, setAttachment] = useState(null);
  102. const [confirmationOpen, setConfirmationOpen] = useState(false);
  103. const [campaignEditable, setCampaignEditable] = useState(true);
  104. const attachmentFile = useRef(null);
  105. const [tagLists, setTagLists] = useState([]);
  106. useEffect(() => {
  107. return () => {
  108. isMounted.current = false;
  109. };
  110. }, []);
  111. useEffect(() => {
  112. (async () => {
  113. try {
  114. const { data } = await api.get("/files/", {
  115. params: { companyId }
  116. });
  117. setFile(data.files);
  118. } catch (err) {
  119. toastError(err);
  120. }
  121. })();
  122. }, []);
  123. useEffect(() => {
  124. if (isMounted.current) {
  125. if (initialValues) {
  126. setCampaign((prevState) => {
  127. return { ...prevState, ...initialValues };
  128. });
  129. }
  130. api
  131. .get(`/contact-lists/list`, { params: { companyId } })
  132. .then(({ data }) => setContactLists(data));
  133. api
  134. .get(`/whatsapp`, { params: { companyId, session: 0 } })
  135. .then(({ data }) => setWhatsapps(data));
  136. api.get(`/tags`, { params: { companyId } })
  137. .then(({ data }) => {
  138. const fetchedTags = data.tags;
  139. // Perform any necessary data transformation here
  140. const formattedTagLists = fetchedTags.map((tag) => ({
  141. id: tag.id,
  142. name: tag.name,
  143. }));
  144. setTagLists(formattedTagLists);
  145. })
  146. .catch((error) => {
  147. console.error("Error retrieving tags:", error);
  148. });
  149. if (!campaignId) return;
  150. api.get(`/campaigns/${campaignId}`).then(({ data }) => {
  151. setCampaign((prev) => {
  152. let prevCampaignData = Object.assign({}, prev);
  153. Object.entries(data).forEach(([key, value]) => {
  154. if (key === "scheduledAt" && value !== "" && value !== null) {
  155. prevCampaignData[key] = moment(value).format("YYYY-MM-DDTHH:mm");
  156. } else {
  157. prevCampaignData[key] = value === null ? "" : value;
  158. }
  159. });
  160. return {...prevCampaignData, tagListId: data.tagId || "Nenhuma"};
  161. });
  162. });
  163. }
  164. }, [campaignId, open, initialValues, companyId]);
  165. useEffect(() => {
  166. const now = moment();
  167. const scheduledAt = moment(campaign.scheduledAt);
  168. const moreThenAnHour =
  169. !Number.isNaN(scheduledAt.diff(now)) && scheduledAt.diff(now, "hour") > 1;
  170. const isEditable =
  171. campaign.status === "INATIVA" ||
  172. (campaign.status === "PROGRAMADA" && moreThenAnHour);
  173. setCampaignEditable(isEditable);
  174. }, [campaign.status, campaign.scheduledAt]);
  175. const handleClose = () => {
  176. onClose();
  177. setCampaign(initialState);
  178. };
  179. const handleAttachmentFile = (e) => {
  180. const file = head(e.target.files);
  181. if (file) {
  182. setAttachment(file);
  183. }
  184. };
  185. const handleSaveCampaign = async (values) => {
  186. try {
  187. const dataValues = {};
  188. Object.entries(values).forEach(([key, value]) => {
  189. if (key === "scheduledAt" && value !== "" && value !== null) {
  190. dataValues[key] = moment(value).format("YYYY-MM-DD HH:mm:ss");
  191. } else {
  192. dataValues[key] = value === "" ? null : value;
  193. }
  194. });
  195. if (campaignId) {
  196. await api.put(`/campaigns/${campaignId}`, dataValues);
  197. if (attachment != null) {
  198. const formData = new FormData();
  199. formData.append("file", attachment);
  200. await api.post(`/campaigns/${campaignId}/media-upload`, formData);
  201. }
  202. handleClose();
  203. } else {
  204. const { data } = await api.post("/campaigns", dataValues);
  205. if (attachment != null) {
  206. const formData = new FormData();
  207. formData.append("file", attachment);
  208. await api.post(`/campaigns/${data.id}/media-upload`, formData);
  209. }
  210. if (onSave) {
  211. onSave(data);
  212. }
  213. handleClose();
  214. }
  215. toast.success(i18n.t("campaigns.toasts.success"));
  216. } catch (err) {
  217. console.log(err);
  218. toastError(err);
  219. }
  220. };
  221. const deleteMedia = async () => {
  222. if (attachment) {
  223. setAttachment(null);
  224. attachmentFile.current.value = null;
  225. }
  226. if (campaign.mediaPath) {
  227. await api.delete(`/campaigns/${campaign.id}/media-upload`);
  228. setCampaign((prev) => ({ ...prev, mediaPath: null, mediaName: null }));
  229. toast.success(i18n.t("campaigns.toasts.deleted"));
  230. }
  231. };
  232. const renderMessageField = (identifier) => {
  233. return (
  234. <Field
  235. as={TextField}
  236. id={identifier}
  237. name={identifier}
  238. fullWidth
  239. rows={5}
  240. label={i18n.t(`campaigns.dialog.form.${identifier}`)}
  241. placeholder={i18n.t("campaigns.dialog.form.messagePlaceholder")}
  242. multiline={true}
  243. variant="outlined"
  244. helperText={i18n.t("campaigns.dialog.form.helper")}
  245. disabled={!campaignEditable && campaign.status !== "CANCELADA"}
  246. />
  247. );
  248. };
  249. const cancelCampaign = async () => {
  250. try {
  251. await api.post(`/campaigns/${campaign.id}/cancel`);
  252. toast.success(i18n.t("campaigns.toasts.cancel"));
  253. setCampaign((prev) => ({ ...prev, status: "CANCELADA" }));
  254. resetPagination();
  255. } catch (err) {
  256. toast.error(err.message);
  257. }
  258. };
  259. const restartCampaign = async () => {
  260. try {
  261. await api.post(`/campaigns/${campaign.id}/restart`);
  262. toast.success(i18n.t("campaigns.toasts.restart"));
  263. setCampaign((prev) => ({ ...prev, status: "EM_ANDAMENTO" }));
  264. resetPagination();
  265. } catch (err) {
  266. toast.error(err.message);
  267. }
  268. };
  269. return (
  270. <div className={classes.root}>
  271. <ConfirmationModal
  272. title={i18n.t("campaigns.confirmationModal.deleteTitle")}
  273. open={confirmationOpen}
  274. onClose={() => setConfirmationOpen(false)}
  275. onConfirm={deleteMedia}
  276. >
  277. {i18n.t("campaigns.confirmationModal.deleteMessage")}
  278. </ConfirmationModal>
  279. <Dialog
  280. open={open}
  281. onClose={handleClose}
  282. fullWidth
  283. maxWidth="md"
  284. scroll="paper"
  285. >
  286. <DialogTitle id="form-dialog-title">
  287. {campaignEditable ? (
  288. <>
  289. {campaignId
  290. ? `${i18n.t("campaigns.dialog.update")}`
  291. : `${i18n.t("campaigns.dialog.new")}`}
  292. </>
  293. ) : (
  294. <>{`${i18n.t("campaigns.dialog.readonly")}`}</>
  295. )}
  296. </DialogTitle>
  297. <div style={{ display: "none" }}>
  298. <input
  299. type="file"
  300. ref={attachmentFile}
  301. onChange={(e) => handleAttachmentFile(e)}
  302. />
  303. </div>
  304. <Formik
  305. initialValues={campaign}
  306. enableReinitialize={true}
  307. validationSchema={CampaignSchema}
  308. onSubmit={(values, actions) => {
  309. setTimeout(() => {
  310. handleSaveCampaign(values);
  311. actions.setSubmitting(false);
  312. }, 400);
  313. }}
  314. >
  315. {({ values, errors, touched, isSubmitting }) => (
  316. <Form>
  317. <DialogContent dividers>
  318. <Grid spacing={2} container>
  319. <Grid xs={12} md={9} item>
  320. <Field
  321. as={TextField}
  322. label={i18n.t("campaigns.dialog.form.name")}
  323. name="name"
  324. error={touched.name && Boolean(errors.name)}
  325. helperText={touched.name && errors.name}
  326. variant="outlined"
  327. margin="dense"
  328. fullWidth
  329. className={classes.textField}
  330. disabled={!campaignEditable}
  331. />
  332. </Grid>
  333. <Grid xs={12} md={4} item>
  334. <FormControl
  335. variant="outlined"
  336. margin="dense"
  337. fullWidth
  338. className={classes.formControl}
  339. >
  340. <InputLabel id="contactList-selection-label">
  341. {i18n.t("campaigns.dialog.form.contactList")}
  342. </InputLabel>
  343. <Field
  344. as={Select}
  345. label={i18n.t("campaigns.dialog.form.contactList")}
  346. placeholder={i18n.t(
  347. "campaigns.dialog.form.contactList"
  348. )}
  349. labelId="contactList-selection-label"
  350. id="contactListId"
  351. name="contactListId"
  352. error={
  353. touched.contactListId && Boolean(errors.contactListId)
  354. }
  355. disabled={!campaignEditable}
  356. >
  357. <MenuItem value="">Nenhuma</MenuItem>
  358. {contactLists &&
  359. contactLists.map((contactList) => (
  360. <MenuItem
  361. key={contactList.id}
  362. value={contactList.id}
  363. >
  364. {contactList.name}
  365. </MenuItem>
  366. ))}
  367. </Field>
  368. </FormControl>
  369. </Grid>
  370. <Grid xs={12} md={4} item>
  371. <FormControl
  372. variant="outlined"
  373. margin="dense"
  374. fullWidth
  375. className={classes.formControl}
  376. >
  377. <InputLabel id="tagList-selection-label">
  378. {i18n.t("campaigns.dialog.form.tagList")}
  379. </InputLabel>
  380. <Field
  381. as={Select}
  382. label={i18n.t("campaigns.dialog.form.tagList")}
  383. placeholder={i18n.t("campaigns.dialog.form.tagList")}
  384. labelId="tagList-selection-label"
  385. id="tagListId"
  386. name="tagListId"
  387. error={touched.tagListId && Boolean(errors.tagListId)}
  388. disabled={!campaignEditable}
  389. >
  390. <MenuItem value="">Nenhuma</MenuItem>
  391. {Array.isArray(tagLists) &&
  392. tagLists.map((tagList) => (
  393. <MenuItem key={tagList.id} value={tagList.id}>
  394. {tagList.name}
  395. </MenuItem>
  396. ))}
  397. </Field>
  398. </FormControl>
  399. </Grid>
  400. <Grid xs={12} md={4} item>
  401. <FormControl
  402. variant="outlined"
  403. margin="dense"
  404. fullWidth
  405. className={classes.formControl}
  406. >
  407. <InputLabel id="whatsapp-selection-label">
  408. {i18n.t("campaigns.dialog.form.whatsapp")}
  409. </InputLabel>
  410. <Field
  411. as={Select}
  412. label={i18n.t("campaigns.dialog.form.whatsapp")}
  413. placeholder={i18n.t("campaigns.dialog.form.whatsapp")}
  414. labelId="whatsapp-selection-label"
  415. id="whatsappId"
  416. name="whatsappId"
  417. error={touched.whatsappId && Boolean(errors.whatsappId)}
  418. disabled={!campaignEditable}
  419. >
  420. <MenuItem value="">Nenhuma</MenuItem>
  421. {whatsapps &&
  422. whatsapps.map((whatsapp) => (
  423. <MenuItem key={whatsapp.id} value={whatsapp.id}>
  424. {whatsapp.name}
  425. </MenuItem>
  426. ))}
  427. </Field>
  428. </FormControl>
  429. </Grid>
  430. <Grid xs={12} md={4} item>
  431. <Field
  432. as={TextField}
  433. label={i18n.t("campaigns.dialog.form.scheduledAt")}
  434. name="scheduledAt"
  435. error={touched.scheduledAt && Boolean(errors.scheduledAt)}
  436. helperText={touched.scheduledAt && errors.scheduledAt}
  437. variant="outlined"
  438. margin="dense"
  439. type="datetime-local"
  440. InputLabelProps={{
  441. shrink: true,
  442. }}
  443. fullWidth
  444. className={classes.textField}
  445. disabled={!campaignEditable}
  446. />
  447. </Grid>
  448. <Grid xs={12} md={4} item>
  449. <FormControl
  450. variant="outlined"
  451. margin="dense"
  452. className={classes.FormControl}
  453. fullWidth
  454. >
  455. <InputLabel id="fileListId-selection-label">{i18n.t("campaigns.dialog.form.fileList")}</InputLabel>
  456. <Field
  457. as={Select}
  458. label={i18n.t("campaigns.dialog.form.fileList")}
  459. name="fileListId"
  460. id="fileListId"
  461. placeholder={i18n.t("campaigns.dialog.form.fileList")}
  462. labelId="fileListId-selection-label"
  463. value={values.fileListId || ""}
  464. >
  465. <MenuItem value={""} >{"Nenhum"}</MenuItem>
  466. {file.map(f => (
  467. <MenuItem key={f.id} value={f.id}>
  468. {f.name}
  469. </MenuItem>
  470. ))}
  471. </Field>
  472. </FormControl>
  473. </Grid>
  474. <Grid xs={12} item>
  475. <Tabs
  476. value={messageTab}
  477. indicatorColor="primary"
  478. textColor="primary"
  479. className={classes.tabmsg}
  480. onChange={(e, v) => setMessageTab(v)}
  481. variant="fullWidth"
  482. centered
  483. style={{
  484. borderRadius: 2,
  485. }}
  486. >
  487. <Tab label="Msg. 1" index={0} />
  488. <Tab label="Msg. 2" index={1} />
  489. <Tab label="Msg. 3" index={2} />
  490. <Tab label="Msg. 4" index={3} />
  491. <Tab label="Msg. 5" index={4} />
  492. </Tabs>
  493. <Box style={{ paddingTop: 20, border: "none" }}>
  494. {messageTab === 0 && (
  495. <>{renderMessageField("message1")}</>
  496. )}
  497. {messageTab === 1 && (
  498. <>{renderMessageField("message2")}</>
  499. )}
  500. {messageTab === 2 && (
  501. <>{renderMessageField("message3")}</>
  502. )}
  503. {messageTab === 3 && (
  504. <>{renderMessageField("message4")}</>
  505. )}
  506. {messageTab === 4 && (
  507. <>{renderMessageField("message5")}</>
  508. )}
  509. </Box>
  510. </Grid>
  511. {(campaign.mediaPath || attachment) && (
  512. <Grid xs={12} item>
  513. <Button startIcon={<AttachFileIcon />}>
  514. {attachment != null
  515. ? attachment.name
  516. : campaign.mediaName}
  517. </Button>
  518. {campaignEditable && (
  519. <IconButton
  520. onClick={() => setConfirmationOpen(true)}
  521. color="secondary"
  522. >
  523. <DeleteOutlineIcon />
  524. </IconButton>
  525. )}
  526. </Grid>
  527. )}
  528. </Grid>
  529. </DialogContent>
  530. <DialogActions>
  531. {campaign.status === "CANCELADA" && (
  532. <Button
  533. color="primary"
  534. onClick={() => restartCampaign()}
  535. variant="outlined"
  536. >
  537. {i18n.t("campaigns.dialog.buttons.restart")}
  538. </Button>
  539. )}
  540. {campaign.status === "EM_ANDAMENTO" && (
  541. <Button
  542. color="primary"
  543. onClick={() => cancelCampaign()}
  544. variant="outlined"
  545. >
  546. {i18n.t("campaigns.dialog.buttons.cancel")}
  547. </Button>
  548. )}
  549. {!attachment && !campaign.mediaPath && campaignEditable && (
  550. <Button
  551. color="primary"
  552. onClick={() => attachmentFile.current.click()}
  553. disabled={isSubmitting}
  554. variant="outlined"
  555. >
  556. {i18n.t("campaigns.dialog.buttons.attach")}
  557. </Button>
  558. )}
  559. <Button
  560. onClick={handleClose}
  561. color="secondary"
  562. disabled={isSubmitting}
  563. variant="outlined"
  564. >
  565. {i18n.t("campaigns.dialog.buttons.close")}
  566. </Button>
  567. {(campaignEditable || campaign.status === "CANCELADA") && (
  568. <Button
  569. type="submit"
  570. color="primary"
  571. disabled={isSubmitting}
  572. variant="contained"
  573. className={classes.btnWrapper}
  574. >
  575. {campaignId
  576. ? `${i18n.t("campaigns.dialog.buttons.edit")}`
  577. : `${i18n.t("campaigns.dialog.buttons.add")}`}
  578. {isSubmitting && (
  579. <CircularProgress
  580. size={24}
  581. className={classes.buttonProgress}
  582. />
  583. )}
  584. </Button>
  585. )}
  586. </DialogActions>
  587. </Form>
  588. )}
  589. </Formik>
  590. </Dialog>
  591. </div>
  592. );
  593. };
  594. export default CampaignModal;