/* ============================================================ CRM — conexão do WhatsApp e componentes compartilhados ============================================================ */ let crmMetaSdkPromise = null; function loadCrmMetaSdk(appId, graphVersion) { if (window.FB) return Promise.resolve(window.FB); if (crmMetaSdkPromise) return crmMetaSdkPromise; crmMetaSdkPromise = new Promise((resolve, reject) => { const previousInit = window.fbAsyncInit; window.fbAsyncInit = function () { if (typeof previousInit === 'function') previousInit(); window.FB.init({ appId: String(appId), cookie: true, xfbml: false, version: String(graphVersion), }); resolve(window.FB); }; const existing = document.getElementById('facebook-jssdk'); if (existing) { const timer = window.setInterval(() => { if (window.FB) { window.clearInterval(timer); resolve(window.FB); } }, 100); window.setTimeout(() => { window.clearInterval(timer); if (!window.FB) reject(new Error('Não foi possível abrir a conexão.')); }, 15000); return; } const script = document.createElement('script'); script.id = 'facebook-jssdk'; script.async = true; script.defer = true; script.crossOrigin = 'anonymous'; script.src = 'https://connect.facebook.net/pt_BR/sdk.js'; script.onerror = () => reject(new Error('Não foi possível abrir a conexão.')); document.head.appendChild(script); }); return crmMetaSdkPromise; } function createEmbeddedSignupWaiter() { let cancel = () => {}; const promise = new Promise((resolve, reject) => { let finished = false; const done = (fn, value) => { if (finished) return; finished = true; window.clearTimeout(timer); window.removeEventListener('message', handler); fn(value); }; const handler = (event) => { let eventUrl; try { eventUrl = new URL(event.origin); } catch { return; } if ( eventUrl.protocol !== 'https:' || (eventUrl.hostname !== 'facebook.com' && !eventUrl.hostname.endsWith('.facebook.com')) ) return; let message = event.data; if (typeof message === 'string') { try { message = JSON.parse(message); } catch { return; } } if (!message || message.type !== 'WA_EMBEDDED_SIGNUP') return; const finishEvents = [ 'FINISH', 'FINISH_ONLY_WABA', 'FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING', ]; if (finishEvents.includes(message.event)) { const data = message.data || {}; done(resolve, { wabaId: String(data.waba_id || ''), phoneNumberId: String(data.phone_number_id || ''), businessId: String(data.business_id || ''), }); } else if (message.event === 'CANCEL' || message.event === 'ERROR') { done(reject, new Error(message.event === 'CANCEL' ? 'A conexão foi cancelada.' : 'Não foi possível concluir a conexão.')); } }; const timer = window.setTimeout( () => done(reject, new Error('A conexão expirou. Tente novamente.')), 3 * 60 * 1000 ); cancel = () => done(resolve, null); window.addEventListener('message', handler); }); return { promise, cancel: () => cancel() }; } function crmMessageWhen(value) { if (!value) return ''; const date = new Date(String(value).replace(' ', 'T')); if (Number.isNaN(date.getTime())) return String(value); return date.toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', }); } const crmMessageStatusLabels = { pending: 'Pendente', sent: 'Enviada', delivered: 'Entregue', read: 'Lida', failed: 'Falhou', received: 'Recebida', }; function CrmRequestDate(value) { if (!value) return ''; const date = new Date(String(value).replace(' ', 'T')); if (Number.isNaN(date.getTime())) return ''; return date.toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } function CrmInactiveScreen({ user }) { const [requestedAt, setRequestedAt] = React.useState(user.crmActivationRequestedAt || ''); const [requesting, setRequesting] = React.useState(false); const [error, setError] = React.useState(''); const canRequest = user.role === 'admin' && user.isPrimaryAdmin; const isReactivation = ['suspended', 'cancelled'].includes(String(user.crmModuleStatus || '')); const formattedRequestDate = CrmRequestDate(requestedAt); React.useEffect(() => { if (user.crmActivationRequestedAt) setRequestedAt(user.crmActivationRequestedAt); }, [user.crmActivationRequestedAt]); const requestActivation = async () => { if (!canRequest || requesting || requestedAt) return; setRequesting(true); setError(''); try { const result = await window.API.crm.requestActivation(); setRequestedAt(result.requestedAt || new Date().toISOString()); } catch (err) { setError(err && err.message ? err.message : 'Não foi possível enviar a solicitação. Tente novamente.'); } finally { setRequesting(false); } }; return (
CRM integrado à Agenda

Transforme conversas em atendimento organizado

Reúna o WhatsApp da empresa em uma caixa de entrada compartilhada, distribua os atendimentos para sua equipe e mantenha cada conversa conectada ao histórico do cliente.

{requestedAt ? (
Solicitação enviada {formattedRequestDate ? `Recebida em ${formattedRequestDate}. Nossa equipe entrará em contato.` : 'Nossa equipe entrará em contato para concluir a ativação.'}
) : canRequest ? ( {requesting ? 'Enviando solicitação…' : isReactivation ? 'Solicitar reativação do CRM' : 'Solicitar ativação do CRM'} ) : (
Quer utilizar o CRM na sua empresa? Solicite agora mesmo a ativação. Nossa equipe entrará em contato para concluir a habilitação.
)}
{error &&
{error}
}
Caixa de entrada
3 atendimentos
Mariana SouzaQuero confirmar meu horário…
Agora
Ricardo CostaQual o próximo horário livre?
10:42
Atendimento atribuído a Ana
Olá, Mariana! Seu horário está confirmado para amanhã às 14h.
Enviado pelo WhatsApp Business

WhatsApp compartilhado

Atenda pelo CRM sem retirar o número do WhatsApp Business usado no celular.

Equipe coordenada

Atribua responsáveis e acompanhe atendimentos abertos, pendentes ou resolvidos.

Cliente e agenda juntos

Vincule a conversa ao cadastro do cliente e consulte o próximo agendamento.

01 Conecte

Integre o número oficial da empresa.

02 Distribua

Organize quem atende cada conversa.

03 Acompanhe

Mantenha histórico e status de cada contato.

); } function CrmActiveScreen({ user }) { const [payload, setPayload] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(''); const [notice, setNotice] = React.useState(''); const [busyUserId, setBusyUserId] = React.useState(0); const [savedUserId, setSavedUserId] = React.useState(0); const [channelBusy, setChannelBusy] = React.useState(''); const [messages, setMessages] = React.useState([]); const [messagesLoading, setMessagesLoading] = React.useState(false); const [messageBusy, setMessageBusy] = React.useState(false); const [messageTo, setMessageTo] = React.useState(''); const [messageBody, setMessageBody] = React.useState(''); const chatEndRef = React.useRef(null); const load = React.useCallback(async () => { setLoading(true); setError(''); try { setPayload(await window.API.crm.status()); } catch (err) { setError('Não foi possível carregar o CRM.'); } finally { setLoading(false); } }, []); React.useEffect(() => { load(); }, [load]); const refreshMessages = React.useCallback(async (silent = false) => { if (!silent) setMessagesLoading(true); try { const result = await window.API.crm.messages(100); setMessages(result.messages || []); } catch (err) { if (!silent) setError('Não foi possível carregar as mensagens.'); } finally { if (!silent) setMessagesLoading(false); } }, []); const activeConnection = payload && payload.channels && payload.channels.whatsapp; const activeConnectionId = Number(activeConnection ? activeConnection.id : 0); const activeConnectionStatus = String(activeConnection ? activeConnection.status || '' : ''); React.useEffect(() => { if (!activeConnectionId || activeConnectionStatus !== 'connected') { setMessages([]); return undefined; } refreshMessages(); const timer = window.setInterval(() => refreshMessages(true), 5000); return () => window.clearInterval(timer); }, [activeConnectionId, activeConnectionStatus, refreshMessages]); React.useEffect(() => { if (chatEndRef.current) { const messageList = chatEndRef.current.parentElement; if (messageList) messageList.scrollTop = messageList.scrollHeight; } }, [messages.length]); const updateAccess = async (targetUserId, role) => { setBusyUserId(targetUserId); setSavedUserId(0); setError(''); try { const result = await window.API.crm.setUserAccess(targetUserId, role); setPayload((current) => ({ ...(current || {}), users: result.users || [] })); setSavedUserId(targetUserId); window.setTimeout(() => setSavedUserId((current) => current === targetUserId ? 0 : current), 1800); } catch (err) { setError('Não foi possível alterar o acesso.'); } finally { setBusyUserId(0); } }; const connectWhatsApp = async () => { let signupWaiter = null; setChannelBusy('connect'); setError(''); setNotice(''); try { const start = await window.API.crm.startOnboarding('whatsapp'); const meta = start.meta || {}; const onboarding = start.onboarding || {}; const state = onboarding.state || ''; const featureType = String(meta.embeddedSignupFeatureType || ''); if ( meta.keepsMobileNumber !== true || onboarding.keepsMobileNumber !== true || onboarding.featureType !== featureType || featureType !== 'whatsapp_business_app_onboarding' ) { throw new Error('Não foi possível iniciar a conexão.'); } const FB = await loadCrmMetaSdk(meta.appId, meta.graphVersion); signupWaiter = createEmbeddedSignupWaiter(); const login = await new Promise((resolve, reject) => { FB.login((response) => { const code = response && response.authResponse && response.authResponse.code; if (code) resolve(String(code)); else reject(new Error('Não foi possível autorizar a conexão.')); }, { config_id: String(meta.configId), response_type: 'code', override_default_response_type: true, extras: { setup: {}, featureType, sessionInfoVersion: '3', }, }); }); const session = await signupWaiter.promise; if (!session) throw new Error('A conexão foi interrompida.'); const result = await window.API.crm.completeOnboarding({ state, code: login, ...session }); setPayload((current) => ({ ...(current || {}), channels: { ...((current && current.channels) || {}), whatsapp: result.connection }, })); setNotice('WhatsApp conectado. O número continua disponível no celular e também pode ser atendido pelo CRM.'); } catch (err) { if (signupWaiter) signupWaiter.cancel(); setError('Não foi possível conectar o WhatsApp. Tente novamente ou entre em contato com o suporte.'); } finally { setChannelBusy(''); } }; const verifyWhatsApp = async () => { setChannelBusy('verify'); setError(''); setNotice(''); try { const result = await window.API.crm.verifyChannel(); setPayload((current) => ({ ...(current || {}), channels: { ...((current && current.channels) || {}), whatsapp: result.connection }, })); setNotice('Conexão verificada com sucesso.'); } catch (err) { setError('Não foi possível verificar a conexão. Tente novamente.'); } finally { setChannelBusy(''); } }; const disconnectWhatsApp = async () => { const question = whatsappCoexistence ? 'Desconectar este número do CRM? Ele continuará disponível no WhatsApp Business do celular.' : 'Desconectar este número do CRM?'; if (!window.confirm(question)) return; setChannelBusy('disconnect'); setError(''); setNotice(''); try { const result = await window.API.crm.disconnectChannel(); setPayload((current) => ({ ...(current || {}), channels: { ...((current && current.channels) || {}), whatsapp: result.connection }, })); setNotice('WhatsApp desconectado do CRM.'); } catch (err) { setError('Não foi possível desconectar o WhatsApp. Tente novamente.'); } finally { setChannelBusy(''); } }; const sendOfficialMessage = async (event) => { event.preventDefault(); setMessageBusy(true); setError(''); setNotice(''); try { const result = await window.API.crm.sendMessage(messageTo, messageBody); if (result.message) { setMessages((current) => [ ...current.filter((item) => item.id !== result.message.id), result.message, ]); } setMessageBody(''); setNotice('Mensagem enviada pelo WhatsApp.'); window.setTimeout(() => refreshMessages(true), 1200); } catch (err) { setError('Não foi possível enviar a mensagem.'); } finally { setMessageBusy(false); } }; if (loading) { return (
Carregando módulo…
); } if (error && !payload) { return (
Tentar novamente
); } const crm = (payload && payload.crm) || {}; const access = crm.access || user.crmAccess || {}; const users = (payload && payload.users) || []; const whatsapp = payload && payload.channels && payload.channels.whatsapp; const whatsappConnected = whatsapp && whatsapp.status === 'connected'; const whatsappCoexistence = whatsappConnected && whatsapp.keepsMobileNumber === true; const channelSetupReady = !!crm.channelsReady; const roleLabels = { admin: 'Administrador', manager: 'Gerente', agent: 'Atendente' }; const statusLabels = { trialing: 'Período de teste', active: 'Ativo', grace: 'Período de carência' }; return (
{error &&
{error}
} {notice &&
{notice}
}
Atendimento

WhatsApp no celular e no CRM

O mesmo número permanece no WhatsApp Business do celular e passa a atender também por esta plataforma.

{statusLabels[crm.status] || 'Ativo'} Seu acesso: {roleLabels[access.role] || 'Atendente'}
WhatsApp Business {whatsappConnected ? `${whatsapp.displayName || 'Conta conectada'} · ${whatsapp.displayPhoneNumber || 'número conectado'}${whatsappCoexistence ? ' · Celular + CRM' : ''}` : 'Mesmo número no aplicativo e no CRM'}
{whatsappConnected ? 'Conectado' : whatsapp && whatsapp.status === 'error' ? 'Requer atenção' : 'Não conectado'} {access.manageChannels && (
{whatsappConnected ? ( <> {channelBusy === 'verify' ? 'Verificando…' : 'Verificar'} {channelBusy === 'disconnect' ? 'Desconectando…' : 'Desconectar'} ) : ( {channelBusy === 'connect' ? 'Abrindo conexão…' : 'Conectar WhatsApp'} )}
)}
Como funciona: use um número ativo no aplicativo WhatsApp Business atualizado. A conexão não remove o número do celular.
{whatsappConnected && !whatsappCoexistence && (
Esta conexão precisa ser refeita para manter o mesmo número no celular e no CRM.
)} {!channelSetupReady && access.manageChannels && (
A conexão do WhatsApp está temporariamente indisponível. Entre em contato com o suporte.
)}
{whatsappConnected && (

Conversas do WhatsApp

Receba e responda mensagens de atendimento em um só lugar.

refreshMessages()} disabled={messagesLoading} > {messagesLoading ? 'Atualizando…' : 'Atualizar'}
{messagesLoading && messages.length === 0 ? (
Carregando mensagens…
) : messages.length === 0 ? (
Nenhuma mensagem registrada Novas mensagens aparecerão aqui.
) : messages.map((message) => (
{message.direction === 'outbound' ? (window.APP_BRAND_NAME || 'DEL AGENDA') : (message.counterpartName || message.counterpartPhone || 'Cliente')} {crmMessageWhen(message.providerTimestamp || message.createdAt)}
{message.body}
{crmMessageStatusLabels[message.status] || message.status}
))}

Enviar mensagem de atendimento

Informe o telefone com país e DDD. Em produção, texto livre só é permitido dentro da janela de atendimento de 24 horas.

setMessageTo(event.target.value)} required /> setMessageBody(event.target.value)} required />
{messageBody.length}/4096 {messageBusy ? 'Enviando…' : 'Enviar pelo WhatsApp'}
Use somente em atendimentos esperados ou autorizados. Fora da janela de 24 horas, utilize uma mensagem previamente aprovada.
)} {access.manageUsers && (

Acessos ao CRM

Esses papéis controlam somente o CRM e não alteram as permissões da Agenda.

{users.filter((item) => item.crmEnabled).length} com acesso
{users.map((item) => (
{item.name || item.login} {item.login} · {item.agendaRole === 'admin' ? 'Administrador da Agenda' : 'Usuário da Agenda'}
updateAccess(item.id, event.target.value)} > {item.isPrimaryAdmin && Acesso obrigatório} {busyUserId === item.id && Salvando…} {savedUserId === item.id && Salvo}
))}
)}
); } function CrmScreen(props) { return props.user && props.user.hasCrm ? : ; } Object.assign(window, { CrmScreen, CrmInactiveScreen, loadCrmMetaSdk, createEmbeddedSignupWaiter, });