diff --git a/backend/src/controllers/MessageController.ts b/backend/src/controllers/MessageController.ts index e026b98..234d654 100644 --- a/backend/src/controllers/MessageController.ts +++ b/backend/src/controllers/MessageController.ts @@ -18,7 +18,9 @@ import { } from "../services/WbotServices/wbotMessageListener"; import CreateOrUpdateContactService from "../services/ContactServices/CreateOrUpdateContactService"; import sendWhatsAppMessageOfficialAPI from "../helpers/sendWhatsAppMessageOfficialAPI"; -import Whatsapp from "../models/Whatsapp"; +import Whatsapp from "../models/Whatsapp"; +import checkLastClientMsg24hs from "../helpers/CheckLastClientMsg24hs"; +import AppError from "../errors/AppError"; type IndexQuery = { pageNumber: string; @@ -29,7 +31,8 @@ type MessageData = { fromMe: boolean; read: boolean; quotedMsg?: Message; - mic_audio?: boolean + mic_audio?: boolean; + params: any; }; export const index = async (req: Request, res: Response): Promise => { @@ -40,18 +43,98 @@ export const index = async (req: Request, res: Response): Promise => { pageNumber, ticketId }); - + return res.json({ count, messages, ticket, hasMore }); }; export const store = async (req: Request, res: Response): Promise => { const { ticketId } = req.params; - const { body, quotedMsg, mic_audio }: MessageData = req.body; + const { body, quotedMsg, mic_audio, params }: MessageData = req.body; const medias = req.files as Express.Multer.File[]; const ticket = await ShowTicketService(ticketId); const { queueId } = ticket; - console.log("-----------> queueId: ", queueId, " | quotedMsg: ", quotedMsg); + console.log( + "-----------> queueId: ", + queueId, + " | quotedMsg: ", + quotedMsg, + " | params: ", + params + ); + + const { phoneNumberId, whatsappId } = ticket; + + if (phoneNumberId) { + const into24hs = await checkLastClientMsg24hs(ticket); + + if (into24hs && into24hs.length == 0) { + if (params) { + console.log("SEND TEMPLATE PARAMS: ", params); + + // return res.send() + + let payloadComponents = []; + + try { + for (let i in params) { + const { parameters, language, type } = params[i]; + if (type == "BODY") { + if (parameters && parameters.length > 0) { + let components: any = [{ type: "body", parameters: [] }]; + for (let x in parameters) { + const { type, text, index } = parameters[x]; + console.log(text); + components[0].parameters.splice(index - 1, 0, { + type, + text + }); + } + payloadComponents.push(components[0]); + } + } else if (type == "BUTTONS") { + } + } + + const name = params.find((p: any) => p?.template_name); + const { language }: any = params.find((p: any) => p?.language); + + const { template_name } = name; + + if (template_name && language) { + const template: any = { + template: { + name: template_name, + language: { code: language }, + components: payloadComponents + } + }; + + sendWhatsAppMessageOfficialAPI(ticket, body, null, template); + + console.log("TEMPLATE: ", template); + return res.send(); + } + } catch (error: any) { + throw new AppError(error.message); + } + } else { + try { + const { wabaId }: any = await Whatsapp.findByPk(whatsappId); + + const { data } = await whatsappOfficialAPI.get( + `/${process.env.VERSION}/${wabaId}/message_templates?language=pt_BR` + ); + + return res.status(200).json(data); + } catch (error) { + return res + .status(500) + .json({ message: "Não foi possível baixar os templates!" }); + } + } + } + } if (medias) { await Promise.all( @@ -70,7 +153,7 @@ export const store = async (req: Request, res: Response): Promise => { media:`, media, "\n" - ); + ); await SendWhatsAppMedia({ media, ticket, mic_audio }); }) ); diff --git a/backend/src/controllers/WbotMonitorController.ts b/backend/src/controllers/WbotMonitorController.ts index a7906d1..7db701a 100644 --- a/backend/src/controllers/WbotMonitorController.ts +++ b/backend/src/controllers/WbotMonitorController.ts @@ -14,6 +14,8 @@ export const wbotMonitorRemote = async (req: Request, res: Response): Promise ACTION: ', req.body['action']) const whatsapp: any = await Whatsapp.findByPk(whatsappId, { raw: true }) diff --git a/backend/src/controllers/WhatsAppController.ts b/backend/src/controllers/WhatsAppController.ts index 9993989..66f4702 100644 --- a/backend/src/controllers/WhatsAppController.ts +++ b/backend/src/controllers/WhatsAppController.ts @@ -80,7 +80,10 @@ export const index = async (req: Request, res: Response): Promise => { } } } catch (error) { - console.log('error on try update classification number from oficial whatsapp in WhatsappController.ts: ', error) + console.log( + "error on try update classification number from oficial whatsapp in WhatsappController.ts: ", + error + ); } } } @@ -244,7 +247,8 @@ export const weebhook = async ( type = "chat"; msg = { ...msg, - body: message.text.body // extract the message text from the webhook payload, + body: message.text.body, // extract the message text from the webhook payload, + type }; } else { const mediaId = message[message.type].id; @@ -265,6 +269,8 @@ export const weebhook = async ( wbot = { ...wbot, media: { filename, mimetype } }; } + msg = { ...msg, phoneNumberId: whatsapp.phoneNumberId }; + console.log("from: ", contact_from); console.log("to: ", contact_to); console.log("msg type: ", type); diff --git a/backend/src/helpers/CheckLastClientMsg24hs.ts b/backend/src/helpers/CheckLastClientMsg24hs.ts new file mode 100644 index 0000000..24f2ba1 --- /dev/null +++ b/backend/src/helpers/CheckLastClientMsg24hs.ts @@ -0,0 +1,22 @@ +import { Op } from "sequelize"; +import { sub, subHours } from "date-fns"; +import Message from "../models/Message"; +import Ticket from "../models/Ticket"; + +async function checkLastClientMsg24hs(ticket: Ticket) { + return await Message.findAll({ + attributes: ["createdAt", "body"], + where: { + contactId: ticket.contactId, + phoneNumberId: ticket.phoneNumberId, + fromMe: false, + createdAt: { + [Op.between]: [+subHours(new Date(), 24), +new Date()] + } + }, + order: [["createdAt", "DESC"]], + limit: 1 + }); +} + +export default checkLastClientMsg24hs; diff --git a/backend/src/helpers/sendWhatsAppMessageOfficialAPI.ts b/backend/src/helpers/sendWhatsAppMessageOfficialAPI.ts index 93e4344..84d1db9 100644 --- a/backend/src/helpers/sendWhatsAppMessageOfficialAPI.ts +++ b/backend/src/helpers/sendWhatsAppMessageOfficialAPI.ts @@ -11,7 +11,8 @@ import whatsappOfficialAPI from "./WhatsappOfficialAPI"; async function sendWhatsAppMessageOfficialAPI( ticket: Ticket, body: string, - quotedMsgSerializedId?: any | undefined + quotedMsgSerializedId?: any | undefined, + _template?: any ) { const { contactId, phoneNumberId } = ticket; @@ -22,14 +23,27 @@ async function sendWhatsAppMessageOfficialAPI( let data: any = { messaging_product: "whatsapp", recipient_type: "individual", - to: number, - type: "text", - text: { - preview_url: true, - body - } + to: number }; + if (_template) { + const { template } = _template; + data = { + ...data, + type: "template", + template + }; + } else { + data = { + ...data, + type: "text", + text: { + preview_url: true, + body + } + }; + } + if (quotedMsgSerializedId) { data = { ...data, context: { message_id: quotedMsgSerializedId.id } }; } @@ -38,6 +52,8 @@ async function sendWhatsAppMessageOfficialAPI( return; } + console.log("SEND MESSAGE: ", JSON.stringify(data, null,2)); + whatsappOfficialAPI .post(`/${process.env.VERSION}/${phoneNumberId}/messages`, data) .then(response => { diff --git a/backend/src/middleware/isAuth.ts b/backend/src/middleware/isAuth.ts index 83cae2a..86428d3 100644 --- a/backend/src/middleware/isAuth.ts +++ b/backend/src/middleware/isAuth.ts @@ -22,7 +22,8 @@ const isAuth = (req: Request, res: Response, next: NextFunction): void => { const [, token] = authHeader.split(" "); try { - const decoded = verify(token, authConfig.secret); + const decoded = verify(token, authConfig.secret); + const { id, profile } = decoded as TokenPayload; req.user = { diff --git a/backend/src/services/WbotServices/SendWhatsAppMessage.ts b/backend/src/services/WbotServices/SendWhatsAppMessage.ts index 87660aa..ec07cd8 100644 --- a/backend/src/services/WbotServices/SendWhatsAppMessage.ts +++ b/backend/src/services/WbotServices/SendWhatsAppMessage.ts @@ -81,7 +81,7 @@ const SendWhatsAppMessage = async ({ } if (!listWhatsapp) { - listWhatsapp = await ListWhatsAppsNumber(ticket.whatsappId, "CONNECTED"); + listWhatsapp = await ListWhatsAppsNumber(ticket.whatsappId, "CONNECTED"); } if ( @@ -116,9 +116,10 @@ const SendWhatsAppMessage = async ({ }); } else if (whatsapps && whatsapps.length == 1) { await ticket.update({ whatsappId: whatsapps[0].id }); - } - else{ - throw new Error('Sessão de Whatsapp desconectada! Entre em contato com o suporte.') + } else { + throw new Error( + "Sessão de Whatsapp desconectada! Entre em contato com o suporte." + ); } } } diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 206659f..06c437d 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -162,7 +162,8 @@ const verifyMediaMessage = async ( read: msg.fromMe, mediaUrl: media.filename, mediaType: media.mimetype.split("/")[0], - quotedMsgId: quotedMsg + quotedMsgId: quotedMsg, + phoneNumberId: msg?.phoneNumberId }; @@ -443,6 +444,7 @@ const mediaTypeWhatsappOfficial = (mimetype: string): object => { const isValidMsg = (msg: any): boolean => { if (msg.from === "status@broadcast") return false; if ( + msg.type === "template" || msg.type === "text" || msg.type === "hsm" || msg.type === "chat" || diff --git a/frontend/src/components/ContactCreateTicketModal/index.js b/frontend/src/components/ContactCreateTicketModal/index.js index bbc199b..3681c6c 100644 --- a/frontend/src/components/ContactCreateTicketModal/index.js +++ b/frontend/src/components/ContactCreateTicketModal/index.js @@ -178,7 +178,7 @@ const ContactCreateTicketModal = ({ modalOpen, onClose, contactId }) => { useEffect(() => { console.log('selectedWhatsId: ', selectedWhatsId) console.log('whatsQuee: ', whatsQueue) - }, [whatsQueue]) + }, [whatsQueue, selectedWhatsId]) return ( diff --git a/frontend/src/components/MessageInput/index.js b/frontend/src/components/MessageInput/index.js index 6acfb4e..30c72d1 100644 --- a/frontend/src/components/MessageInput/index.js +++ b/frontend/src/components/MessageInput/index.js @@ -1,47 +1,50 @@ -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 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 MoreVert from "@material-ui/icons/MoreVert"; -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 { 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 MoreVert from "@material-ui/icons/MoreVert" +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, Hidden, Menu, MenuItem, Switch, -} from "@material-ui/core"; -import ClickAwayListener from "@material-ui/core/ClickAwayListener"; +} from "@material-ui/core" +import ClickAwayListener from "@material-ui/core/ClickAwayListener" -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"; +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" // import TicketsManager from "../../components/TicketsManager/"; -import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption"; +import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption" +import ModalTemplate from "../ModalTemplate" -const Mp3Recorder = new MicRecorder({ bitRate: 128 }); +import { render } from '@testing-library/react' + +const Mp3Recorder = new MicRecorder({ bitRate: 128 }) const useStyles = makeStyles((theme) => ({ mainWrapper: { @@ -203,107 +206,116 @@ const useStyles = makeStyles((theme) => ({ }, }, }, -})); +})) const MessageInput = ({ ticketStatus }) => { - const { tabOption, setTabOption } = useContext(TabTicketContext); + const { tabOption, setTabOption } = useContext(TabTicketContext) - const classes = useStyles(); - const { ticketId } = useParams(); + 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 [quickAnswers, setQuickAnswer] = useState([]); - const [typeBar, setTypeBar] = useState(false); - const inputRef = useRef(); - const [anchorEl, setAnchorEl] = useState(null); - const { setReplyingMessage, replyingMessage } = useContext(ReplyMessageContext); - const { user } = useContext(AuthContext); + const [medias, setMedias] = useState([]) + const [inputMessage, setInputMessage] = useState("") + const [showEmoji, setShowEmoji] = useState(false) + const [loading, setLoading] = useState(false) + const [recording, setRecording] = useState(false) + const [quickAnswers, setQuickAnswer] = useState([]) + const [typeBar, setTypeBar] = useState(false) + const inputRef = useRef() + const [anchorEl, setAnchorEl] = useState(null) + const { setReplyingMessage, replyingMessage } = useContext(ReplyMessageContext) + const { user } = useContext(AuthContext) + const [templates, setTemplates] = useState(null) + const [params, setParams] = useState(null) + + const [signMessage, setSignMessage] = useLocalStorage("signOption", true) + + const isRun = useRef(false) - const [signMessage, setSignMessage] = useLocalStorage("signOption", true); useEffect(() => { - inputRef.current.focus(); - }, [replyingMessage]); + inputRef.current.focus() + }, [replyingMessage]) useEffect(() => { - inputRef.current.focus(); + inputRef.current.focus() return () => { - setInputMessage(""); - setShowEmoji(false); - setMedias([]); - setReplyingMessage(null); - }; - }, [ticketId, setReplyingMessage]); + setInputMessage("") + setShowEmoji(false) + setMedias([]) + setReplyingMessage(null) + } + }, [ticketId, setReplyingMessage]) const handleChangeInput = (e) => { - setInputMessage(e.target.value); - handleLoadQuickAnswer(e.target.value); - }; + setInputMessage(e.target.value) + handleLoadQuickAnswer(e.target.value) + } const handleQuickAnswersClick = (value) => { - setInputMessage(value); - setTypeBar(false); - }; + setInputMessage(value) + setTypeBar(false) + } const handleAddEmoji = (e) => { - let emoji = e.native; - setInputMessage((prevState) => prevState + emoji); - }; + let emoji = e.native + setInputMessage((prevState) => prevState + emoji) + } const handleChangeMedias = (e) => { if (!e.target.files) { - return; + return } - const selectedMedias = Array.from(e.target.files); - setMedias(selectedMedias); - }; + const selectedMedias = Array.from(e.target.files) + setMedias(selectedMedias) + } const handleInputPaste = (e) => { if (e.clipboardData.files[0]) { console.log('clipboardData: ', e.clipboardData.files[0]) - setMedias([e.clipboardData.files[0]]); + setMedias([e.clipboardData.files[0]]) } - }; + } const handleUploadMedia = async (e) => { - setLoading(true); - e.preventDefault(); + setLoading(true) + e.preventDefault() + - if (tabOption === 'search') { setTabOption('open') } - const formData = new FormData(); - formData.append("fromMe", true); + const formData = new FormData() + formData.append("fromMe", true) medias.forEach((media) => { - formData.append("medias", media); - formData.append("body", media.name); - }); + formData.append("medias", media) + formData.append("body", media.name) + }) try { - await api.post(`/messages/${ticketId}`, formData); + const { data } = await api.post(`/messages/${ticketId}`, formData) + + console.log('DATA FROM SEND MESSAGE MEDIA: ', data) + } catch (err) { - toastError(err); + toastError(err) } - setLoading(false); - setMedias([]); - }; + setLoading(false) + setMedias([]) + } - const handleSendMessage = async () => { - if (inputMessage.trim() === "") return; - setLoading(true); - - + const handleSendMessage = async (templateParams = null) => { + + console.log('templateParams: ', templateParams, ' | inputMessage: ', inputMessage) + + if (inputMessage.trim() === "") return + setLoading(true) if (tabOption === 'search') { setTabOption('open') @@ -313,108 +325,166 @@ const MessageInput = ({ ticketStatus }) => { read: 1, fromMe: true, mediaUrl: "", - body: signMessage + body: (signMessage && !templateParams) ? `*${user?.name}:*\n${inputMessage.trim()}` : inputMessage.trim(), quotedMsg: replyingMessage, - }; + params: templateParams + } try { - // console.log('message: ', message) - await api.post(`/messages/${ticketId}`, message); + const { data } = await api.post(`/messages/${ticketId}`, message) + setParams(null) + if (data && data?.data && Array.isArray(data.data)) { + setTemplates(data.data) + } + } catch (err) { - toastError(err); + toastError(err) } - setInputMessage(""); - setShowEmoji(false); - setLoading(false); - setReplyingMessage(null); - }; + setInputMessage("") + setShowEmoji(false) + setLoading(false) + setReplyingMessage(null) + } + + + useEffect(() => { + + if (!params) return + + const body_params = params.find(p => p?.type === 'BODY') + + let { text } = body_params + + console.log('PARAMS FROM MESSAGE INPUT: ', params, ' | text: ', text) + + let body = text.match(/{{\d+}}/g) + + if (body && body.length > 0) { + + const { parameters } = body_params + + for (const key in parameters) { + if (!isNaN(key)) { + const { index, text: body_text } = parameters[key] + text = text.replace(`{{${index}}}`, body_text) + } + } + + } + console.log('NEW TEXT: ', text) + setInputMessage(text) + + }, [params]) + + useEffect(() => { + + if (params) { + handleSendMessage(params) + } + + }, [inputMessage, params]) + + useEffect(() => { + + if (!templates) return + + return render( { + return { id, name, components, language, } + })} + ticketId={ticketId} + />) + + }, [templates]) const handleStartRecording = async () => { - setLoading(true); + setLoading(true) try { - await navigator.mediaDevices.getUserMedia({ audio: true }); - await Mp3Recorder.start(); - setRecording(true); - setLoading(false); + await navigator.mediaDevices.getUserMedia({ audio: true }) + await Mp3Recorder.start() + setRecording(true) + setLoading(false) } catch (err) { - toastError(err); - setLoading(false); + toastError(err) + setLoading(false) } - }; + } const handleLoadQuickAnswer = async (value) => { if (value && value.indexOf("/") === 0) { try { const { data } = await api.get("/quickAnswers/", { params: { searchParam: inputMessage.substring(1) }, - }); - setQuickAnswer(data.quickAnswers); + }) + setQuickAnswer(data.quickAnswers) if (data.quickAnswers.length > 0) { - setTypeBar(true); + setTypeBar(true) } else { - setTypeBar(false); + setTypeBar(false) } } catch (err) { - setTypeBar(false); + setTypeBar(false) } } else { - setTypeBar(false); + setTypeBar(false) } - }; + } const handleUploadAudio = async () => { - setLoading(true); + setLoading(true) + - if (tabOption === 'search') { setTabOption('open') } try { - const [, blob] = await Mp3Recorder.stop().getMp3(); + const [, blob] = await Mp3Recorder.stop().getMp3() if (blob.size < 10000) { - setLoading(false); - setRecording(false); - return; + 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); + const formData = new FormData() + const filename = `${new Date().getTime()}.mp3` + formData.append("medias", blob, filename) + formData.append("body", filename) + formData.append("fromMe", true) formData.append("mic_audio", true) - await api.post(`/messages/${ticketId}`, formData); + await api.post(`/messages/${ticketId}`, formData) } catch (err) { - toastError(err); + toastError(err) } - setRecording(false); - setLoading(false); - }; + setRecording(false) + setLoading(false) + } const handleCancelAudio = async () => { try { - await Mp3Recorder.stop().getMp3(); - setRecording(false); + await Mp3Recorder.stop().getMp3() + setRecording(false) } catch (err) { - toastError(err); + toastError(err) } - }; + } const handleOpenMenuClick = (event) => { - setAnchorEl(event.currentTarget); - }; + setAnchorEl(event.currentTarget) + } const handleMenuItemClick = (event) => { - setAnchorEl(null); - }; + setAnchorEl(null) + } const renderReplyingMessage = (message) => { return ( @@ -443,8 +513,8 @@ const MessageInput = ({ ticketStatus }) => { - ); - }; + ) + } if (medias.length > 0) return ( @@ -476,7 +546,7 @@ const MessageInput = ({ ticketStatus }) => { - ); + ) else { return ( @@ -530,7 +600,7 @@ const MessageInput = ({ ticketStatus }) => { size="small" checked={signMessage} onChange={(e) => { - setSignMessage(e.target.checked); + setSignMessage(e.target.checked) }} name="showAllTickets" color="primary" @@ -592,7 +662,7 @@ const MessageInput = ({ ticketStatus }) => { size="small" checked={signMessage} onChange={(e) => { - setSignMessage(e.target.checked); + setSignMessage(e.target.checked) }} name="showAllTickets" color="primary" @@ -605,8 +675,8 @@ const MessageInput = ({ ticketStatus }) => {
{ - input && input.focus(); - input && (inputRef.current = input); + input && input.focus() + input && (inputRef.current = input) }} className={classes.messageInput} placeholder={ @@ -621,12 +691,12 @@ const MessageInput = ({ ticketStatus }) => { onChange={handleChangeInput} disabled={recording || loading || ticketStatus !== "open"} onPaste={(e) => { - ticketStatus === "open" && handleInputPaste(e); + ticketStatus === "open" && handleInputPaste(e) }} onKeyPress={(e) => { - if (loading || e.shiftKey) return; + if (loading || e.shiftKey) return else if (e.key === "Enter") { - handleSendMessage(); + handleSendMessage() } }} /> @@ -643,7 +713,7 @@ const MessageInput = ({ ticketStatus }) => { {`${value.shortcut} - ${value.message}`} - ); + ) })} ) : ( @@ -699,8 +769,8 @@ const MessageInput = ({ ticketStatus }) => { )}
- ); + ) } -}; +} -export default MessageInput; +export default MessageInput diff --git a/frontend/src/components/ModalTemplate/index.js b/frontend/src/components/ModalTemplate/index.js new file mode 100644 index 0000000..c969403 --- /dev/null +++ b/frontend/src/components/ModalTemplate/index.js @@ -0,0 +1,242 @@ + +import React, { useState, useEffect, useRef, } from 'react' +import Button from '@mui/material/Button' +import Dialog from '@mui/material/Dialog' +import DialogActions from '@mui/material/DialogActions' +import DialogContent from '@mui/material/DialogContent' +import DialogContentText from '@mui/material/DialogContentText' +import DialogTitle from '@mui/material/DialogTitle' +import SelectField from "../Report/SelectField" + +import TextField from '@mui/material/TextField' + + + +const ModalTemplate = ({ templates, modal_header, func }) => { + + templates = [{}, ...templates] + + const [open, setOpen] = useState(true) + const [scroll, /*setScroll*/] = useState('body') + const [templateId, setTemplateId] = useState(null) + const [templateComponents, setTemplateComponents] = useState(null) + const [language, setLanguage] = useState(null) + const [params, setParams] = useState([]) + + const handleCancel = (event, reason) => { + + if (reason && reason === "backdropClick") + return + + setOpen(false) + } + + const handleChatEnd = () => { + + console.log('PARAMS TO SEND TO MESSAGE INPUT: ', params) + func(params) + setOpen(false) + } + + const descriptionElementRef = useRef(null) + useEffect(() => { + if (open) { + const { current: descriptionElement } = descriptionElementRef + if (descriptionElement !== null) { + descriptionElement.focus() + } + } + }, [open]) + + + // Get from child 1 + const changedTextFieldSelect = (data) => { + setTemplateId(data) + } + + useEffect(() => { + + const index = templates.findIndex(t => t.id === templateId) + setParams([]) + if (index !== -1) { + + const { components, language, name } = templates[index] + + setParams((params) => [...params, { 'template_name': name }]) + + const buttons = components.find(c => c.type === 'BUTTONS') + + if (buttons) { + handleButtons(buttons?.buttons) + } + + setTemplateComponents(components) + setLanguage(language) + } + else { + setTemplateComponents(null) + setLanguage(null) + } + + }, [templateId]) + + const handleButtons = (buttons) => { + + let buttonsParams = { + type: 'BUTTONS', + parameters: [] + } + + if (buttons && buttons.length > 0) { + for (let i in buttons) { + const { text, type: sub_type } = buttons[i] + buttonsParams.parameters.push({ sub_type, text, index: i, }) + } + } + + setParams((params) => [...params, buttonsParams]) + + } + + const handleTextChange = (value, index, type, text, language,) => { + + if (!params) return + + setParams((params) => { + + const _index = params.findIndex(({ type }) => type === 'BODY') + + if (_index !== -1) { + + const indexParameter = params[_index].parameters.findIndex((param) => param.index === index) + + if (indexParameter !== -1) { + params[_index].parameters[indexParameter] = { ...params[_index].parameters[indexParameter], type: 'text', text: value, index } + } + else { + params[_index].parameters = [...params[_index].parameters, { type: 'text', text: value, index }] + } + + return params + + } + return [...params, { type, text, language, parameters: [{ type: 'text', text: value, index }] }] + + }) + } + + useEffect(() => { + console.log('---------> PARAMS: ', params) + }, [params]) + + const dinamicTextField = (replicateItems, func, type, text, language) => { + + let textFields = Array.from({ length: replicateItems }, (_, index) => index) + + return textFields.map((t) => { + return func(e.target.value, (t + 1), type, text, language)} + /> + }) + } + + + return ( + + + + {modal_header} + + + + + + + <> + + { + const { name, id } = template + return { 'value': id, 'label': name } + })} /> + + {templateComponents && + templateComponents.map((components,) => { + const { type, format, text, buttons } = components + + let body_params = 0 + + if (type === 'BODY') { + body_params = text.match(/{{\d+}}/g) + } + + const titleCss = { + margin: 0, + fontSize: 12 + } + const valueCss = { margin: 0, padding: 0, 'marginBottom': '15px', fontSize: 12 } + const valueCssButton = { margin: 0, padding: 0, fontSize: 11 } + + return
+ {type && {type}} + {format && format !== 'TEXT' &&

TYPE {format}

} + {text && +
+

{text}

+ {type && (type === 'BODY') && dinamicTextField(body_params.length, handleTextChange, type, text, language)} +
} + {buttons &&
{buttons.map((b) => { + const { type, text, url } = b + return
+ {type &&

TYPE {type}

} + {text &&

{text}

} + {url &&

{url}

} +
+ })}
} + +
+ }) + } + + + + + + + +
+ + + +
+ +
+ +
+
+ + ) +} + +export default ModalTemplate \ No newline at end of file diff --git a/frontend/src/components/TicketListItem/index.js b/frontend/src/components/TicketListItem/index.js index 5c230ef..60c0fee 100644 --- a/frontend/src/components/TicketListItem/index.js +++ b/frontend/src/components/TicketListItem/index.js @@ -1,27 +1,27 @@ -import React, { useState, useEffect, useRef, useContext } from "react"; +import React, { useState, useEffect, useRef, useContext } from "react" -import { useHistory, useParams } from "react-router-dom"; -import { parseISO, format, isSameDay } from "date-fns"; -import clsx from "clsx"; +import { useHistory, useParams } from "react-router-dom" +import { parseISO, format, isSameDay } from "date-fns" +import clsx from "clsx" -import { makeStyles } from "@material-ui/core/styles"; -import { green } from "@material-ui/core/colors"; -import ListItem from "@material-ui/core/ListItem"; -import ListItemText from "@material-ui/core/ListItemText"; -import ListItemAvatar from "@material-ui/core/ListItemAvatar"; -import Typography from "@material-ui/core/Typography"; -import Avatar from "@material-ui/core/Avatar"; -import Divider from "@material-ui/core/Divider"; -import Badge from "@material-ui/core/Badge"; +import { makeStyles } from "@material-ui/core/styles" +import { green } from "@material-ui/core/colors" +import ListItem from "@material-ui/core/ListItem" +import ListItemText from "@material-ui/core/ListItemText" +import ListItemAvatar from "@material-ui/core/ListItemAvatar" +import Typography from "@material-ui/core/Typography" +import Avatar from "@material-ui/core/Avatar" +import Divider from "@material-ui/core/Divider" +import Badge from "@material-ui/core/Badge" -import { i18n } from "../../translate/i18n"; +import { i18n } from "../../translate/i18n" -import api from "../../services/api"; -import ButtonWithSpinner from "../ButtonWithSpinner"; -import MarkdownWrapper from "../MarkdownWrapper"; -import { Tooltip } from "@material-ui/core"; -import { AuthContext } from "../../context/Auth/AuthContext"; -import toastError from "../../errors/toastError"; +import api from "../../services/api" +import ButtonWithSpinner from "../ButtonWithSpinner" +import MarkdownWrapper from "../MarkdownWrapper" +import { Tooltip } from "@material-ui/core" +import { AuthContext } from "../../context/Auth/AuthContext" +import toastError from "../../errors/toastError" const useStyles = makeStyles(theme => ({ ticket: { @@ -99,46 +99,46 @@ const useStyles = makeStyles(theme => ({ top: "0%", left: "0%", }, -})); +})) const TicketListItem = ({ ticket }) => { - const classes = useStyles(); - const history = useHistory(); - const [loading, setLoading] = useState(false); - const { ticketId } = useParams(); - const isMounted = useRef(true); - const { user } = useContext(AuthContext); + const classes = useStyles() + const history = useHistory() + const [loading, setLoading] = useState(false) + const { ticketId } = useParams() + const isMounted = useRef(true) + const { user } = useContext(AuthContext) useEffect(() => { return () => { - isMounted.current = false; - }; - }, []); + isMounted.current = false + } + }, []) const handleAcepptTicket = async id => { - setLoading(true); + setLoading(true) try { - + await api.put(`/tickets/${id}`, { status: "open", userId: user?.id, - }); + }) } catch (err) { - setLoading(false); - toastError(err); + setLoading(false) + toastError(err) } if (isMounted.current) { - setLoading(false); + setLoading(false) } - - history.push(`/tickets/${id}`); - }; + + history.push(`/tickets/${id}`) + } const handleSelectTicket = id => { - history.push(`/tickets/${id}`); - }; + history.push(`/tickets/${id}`) + } return ( @@ -146,8 +146,8 @@ const TicketListItem = ({ ticket }) => { dense button onClick={e => { - if (ticket.status === "pending") return; - handleSelectTicket(ticket.id); + if (ticket.status === "pending") return + handleSelectTicket(ticket.id) }} selected={ticketId && +ticketId === ticket.id} className={clsx(classes.ticket, { @@ -193,6 +193,7 @@ const TicketListItem = ({ ticket }) => { variant="body2" color="textSecondary" > + {ticket?.phoneNumberId && Oficial}{" "} {isSameDay(parseISO(ticket.updatedAt), new Date()) ? ( <>{format(parseISO(ticket.updatedAt), "HH:mm")} ) : ( @@ -251,7 +252,7 @@ const TicketListItem = ({ ticket }) => { - ); -}; + ) +} -export default TicketListItem; +export default TicketListItem