Whaticket + Wavoip
2025

Passo a Passo

				
					WHATICKET + WAVOIP

# CRIAR CONTA E DISPOSITIVO WAVOIP
- https://www.wavoip.com/
- CUPOM: ZDG40

###### Construir integração

--- ERRATA --- PAGES > CONNECTION NÃO É NECESSÁRIO

# Back
- backend\src\database\migrations\20250124140438-add-wavoip-to-whatsapp.ts
- backend\src\models\Whatsapp.ts
- backend\src\services\TicketServices\ShowTicketService.ts
- backend\src\services\WhatsappService\CreateWhatsAppService.ts
- backend\src\services\WhatsappService\UpdateWhatsAppService.ts

# Front:

- frontend\src\components\TicketActionButtons\index.js
- frontend\src\components\WhatsAppModal\index.js

###### Link: https://app.wavoip.com/call

# Parâmetros
token= token do Dispositivo Wavoip
&phone= telefone destino da ligação
&name= nome do contato
&start_if_ready= inicia a call sem precisar clicar no botão
&available_after_call= permite que faça outra chamada após acabar
&close_after_call= fecha janela assim que encerrar a ligação
				
			

BACKEND

DATABASE

backend\src\database\migrations\20250124140438-add-wavoip-to-whatsapp.ts

				
					import { QueryInterface, DataTypes } from "sequelize";

module.exports = {
  up: (queryInterface: QueryInterface) => {
    return queryInterface.addColumn("Whatsapps", "wavoip", {
      type: DataTypes.TEXT
    });
  },

  down: (queryInterface: QueryInterface) => {
    return queryInterface.removeColumn("Whatsapps", "wavoip");
  }
};

				
			

MODELS

backend\src\models\Whatsapp.ts

				
					@Column
  wavoip: string;
				
			

SERVICES

backend\src\services\TicketServices\ShowTicketService.ts

				
					import AppError from "../../errors/AppError";
import CheckContactOpenTickets from "../../helpers/CheckContactOpenTickets";
import Ticket from "../../models/Ticket";
import User from "../../models/User";
import Whatsapp from "../../models/Whatsapp";
import ShowContactService from "../ContactServices/ShowContactService";

interface Request {
  contactId: number;
  status: string;
  userId: number;
  queueId ?: number;
  channel: string;
}

const CreateTicketService = async ({
  contactId,
  status,
  userId,
  queueId,
  channel
}: Request): Promise<Ticket> => {

  let connectionType
  
  if(channel === 'instagram' || channel === 'facebook') {
    connectionType = 'facebook'
  }

  console.log('channel', channel)
  console.log('connectionType', connectionType)

  const connection = await Whatsapp.findOne({
    where: { type: connectionType! }
  });

  if (!connection) {
    throw new Error("Connection id not found");
  }

  await CheckContactOpenTickets(contactId, connection.id);

  const { isGroup } = await ShowContactService(contactId);

  if(queueId === undefined) {
    const user = await User.findByPk(userId, { include: ["queues"]});
    queueId = user?.queues.length === 1 ? user.queues[0].id : undefined;
  }

  const newTicket = await Ticket.create({
    status,
    lastMessage: null,
    contactId,
    isGroup,
    whatsappId: connection.id
  });

  const ticket = await Ticket.findByPk(newTicket.id, { include: ["contact"] });

  if (!ticket) {
    throw new AppError("ERR_CREATING_TICKET");
  }

  return ticket;
};

export default CreateTicketService;

				
			

backend\src\services\WhatsappService\CreateWhatsAppService.ts

				
					import * as Yup from "yup";

import AppError from "../../errors/AppError";
import Whatsapp from "../../models/Whatsapp";
import AssociateWhatsappQueue from "./AssociateWhatsappQueue";

interface Request {
  name: string;
  queueIds?: number[];
  greetingMessage?: string;
  farewellMessage?: string;
  status?: string;
  isDefault?: boolean;
  wavoip?: string;
}

interface Response {
  whatsapp: Whatsapp;
  oldDefaultWhatsapp: Whatsapp | null;
}

const CreateWhatsAppService = async ({
  name,
  status = "OPENING",
  queueIds = [],
  greetingMessage,
  farewellMessage,
  isDefault = false,
  wavoip
}: Request): Promise<Response> => {
  const schema = Yup.object().shape({
    name: Yup.string()
      .required()
      .min(2)
      .test(
        "Check-name",
        "This whatsapp name is already used.",
        async value => {
          if (!value) return false;
          const nameExists = await Whatsapp.findOne({
            where: { name: value }
          });
          return !nameExists;
        }
      ),
    isDefault: Yup.boolean().required()
  });

  try {
    await schema.validate({ name, status, isDefault });
  } catch (err) {
    throw new AppError(err.message);
  }

  const whatsappFound = await Whatsapp.findOne();

  isDefault = !whatsappFound;

  let oldDefaultWhatsapp: Whatsapp | null = null;

  if (isDefault) {
    oldDefaultWhatsapp = await Whatsapp.findOne({
      where: { isDefault: true }
    });
    if (oldDefaultWhatsapp) {
      await oldDefaultWhatsapp.update({ isDefault: false });
    }
  }

  if (queueIds.length > 1 && !greetingMessage) {
    throw new AppError("ERR_WAPP_GREETING_REQUIRED");
  }

  const whatsapp = await Whatsapp.create(
    {
      name,
      status,
      greetingMessage,
      farewellMessage,
      isDefault,
      wavoip
    },
    { include: ["queues"] }
  );

  await AssociateWhatsappQueue(whatsapp, queueIds);

  return { whatsapp, oldDefaultWhatsapp };
};

export default CreateWhatsAppService;

				
			

backend\src\services\WhatsappService\UpdateWhatsAppService.ts

				
					import * as Yup from "yup";
import { Op } from "sequelize";

import AppError from "../../errors/AppError";
import Whatsapp from "../../models/Whatsapp";
import ShowWhatsAppService from "./ShowWhatsAppService";
import AssociateWhatsappQueue from "./AssociateWhatsappQueue";

interface WhatsappData {
  name?: string;
  status?: string;
  session?: string;
  isDefault?: boolean;
  greetingMessage?: string;
  farewellMessage?: string;
  queueIds?: number[];
  wavoip?: string;
}

interface Request {
  whatsappData: WhatsappData;
  whatsappId: string;
}

interface Response {
  whatsapp: Whatsapp;
  oldDefaultWhatsapp: Whatsapp | null;
}

const UpdateWhatsAppService = async ({
  whatsappData,
  whatsappId
}: Request): Promise<Response> => {
  const schema = Yup.object().shape({
    name: Yup.string().min(2),
    status: Yup.string(),
    isDefault: Yup.boolean()
  });

  const {
    name,
    status,
    isDefault,
    session,
    greetingMessage,
    farewellMessage,
    queueIds = [],
    wavoip
  } = whatsappData;

  try {
    await schema.validate({ name, status, isDefault });
  } catch (err) {
    throw new AppError(err.message);
  }

  if (queueIds.length > 1 && !greetingMessage) {
    throw new AppError("ERR_WAPP_GREETING_REQUIRED");
  }

  let oldDefaultWhatsapp: Whatsapp | null = null;

  if (isDefault) {
    oldDefaultWhatsapp = await Whatsapp.findOne({
      where: { isDefault: true, id: { [Op.not]: whatsappId } }
    });
    if (oldDefaultWhatsapp) {
      await oldDefaultWhatsapp.update({ isDefault: false });
    }
  }

  const whatsapp = await ShowWhatsAppService(whatsappId);

  await whatsapp.update({
    name,
    status,
    session,
    greetingMessage,
    farewellMessage,
    isDefault,
    wavoip
  });

  await AssociateWhatsappQueue(whatsapp, queueIds);

  return { whatsapp, oldDefaultWhatsapp };
};

export default UpdateWhatsAppService;

				
			

FRONTEND

COMPONENTS

frontend\src\components\TicketActionButtons\index.js

				
					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, Phone } 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",
		"& > *": {
			margin: 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);
		}
	};

	const handleOpenWavoipCall = () => {
		if (!ticket?.whatsapp?.wavoip || !ticket?.contact?.number) {
			toastError("Erro: Token ou número de telefone não disponível.");
			return;
		}

		const token = ticket.whatsapp.wavoip;
		const phone = ticket.contact.number.replace(/\D/g, "");
		const name = ticket.contact.name;
		const url = `https://app.wavoip.com/call?token=${token}&phone=${phone}&name=${name}&start_if_ready=true&close_after_call=true`;

		window.open(url, "wavoip", "toolbar=no,scrollbars=no,resizable=no,top=500,left=500,width=500,height=700");
	};

	return (
		<div className={classes.actionButtons}>
			<IconButton color="secondary" onClick={handleOpenWavoipCall}>
				<Phone />
			</IconButton>
			{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;

				
			

frontend\src\components\WhatsAppModal\index.js

				
					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 {
	Dialog,
	DialogContent,
	DialogTitle,
	Button,
	DialogActions,
	CircularProgress,
	TextField,
	Switch,
	FormControlLabel,
	Select,
	MenuItem,
} from "@material-ui/core";

import api from "../../services/api";
import { i18n } from "../../translate/i18n";
import toastError from "../../errors/toastError";
import QueueSelect from "../QueueSelect";

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,
	},
}));

const SessionSchema = Yup.object().shape({
	name: Yup.string()
		.min(2, "Too Short!")
		.max(50, "Too Long!")
		.required("Required"),
});

const WhatsAppModal = ({ open, onClose, whatsAppId }) => {
	const classes = useStyles();
	const initialState = {
		name: "",
		greetingMessage: "",
		farewellMessage: "",
		isDefault: false,
		wavoip: "",
	};
	const [whatsApp, setWhatsApp] = useState(initialState);
	const [selectedQueueIds, setSelectedQueueIds] = useState([]);
	const [isHubSelected, setIsHubSelected] = useState(false);
	const [availableChannels, setAvailableChannels] = useState([]);
	const [selectedChannel, setSelectedChannel] = useState("");

	// Função para buscar os canais disponíveis no hub
	const fetchChannels = async () => {
		try {
			const { data } = await api.get("/hub-channel/");
			console.log("Canais disponíveis:", data); // Adicione isso para verificar os dados recebidos
			setAvailableChannels(data);
		} catch (err) {
			toastError(err);
		}
	};

	useEffect(() => {
		console.log("selectedChannel has changed:", selectedChannel);
	}, [selectedChannel]);	

	useEffect(() => {
		const fetchSession = async () => {
			if (!whatsAppId) return;

			try {
				const { data } = await api.get(`whatsapp/${whatsAppId}`);
				setWhatsApp(data);

				const whatsQueueIds = data.queues?.map(queue => queue.id);
				setSelectedQueueIds(whatsQueueIds);
			} catch (err) {
				toastError(err);
			}
		};
		fetchSession();
	}, [whatsAppId]);

	const handleSaveWhatsApp = async values => {
		const whatsappData = { ...values, 
			queueIds: selectedQueueIds,
			type: isHubSelected ? "hub" : "whatsapp",
			...(isHubSelected ? {} : { wavoip: values.wavoip }),
		};
	
		try {
			if (isHubSelected && selectedChannel) {
				console.log("2 >>>>>>>>>>>>>>>")
				// Encontrar o objeto do canal completo baseado no ID do canal selecionado
				const selectedChannelObj = availableChannels.find(
					channel => channel.id === selectedChannel
				);
				
				if (selectedChannelObj) {
					// Enviar o objeto completo do canal
					const channels = [selectedChannelObj];
					await api.post("/hub-channel/", {
						...whatsappData,
						channels
					});
					setTimeout(() => {
						window.location.reload();
					}, 100);
				}
			} else {
				if (whatsAppId) {
					await api.put(`/whatsapp/${whatsAppId}`, whatsappData);
				} else {
					await api.post("/whatsapp", whatsappData);
				}
			}
			toast.success(i18n.t("whatsappModal.success"));
			handleClose();
		} catch (err) {
			toastError(err);
		}
	};
	

	const handleClose = () => {
		onClose();
		setWhatsApp(initialState);
		setIsHubSelected(false);
		setSelectedChannel("");
	};

	return (
		<div className={classes.root}>
			<Dialog
				open={open}
				onClose={handleClose}
				maxWidth="sm"
				fullWidth
				scroll="paper"
			>
				<DialogTitle>
					{whatsAppId
						? i18n.t("whatsappModal.title.edit")
						: i18n.t("whatsappModal.title.add")}
				</DialogTitle>
				<Formik
					initialValues={whatsApp}
					enableReinitialize={true}
					validationSchema={SessionSchema}
					onSubmit={(values, actions) => {
						setTimeout(() => {
							handleSaveWhatsApp(values);
							actions.setSubmitting(false);
						}, 400);
					}}
				>
					{({ values, touched, errors, isSubmitting }) => (
						<Form>
							<DialogContent dividers>
								<div className={classes.multFieldLine}>
									<Field
										as={TextField}
										label={i18n.t("whatsappModal.form.name")}
										autoFocus
										name="name"
										error={touched.name && Boolean(errors.name)}
										helperText={touched.name && errors.name}
										variant="outlined"
										margin="dense"
										className={classes.textField}
									/>
									<FormControlLabel
										control={
											<Switch
												checked={isHubSelected}
												onChange={() => {
													setIsHubSelected(prev => !prev);
													if (!isHubSelected) {
														fetchChannels();
													}
												}}
												color="primary"
											/>
										}
										label="Hub Notifcame"
									/>
									{!isHubSelected && (
										<>
			
											<FormControlLabel
												control={
													<Field
														as={Switch}
														color="primary"
														name="isDefault"
														checked={values.isDefault}
													/>
												}
												label={i18n.t("whatsappModal.form.default")}
											/>
										</>
									)}

								</div>

								{/* Se um hub for selecionado, mostrar lista de canais */}
								{isHubSelected && (
									<div>
										<Select
											label="Select Channel"
											fullWidth
											value={selectedChannel || ""} // Use '' como fallback para valores undefined
											onChange={e => {
												const value = e.target.value;
												setSelectedChannel(value); // Mantenha o ID selecionado para buscar o objeto completo
											}}
											displayEmpty
										>
											<MenuItem value="" disabled>
												Selecione um canal
											</MenuItem>
											{availableChannels.map(channel => (
												<MenuItem key={channel.id} value={channel.id}>
													{channel.name}
												</MenuItem>
											))}
										</Select>
									</div>
								)}

								{!isHubSelected && (
									<>
										<div>
											<Field
												as={TextField}
												label={i18n.t("queueModal.form.greetingMessage")}
												type="greetingMessage"
												multiline
												rows={5}
												fullWidth
												name="greetingMessage"
												error={
													touched.greetingMessage && Boolean(errors.greetingMessage)
												}
												helperText={
													touched.greetingMessage && errors.greetingMessage
												}
												variant="outlined"
												margin="dense"
											/>
										</div>
										<div>
											<Field
												as={TextField}
												label={i18n.t("whatsappModal.form.farewellMessage")}
												type="farewellMessage"
												multiline
												rows={5}
												fullWidth
												name="farewellMessage"
												error={
													touched.farewellMessage && Boolean(errors.farewellMessage)
												}
												helperText={
													touched.farewellMessage && errors.farewellMessage
												}
												variant="outlined"
												margin="dense"
											/>
										</div>
										<QueueSelect
											selectedQueueIds={selectedQueueIds}
											onChange={selectedIds => setSelectedQueueIds(selectedIds)}
										/>
										<div>
											<Field
												as={TextField}
												label="Wavoip"
												fullWidth
												name="wavoip"
												variant="outlined"
												margin="dense"
											/>
										</div>
									</>
								)}

							</DialogContent>
							<DialogActions>
								<Button
									onClick={handleClose}
									color="secondary"
									disabled={isSubmitting}
									variant="outlined"
								>
									{i18n.t("whatsappModal.buttons.cancel")}
								</Button>
								<Button
									type="submit"
									color="primary"
									disabled={isSubmitting}
									variant="contained"
									className={classes.btnWrapper}
								>
									{whatsAppId
										? i18n.t("whatsappModal.buttons.okEdit")
										: i18n.t("whatsappModal.buttons.okAdd")}
									{isSubmitting && (
										<CircularProgress
											size={24}
											className={classes.buttonProgress}
										/>
									)}
								</Button>
							</DialogActions>
						</Form>
					)}
				</Formik>
			</Dialog>
		</div>
	);
};

export default React.memo(WhatsAppModal);

				
			

Obtenha suporte para instalação na comunidade ZDG

 

🔗 Wavoip

🧩 Ligações por Whatsapp

🧩 Ilimitado

🧩 Whaticket

Conheça alguns dos alunos da melhor comunidade de marketing de conversa do Brasil

E entenda porque tanta gente está economizando tempo e ganhando dinheiro explorando robôs e automações, mesmo sem nunca antes ter tido contato com uma API.

"O Pedrinho pega na nossa mão. Se eu consegui, você também consegue."

"Eu sou desenvolvedor de sistemas, e venho utilizando as soluções do Pedrinho para integrar nos meus sistemas, e o ganho de tempo é excepcional."

"O Pedrinho tem uma didática excelente e com o curso dele, consegui colocar minha API para rodar 24 horas e estou fazendo vendas todos os dias."

"A estratégia mais eficiente e totalmente escalável."

Comunidade ZDG © 2023 | CNPJ: 35.617.749/0001-67 | Razão Social: BIANCA SANT ANA PEREIRA 10398514607
Rua Alaor Ferreira da Fonseca, 295, CEP 37.136-132, Alfenas – MG | Tel: (35) 9 8875-4197 | E-mail: [email protected] | Política de Privacidade | Termos de Uso
Art. 49 do Código de Defesa do Consumidor | GARANTIA TOTAL DE 7 DIAS | Este produto não garante a obtenção de resultados. Qualquer referência ao desempenho de uma estratégia não deve ser interpretada como uma garantia de resultados.