Atualização basica para envio de template do Whatsapp cloud API

pull/21/head
adriano 2023-09-19 09:41:15 -03:00
parent 72c489a27b
commit c151473d52
12 changed files with 667 additions and 221 deletions

View File

@ -19,6 +19,8 @@ import {
import CreateOrUpdateContactService from "../services/ContactServices/CreateOrUpdateContactService"; import CreateOrUpdateContactService from "../services/ContactServices/CreateOrUpdateContactService";
import sendWhatsAppMessageOfficialAPI from "../helpers/sendWhatsAppMessageOfficialAPI"; 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 = { type IndexQuery = {
pageNumber: string; pageNumber: string;
@ -29,7 +31,8 @@ type MessageData = {
fromMe: boolean; fromMe: boolean;
read: boolean; read: boolean;
quotedMsg?: Message; quotedMsg?: Message;
mic_audio?: boolean mic_audio?: boolean;
params: any;
}; };
export const index = async (req: Request, res: Response): Promise<Response> => { export const index = async (req: Request, res: Response): Promise<Response> => {
@ -46,12 +49,92 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
export const store = async (req: Request, res: Response): Promise<Response> => { export const store = async (req: Request, res: Response): Promise<Response> => {
const { ticketId } = req.params; 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 medias = req.files as Express.Multer.File[];
const ticket = await ShowTicketService(ticketId); const ticket = await ShowTicketService(ticketId);
const { queueId } = ticket; 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) { if (medias) {
await Promise.all( await Promise.all(

View File

@ -14,6 +14,8 @@ export const wbotMonitorRemote = async (req: Request, res: Response): Promise<Re
const { action, whatsappId, reason } = req.body const { action, whatsappId, reason } = req.body
console.log('action: ', action, ' | whatsappId: ', whatsappId, ' | reason: ', reason)
console.log('-----------> ACTION: ', req.body['action']) console.log('-----------> ACTION: ', req.body['action'])
const whatsapp: any = await Whatsapp.findByPk(whatsappId, { raw: true }) const whatsapp: any = await Whatsapp.findByPk(whatsappId, { raw: true })

View File

@ -80,7 +80,10 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
} }
} }
} catch (error) { } 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"; type = "chat";
msg = { msg = {
...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 { } else {
const mediaId = message[message.type].id; const mediaId = message[message.type].id;
@ -265,6 +269,8 @@ export const weebhook = async (
wbot = { ...wbot, media: { filename, mimetype } }; wbot = { ...wbot, media: { filename, mimetype } };
} }
msg = { ...msg, phoneNumberId: whatsapp.phoneNumberId };
console.log("from: ", contact_from); console.log("from: ", contact_from);
console.log("to: ", contact_to); console.log("to: ", contact_to);
console.log("msg type: ", type); console.log("msg type: ", type);

View File

@ -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;

View File

@ -11,7 +11,8 @@ import whatsappOfficialAPI from "./WhatsappOfficialAPI";
async function sendWhatsAppMessageOfficialAPI( async function sendWhatsAppMessageOfficialAPI(
ticket: Ticket, ticket: Ticket,
body: string, body: string,
quotedMsgSerializedId?: any | undefined quotedMsgSerializedId?: any | undefined,
_template?: any
) { ) {
const { contactId, phoneNumberId } = ticket; const { contactId, phoneNumberId } = ticket;
@ -22,13 +23,26 @@ async function sendWhatsAppMessageOfficialAPI(
let data: any = { let data: any = {
messaging_product: "whatsapp", messaging_product: "whatsapp",
recipient_type: "individual", recipient_type: "individual",
to: number, to: number
};
if (_template) {
const { template } = _template;
data = {
...data,
type: "template",
template
};
} else {
data = {
...data,
type: "text", type: "text",
text: { text: {
preview_url: true, preview_url: true,
body body
} }
}; };
}
if (quotedMsgSerializedId) { if (quotedMsgSerializedId) {
data = { ...data, context: { message_id: quotedMsgSerializedId.id } }; data = { ...data, context: { message_id: quotedMsgSerializedId.id } };
@ -38,6 +52,8 @@ async function sendWhatsAppMessageOfficialAPI(
return; return;
} }
console.log("SEND MESSAGE: ", JSON.stringify(data, null,2));
whatsappOfficialAPI whatsappOfficialAPI
.post(`/${process.env.VERSION}/${phoneNumberId}/messages`, data) .post(`/${process.env.VERSION}/${phoneNumberId}/messages`, data)
.then(response => { .then(response => {

View File

@ -23,6 +23,7 @@ const isAuth = (req: Request, res: Response, next: NextFunction): void => {
try { try {
const decoded = verify(token, authConfig.secret); const decoded = verify(token, authConfig.secret);
const { id, profile } = decoded as TokenPayload; const { id, profile } = decoded as TokenPayload;
req.user = { req.user = {

View File

@ -116,9 +116,10 @@ const SendWhatsAppMessage = async ({
}); });
} else if (whatsapps && whatsapps.length == 1) { } else if (whatsapps && whatsapps.length == 1) {
await ticket.update({ whatsappId: whatsapps[0].id }); await ticket.update({ whatsappId: whatsapps[0].id });
} } else {
else{ throw new Error(
throw new Error('Sessão de Whatsapp desconectada! Entre em contato com o suporte.') "Sessão de Whatsapp desconectada! Entre em contato com o suporte."
);
} }
} }
} }

View File

@ -162,7 +162,8 @@ const verifyMediaMessage = async (
read: msg.fromMe, read: msg.fromMe,
mediaUrl: media.filename, mediaUrl: media.filename,
mediaType: media.mimetype.split("/")[0], 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 => { const isValidMsg = (msg: any): boolean => {
if (msg.from === "status@broadcast") return false; if (msg.from === "status@broadcast") return false;
if ( if (
msg.type === "template" ||
msg.type === "text" || msg.type === "text" ||
msg.type === "hsm" || msg.type === "hsm" ||
msg.type === "chat" || msg.type === "chat" ||

View File

@ -178,7 +178,7 @@ const ContactCreateTicketModal = ({ modalOpen, onClose, contactId }) => {
useEffect(() => { useEffect(() => {
console.log('selectedWhatsId: ', selectedWhatsId) console.log('selectedWhatsId: ', selectedWhatsId)
console.log('whatsQuee: ', whatsQueue) console.log('whatsQuee: ', whatsQueue)
}, [whatsQueue]) }, [whatsQueue, selectedWhatsId])
return ( return (
<Dialog open={modalOpen} onClose={handleClose} maxWidth="xs" scroll="paper" classes={{ paper: classes.paper }}> <Dialog open={modalOpen} onClose={handleClose} maxWidth="xs" scroll="paper" classes={{ paper: classes.paper }}>

View File

@ -1,47 +1,50 @@
import React, { useState, useEffect, useContext, useRef } from "react"; import React, { useState, useEffect, useContext, useRef } from "react"
import "emoji-mart/css/emoji-mart.css"; import "emoji-mart/css/emoji-mart.css"
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom"
import { Picker } from "emoji-mart"; import { Picker } from "emoji-mart"
import MicRecorder from "mic-recorder-to-mp3"; import MicRecorder from "mic-recorder-to-mp3"
import clsx from "clsx"; import clsx from "clsx"
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles"
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper"
import InputBase from "@material-ui/core/InputBase"; import InputBase from "@material-ui/core/InputBase"
import CircularProgress from "@material-ui/core/CircularProgress"; import CircularProgress from "@material-ui/core/CircularProgress"
import { green } from "@material-ui/core/colors"; import { green } from "@material-ui/core/colors"
import AttachFileIcon from "@material-ui/icons/AttachFile"; import AttachFileIcon from "@material-ui/icons/AttachFile"
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton"
import MoreVert from "@material-ui/icons/MoreVert"; import MoreVert from "@material-ui/icons/MoreVert"
import MoodIcon from "@material-ui/icons/Mood"; import MoodIcon from "@material-ui/icons/Mood"
import SendIcon from "@material-ui/icons/Send"; import SendIcon from "@material-ui/icons/Send"
import CancelIcon from "@material-ui/icons/Cancel"; import CancelIcon from "@material-ui/icons/Cancel"
import ClearIcon from "@material-ui/icons/Clear"; import ClearIcon from "@material-ui/icons/Clear"
import MicIcon from "@material-ui/icons/Mic"; import MicIcon from "@material-ui/icons/Mic"
import CheckCircleOutlineIcon from "@material-ui/icons/CheckCircleOutline"; import CheckCircleOutlineIcon from "@material-ui/icons/CheckCircleOutline"
import HighlightOffIcon from "@material-ui/icons/HighlightOff"; import HighlightOffIcon from "@material-ui/icons/HighlightOff"
import { import {
FormControlLabel, FormControlLabel,
Hidden, Hidden,
Menu, Menu,
MenuItem, MenuItem,
Switch, Switch,
} from "@material-ui/core"; } from "@material-ui/core"
import ClickAwayListener from "@material-ui/core/ClickAwayListener"; import ClickAwayListener from "@material-ui/core/ClickAwayListener"
import { i18n } from "../../translate/i18n"; import { i18n } from "../../translate/i18n"
import api from "../../services/api"; import api from "../../services/api"
import RecordingTimer from "./RecordingTimer"; import RecordingTimer from "./RecordingTimer"
import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext"; import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext"
import { AuthContext } from "../../context/Auth/AuthContext"; import { AuthContext } from "../../context/Auth/AuthContext"
import { useLocalStorage } from "../../hooks/useLocalStorage"; import { useLocalStorage } from "../../hooks/useLocalStorage"
import toastError from "../../errors/toastError"; import toastError from "../../errors/toastError"
// import TicketsManager from "../../components/TicketsManager/"; // 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) => ({ const useStyles = makeStyles((theme) => ({
mainWrapper: { mainWrapper: {
@ -203,78 +206,83 @@ const useStyles = makeStyles((theme) => ({
}, },
}, },
}, },
})); }))
const MessageInput = ({ ticketStatus }) => { const MessageInput = ({ ticketStatus }) => {
const { tabOption, setTabOption } = useContext(TabTicketContext); const { tabOption, setTabOption } = useContext(TabTicketContext)
const classes = useStyles(); const classes = useStyles()
const { ticketId } = useParams(); const { ticketId } = useParams()
const [medias, setMedias] = useState([]); const [medias, setMedias] = useState([])
const [inputMessage, setInputMessage] = useState(""); const [inputMessage, setInputMessage] = useState("")
const [showEmoji, setShowEmoji] = useState(false); const [showEmoji, setShowEmoji] = useState(false)
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false)
const [recording, setRecording] = useState(false); const [recording, setRecording] = useState(false)
const [quickAnswers, setQuickAnswer] = useState([]); const [quickAnswers, setQuickAnswer] = useState([])
const [typeBar, setTypeBar] = useState(false); const [typeBar, setTypeBar] = useState(false)
const inputRef = useRef(); const inputRef = useRef()
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null)
const { setReplyingMessage, replyingMessage } = useContext(ReplyMessageContext); const { setReplyingMessage, replyingMessage } = useContext(ReplyMessageContext)
const { user } = useContext(AuthContext); 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(() => { useEffect(() => {
inputRef.current.focus(); inputRef.current.focus()
}, [replyingMessage]); }, [replyingMessage])
useEffect(() => { useEffect(() => {
inputRef.current.focus(); inputRef.current.focus()
return () => { return () => {
setInputMessage(""); setInputMessage("")
setShowEmoji(false); setShowEmoji(false)
setMedias([]); setMedias([])
setReplyingMessage(null); setReplyingMessage(null)
}; }
}, [ticketId, setReplyingMessage]); }, [ticketId, setReplyingMessage])
const handleChangeInput = (e) => { const handleChangeInput = (e) => {
setInputMessage(e.target.value); setInputMessage(e.target.value)
handleLoadQuickAnswer(e.target.value); handleLoadQuickAnswer(e.target.value)
}; }
const handleQuickAnswersClick = (value) => { const handleQuickAnswersClick = (value) => {
setInputMessage(value); setInputMessage(value)
setTypeBar(false); setTypeBar(false)
}; }
const handleAddEmoji = (e) => { const handleAddEmoji = (e) => {
let emoji = e.native; let emoji = e.native
setInputMessage((prevState) => prevState + emoji); setInputMessage((prevState) => prevState + emoji)
}; }
const handleChangeMedias = (e) => { const handleChangeMedias = (e) => {
if (!e.target.files) { if (!e.target.files) {
return; return
} }
const selectedMedias = Array.from(e.target.files); const selectedMedias = Array.from(e.target.files)
setMedias(selectedMedias); setMedias(selectedMedias)
}; }
const handleInputPaste = (e) => { const handleInputPaste = (e) => {
if (e.clipboardData.files[0]) { if (e.clipboardData.files[0]) {
console.log('clipboardData: ', 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) => { const handleUploadMedia = async (e) => {
setLoading(true); setLoading(true)
e.preventDefault(); e.preventDefault()
@ -282,28 +290,32 @@ const MessageInput = ({ ticketStatus }) => {
setTabOption('open') setTabOption('open')
} }
const formData = new FormData(); const formData = new FormData()
formData.append("fromMe", true); formData.append("fromMe", true)
medias.forEach((media) => { medias.forEach((media) => {
formData.append("medias", media); formData.append("medias", media)
formData.append("body", media.name); formData.append("body", media.name)
}); })
try { 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) { } catch (err) {
toastError(err); toastError(err)
} }
setLoading(false); setLoading(false)
setMedias([]); setMedias([])
}; }
const handleSendMessage = async () => { const handleSendMessage = async (templateParams = null) => {
if (inputMessage.trim() === "") return;
setLoading(true);
console.log('templateParams: ', templateParams, ' | inputMessage: ', inputMessage)
if (inputMessage.trim() === "") return
setLoading(true)
if (tabOption === 'search') { if (tabOption === 'search') {
setTabOption('open') setTabOption('open')
@ -313,61 +325,119 @@ const MessageInput = ({ ticketStatus }) => {
read: 1, read: 1,
fromMe: true, fromMe: true,
mediaUrl: "", mediaUrl: "",
body: signMessage body: (signMessage && !templateParams)
? `*${user?.name}:*\n${inputMessage.trim()}` ? `*${user?.name}:*\n${inputMessage.trim()}`
: inputMessage.trim(), : inputMessage.trim(),
quotedMsg: replyingMessage, quotedMsg: replyingMessage,
}; params: templateParams
}
try { try {
// console.log('message: ', message) const { data } = await api.post(`/messages/${ticketId}`, message)
await api.post(`/messages/${ticketId}`, message); setParams(null)
} catch (err) { if (data && data?.data && Array.isArray(data.data)) {
toastError(err); setTemplates(data.data)
} }
setInputMessage(""); } catch (err) {
setShowEmoji(false); toastError(err)
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(<ModalTemplate
modal_header={'Escolha um template para iniciar o Atendimento'}
func={setParams}
templates={templates.map(({ id, name, components, language, }) => {
return { id, name, components, language, }
})}
ticketId={ticketId}
/>)
}, [templates])
const handleStartRecording = async () => { const handleStartRecording = async () => {
setLoading(true); setLoading(true)
try { try {
await navigator.mediaDevices.getUserMedia({ audio: true }); await navigator.mediaDevices.getUserMedia({ audio: true })
await Mp3Recorder.start(); await Mp3Recorder.start()
setRecording(true); setRecording(true)
setLoading(false); setLoading(false)
} catch (err) { } catch (err) {
toastError(err); toastError(err)
setLoading(false); setLoading(false)
}
} }
};
const handleLoadQuickAnswer = async (value) => { const handleLoadQuickAnswer = async (value) => {
if (value && value.indexOf("/") === 0) { if (value && value.indexOf("/") === 0) {
try { try {
const { data } = await api.get("/quickAnswers/", { const { data } = await api.get("/quickAnswers/", {
params: { searchParam: inputMessage.substring(1) }, params: { searchParam: inputMessage.substring(1) },
}); })
setQuickAnswer(data.quickAnswers); setQuickAnswer(data.quickAnswers)
if (data.quickAnswers.length > 0) { if (data.quickAnswers.length > 0) {
setTypeBar(true); setTypeBar(true)
} else { } else {
setTypeBar(false); setTypeBar(false)
} }
} catch (err) { } catch (err) {
setTypeBar(false); setTypeBar(false)
} }
} else { } else {
setTypeBar(false); setTypeBar(false)
}
} }
};
const handleUploadAudio = async () => { const handleUploadAudio = async () => {
setLoading(true); setLoading(true)
@ -376,45 +446,45 @@ const MessageInput = ({ ticketStatus }) => {
} }
try { try {
const [, blob] = await Mp3Recorder.stop().getMp3(); const [, blob] = await Mp3Recorder.stop().getMp3()
if (blob.size < 10000) { if (blob.size < 10000) {
setLoading(false); setLoading(false)
setRecording(false); setRecording(false)
return; return
} }
const formData = new FormData(); const formData = new FormData()
const filename = `${new Date().getTime()}.mp3`; const filename = `${new Date().getTime()}.mp3`
formData.append("medias", blob, filename); formData.append("medias", blob, filename)
formData.append("body", filename); formData.append("body", filename)
formData.append("fromMe", true); formData.append("fromMe", true)
formData.append("mic_audio", true) formData.append("mic_audio", true)
await api.post(`/messages/${ticketId}`, formData); await api.post(`/messages/${ticketId}`, formData)
} catch (err) { } catch (err) {
toastError(err); toastError(err)
} }
setRecording(false); setRecording(false)
setLoading(false); setLoading(false)
}; }
const handleCancelAudio = async () => { const handleCancelAudio = async () => {
try { try {
await Mp3Recorder.stop().getMp3(); await Mp3Recorder.stop().getMp3()
setRecording(false); setRecording(false)
} catch (err) { } catch (err) {
toastError(err); toastError(err)
}
} }
};
const handleOpenMenuClick = (event) => { const handleOpenMenuClick = (event) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget)
}; }
const handleMenuItemClick = (event) => { const handleMenuItemClick = (event) => {
setAnchorEl(null); setAnchorEl(null)
}; }
const renderReplyingMessage = (message) => { const renderReplyingMessage = (message) => {
return ( return (
@ -443,8 +513,8 @@ const MessageInput = ({ ticketStatus }) => {
<ClearIcon className={classes.sendMessageIcons} /> <ClearIcon className={classes.sendMessageIcons} />
</IconButton> </IconButton>
</div> </div>
); )
}; }
if (medias.length > 0) if (medias.length > 0)
return ( return (
@ -476,7 +546,7 @@ const MessageInput = ({ ticketStatus }) => {
<SendIcon className={classes.sendMessageIcons} /> <SendIcon className={classes.sendMessageIcons} />
</IconButton> </IconButton>
</Paper> </Paper>
); )
else { else {
return ( return (
<Paper square elevation={0} className={classes.mainWrapper}> <Paper square elevation={0} className={classes.mainWrapper}>
@ -530,7 +600,7 @@ const MessageInput = ({ ticketStatus }) => {
size="small" size="small"
checked={signMessage} checked={signMessage}
onChange={(e) => { onChange={(e) => {
setSignMessage(e.target.checked); setSignMessage(e.target.checked)
}} }}
name="showAllTickets" name="showAllTickets"
color="primary" color="primary"
@ -592,7 +662,7 @@ const MessageInput = ({ ticketStatus }) => {
size="small" size="small"
checked={signMessage} checked={signMessage}
onChange={(e) => { onChange={(e) => {
setSignMessage(e.target.checked); setSignMessage(e.target.checked)
}} }}
name="showAllTickets" name="showAllTickets"
color="primary" color="primary"
@ -605,8 +675,8 @@ const MessageInput = ({ ticketStatus }) => {
<div className={classes.messageInputWrapper}> <div className={classes.messageInputWrapper}>
<InputBase <InputBase
inputRef={(input) => { inputRef={(input) => {
input && input.focus(); input && input.focus()
input && (inputRef.current = input); input && (inputRef.current = input)
}} }}
className={classes.messageInput} className={classes.messageInput}
placeholder={ placeholder={
@ -621,12 +691,12 @@ const MessageInput = ({ ticketStatus }) => {
onChange={handleChangeInput} onChange={handleChangeInput}
disabled={recording || loading || ticketStatus !== "open"} disabled={recording || loading || ticketStatus !== "open"}
onPaste={(e) => { onPaste={(e) => {
ticketStatus === "open" && handleInputPaste(e); ticketStatus === "open" && handleInputPaste(e)
}} }}
onKeyPress={(e) => { onKeyPress={(e) => {
if (loading || e.shiftKey) return; if (loading || e.shiftKey) return
else if (e.key === "Enter") { else if (e.key === "Enter") {
handleSendMessage(); handleSendMessage()
} }
}} }}
/> />
@ -643,7 +713,7 @@ const MessageInput = ({ ticketStatus }) => {
{`${value.shortcut} - ${value.message}`} {`${value.shortcut} - ${value.message}`}
</a> </a>
</li> </li>
); )
})} })}
</ul> </ul>
) : ( ) : (
@ -699,8 +769,8 @@ const MessageInput = ({ ticketStatus }) => {
)} )}
</div> </div>
</Paper> </Paper>
); )
}
} }
};
export default MessageInput; export default MessageInput

View File

@ -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 <TextField
key={t}
label={`{{${t + 1}}}`}
variant="outlined"
style={{ margin: '4px', }} // Adjust the height as needed
onChange={(e) => func(e.target.value, (t + 1), type, text, language)}
/>
})
}
return (
<Dialog
open={open}
onClose={handleCancel}
// fullWidth={true}
// maxWidth={true}
disableEscapeKeyDown
scroll={scroll}
aria-labelledby="scroll-dialog-title"
aria-describedby="scroll-dialog-description"
>
<DialogTitle id="scroll-dialog-title">{modal_header}</DialogTitle>
<DialogContent dividers={scroll === 'body'}>
<DialogContentText
id="scroll-dialog-description"
ref={descriptionElementRef}
tabIndex={-1}
>
</DialogContentText>
<>
<SelectField func={changedTextFieldSelect}
emptyField={false}
textBoxFieldSelected={'1'}
header={'Selecione um template'}
currencies={templates.map((template,) => {
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 <div key={text}>
{type && <strong style={titleCss}>{type}</strong>}
{format && format !== 'TEXT' && <p style={valueCss}>TYPE {format}</p>}
{text &&
<div style={{ margin: 0, padding: 0, 'marginBottom': '15px' }}>
<p style={{ margin: 0, padding: 0, fontSize: 12 }}>{text}</p>
{type && (type === 'BODY') && dinamicTextField(body_params.length, handleTextChange, type, text, language)}
</div>}
{buttons && <div>{buttons.map((b) => {
const { type, text, url } = b
return <div style={{ margin: 0, padding: 0, 'marginBottom': '10px' }}>
{type && <p style={valueCssButton}>TYPE {type}</p>}
{text && <p style={valueCssButton}>{text}</p>}
{url && <p style={valueCssButton}>{url}</p>}
</div>
})} </div>}
</div>
})
}
</>
</DialogContent>
<DialogActions>
<div style={{ marginRight: '50px' }}>
<Button onClick={handleCancel}>Cancelar</Button>
</div>
<Button onClick={handleChatEnd}>Ok</Button>
</DialogActions>
</Dialog >
)
}
export default ModalTemplate

View File

@ -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 { useHistory, useParams } from "react-router-dom"
import { parseISO, format, isSameDay } from "date-fns"; import { parseISO, format, isSameDay } from "date-fns"
import clsx from "clsx"; import clsx from "clsx"
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles"
import { green } from "@material-ui/core/colors"; import { green } from "@material-ui/core/colors"
import ListItem from "@material-ui/core/ListItem"; import ListItem from "@material-ui/core/ListItem"
import ListItemText from "@material-ui/core/ListItemText"; import ListItemText from "@material-ui/core/ListItemText"
import ListItemAvatar from "@material-ui/core/ListItemAvatar"; import ListItemAvatar from "@material-ui/core/ListItemAvatar"
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography"
import Avatar from "@material-ui/core/Avatar"; import Avatar from "@material-ui/core/Avatar"
import Divider from "@material-ui/core/Divider"; import Divider from "@material-ui/core/Divider"
import Badge from "@material-ui/core/Badge"; import Badge from "@material-ui/core/Badge"
import { i18n } from "../../translate/i18n"; import { i18n } from "../../translate/i18n"
import api from "../../services/api"; import api from "../../services/api"
import ButtonWithSpinner from "../ButtonWithSpinner"; import ButtonWithSpinner from "../ButtonWithSpinner"
import MarkdownWrapper from "../MarkdownWrapper"; import MarkdownWrapper from "../MarkdownWrapper"
import { Tooltip } from "@material-ui/core"; import { Tooltip } from "@material-ui/core"
import { AuthContext } from "../../context/Auth/AuthContext"; import { AuthContext } from "../../context/Auth/AuthContext"
import toastError from "../../errors/toastError"; import toastError from "../../errors/toastError"
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
ticket: { ticket: {
@ -99,46 +99,46 @@ const useStyles = makeStyles(theme => ({
top: "0%", top: "0%",
left: "0%", left: "0%",
}, },
})); }))
const TicketListItem = ({ ticket }) => { const TicketListItem = ({ ticket }) => {
const classes = useStyles(); const classes = useStyles()
const history = useHistory(); const history = useHistory()
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false)
const { ticketId } = useParams(); const { ticketId } = useParams()
const isMounted = useRef(true); const isMounted = useRef(true)
const { user } = useContext(AuthContext); const { user } = useContext(AuthContext)
useEffect(() => { useEffect(() => {
return () => { return () => {
isMounted.current = false; isMounted.current = false
}; }
}, []); }, [])
const handleAcepptTicket = async id => { const handleAcepptTicket = async id => {
setLoading(true); setLoading(true)
try { try {
await api.put(`/tickets/${id}`, { await api.put(`/tickets/${id}`, {
status: "open", status: "open",
userId: user?.id, userId: user?.id,
}); })
} catch (err) { } catch (err) {
setLoading(false); setLoading(false)
toastError(err); toastError(err)
} }
if (isMounted.current) { if (isMounted.current) {
setLoading(false); setLoading(false)
} }
history.push(`/tickets/${id}`); history.push(`/tickets/${id}`)
}; }
const handleSelectTicket = id => { const handleSelectTicket = id => {
history.push(`/tickets/${id}`); history.push(`/tickets/${id}`)
}; }
return ( return (
<React.Fragment key={ticket.id}> <React.Fragment key={ticket.id}>
@ -146,8 +146,8 @@ const TicketListItem = ({ ticket }) => {
dense dense
button button
onClick={e => { onClick={e => {
if (ticket.status === "pending") return; if (ticket.status === "pending") return
handleSelectTicket(ticket.id); handleSelectTicket(ticket.id)
}} }}
selected={ticketId && +ticketId === ticket.id} selected={ticketId && +ticketId === ticket.id}
className={clsx(classes.ticket, { className={clsx(classes.ticket, {
@ -193,6 +193,7 @@ const TicketListItem = ({ ticket }) => {
variant="body2" variant="body2"
color="textSecondary" color="textSecondary"
> >
{ticket?.phoneNumberId && <span style={{ 'fontWeight': 'bold' }}>Oficial</span>}{" "}
{isSameDay(parseISO(ticket.updatedAt), new Date()) ? ( {isSameDay(parseISO(ticket.updatedAt), new Date()) ? (
<>{format(parseISO(ticket.updatedAt), "HH:mm")}</> <>{format(parseISO(ticket.updatedAt), "HH:mm")}</>
) : ( ) : (
@ -251,7 +252,7 @@ const TicketListItem = ({ ticket }) => {
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
</React.Fragment> </React.Fragment>
); )
}; }
export default TicketListItem; export default TicketListItem