🏋

DUXFIT CRM

Sistema Multi-Agentes com IA

Pipeline Kanban
🔔
Status do Agente
ATIVO
Vigiando CRM e WhatsApp 24/7
Followups Hoje
0
Mensagens de cadência enviadas
Leads em Espera
0
Minerados pelo SDR-PRO

🤖 Configuração do Cérebro 360

📲 Conexão WhatsApp Web

Escaneie para vincular o WhatsApp da Academia

Aguardando Conexão

📝 Logs de Atividade do Agente

[18:15:02] Agente 360 Inicializado...
[18:15:05] Sincronizando com Pacto ERP...
[18:15:10] SDR-PRO: 12 novos leads minerados no Instagram.
[18:15:15] Aguardando comando para iniciar disparos...
Kanban
Prompt Global
Gerenciar Colunas
🎯 Meta do Dia
🛒Vendas:0/10
💲Receita:R$0/R$5000
🎫Ticket:R$0
🏁Fechamento:0%
Canal:
­ƒñû Feed dos Agentes 0
Ôû╝
Todas Conversas
Atendimento Humano
?
Selecione uma conversa
Selecione um lead para ver a conversa
Nova mensagem ↓

Leads por Dia

Funil de Vendas

Conversao Semanal

Performance dos Agentes

🔄 Fluxos de Automacao

Visualize e edite os processos automaticos dos agentes de IA

Configuracao
Prompts
Logs
Estatisticas
Follow-up
🤖 Resumo Diario IA
Carregando resumo...

📊 Leads Dia/Mes

Acompanhamento diario e mensal de leads e conversoes

Leads Hoje
-
Novos leads do dia
Leads Mes
-
Total no mes atual
Conversoes Hoje
-
Matriculados hoje
Conversoes Mes
-
Matriculados no mes
🏆 Top Leads
-
Leads com score ≥ 70
Taxa de Conversao (Mes)
Percentual de leads que se matricularam
--%

Leads por Status (Mes)

Carregando...

Leads por Fonte (Mes)

Carregando...

Leads por Dia (30 dias)

Leads de Hoje

Nome Telefone Status Origem Ultima Conversa Proximos Passos
Carregando...

🎯 Meta Diaria de Vendas

Vendas reais do Pacto ERP + CRM ÔÇö atualizado a cada 5 min

🛒 Vendas Hoje
0
meta: 10
💰 Receita
R$ 0
meta: R$ 5.000
🎫 Ticket Medio
R$ 0
📈
🏁 Faltam
0
vendas para a meta
Progresso da Meta 0%
0 Meta: 10 vendas

📊 Meta vs Realizado - Semana Atual

⚙ Configurar Metas por Dia da Semana

Pesos baseados em dados reais do Pacto (Segunda e o dia mais forte). O gerente pode ajustar.

Dia Peso Meta Vendas Meta Receita Ativo
⚙ Metas Globais (Legado)

🏆 Ranking Colaboradores

Carregando...

📋 Vendas do Periodo

Data/Hora Cliente Status Consultor
Carregando...
💬 Follow-up Inteligente por Categoria
Carregando...

🏆 Metas por Vendedora

Carregando...
⚙ Configurar Metas Individuais

📈 Evolucao Mensal de Matriculas

📅 Calendario de Visitas

📍 Visitas de Hoje

Carregando...

🔗 Sincronizacao Pacto ERP

Carregando...

Disparador de Mensagens

Envio em massa com limite de 150/dia

ÔÜí
Intervalos Irregulares Anti-Spam
Cada disparo tem intervalo aleatorio de 2 a 20 segundos (ex: 2s, 7s, 4s, 11s, 3s...) + 15% chance de pausa extra 5-15s. Simula comportamento humano para evitar bloqueio.

🎬 Minhas Gravacoes

Grave audios, videos ou tire fotos. Os agentes IA podem enviar suas gravacoes para os leads.

🎧

Gravar Audio

Grave uma mensagem de voz personalizada

🎥

Gravar Video

Grave um video com a camera do dispositivo

📷

Tirar Foto

Tire uma foto para enviar aos leads

Upload de arquivo:

Minhas gravacoes recentes

☁ WhatsApp Cloud API

Integracao direta com Meta Graph API v21.0

Verificando...

💬 Enviar Mensagem de Teste

📈 Uso do Mes

Carregando...

🔌 Configuracao do Webhook

Webhook URL: https://crm.vendedorgpt.com.br:8443/webhook/whatsapp-cloud
Verify Token: vendedorgpt_webhook_2026
Campos: messages

📚 Guia Passo-a-Passo (Setup do Zero)

1 Criar Conta no Meta Business Suite

Acesse business.facebook.com e crie uma conta empresarial.

Use email corporativo. Anote o Business ID (15 digitos) em Business Settings > Business Info.

2 Criar App no Meta for Developers

Acesse developers.facebook.com/apps > Criar aplicativo > tipo Empresa.

Vincule a conta Business do passo 1. Anote o App ID.

3 Adicionar Produto WhatsApp ao App

No painel do App, localize o card WhatsApp e clique Configurar. Aceite os Termos de Servico.

4 Copiar Phone Number ID e WABA ID

Em WhatsApp > API Setup, copie:

  • Phone Number ID (ex: 109876543210987)
  • WABA ID - WhatsApp Business Account ID
5 Adicionar Numero de Telefone

Clique em Adicionar numero. O numero NAO pode estar no WhatsApp pessoal.

Se o numero esta no WA pessoal: Configuracoes > Conta > Excluir conta. Aguarde 5 min.

Verifique via SMS ou chamada de voz (numeros fixos aceitos).

6 Criar System User e Token Permanente

Acesse Business Settings > System Users.

  1. Adicionar > Nome: whatsapp-api-bot > Funcao: Admin
  2. Atribuir Ativos > Apps > seu App > Controle total
  3. Atribuir Ativos > Contas WhatsApp > sua WABA > Controle total
  4. Gerar Token > selecionar whatsapp_business_messaging + whatsapp_business_management
  5. COPIE O TOKEN IMEDIATAMENTE (so aparece 1 vez!)
7 Configurar Credenciais no CRM

Informe ao administrador os valores para configurar no .env do backend:

WHATSAPP_CLOUD_TOKEN=EAAxxxxxxx...
WHATSAPP_CLOUD_PHONE_ID=109876543210987
WHATSAPP_CLOUD_WABA_ID=102345678901234
META_APP_SECRET=abc123def456...

Apos configurar, reinicie o backend e clique Atualizar acima.

8 Configurar Webhook no Meta

No App > WhatsApp > Configuracao > Webhooks:

  1. Clique em Editar
  2. Cole a URL do Webhook mostrada acima
  3. Cole o Verify Token: vendedorgpt_webhook_2026
  4. Clique Verificar e salvar
  5. Marque o campo messages
9 Verificacao de Negocio (Opcional)

Em Security Center > Verificacao de negocio.

Sem verificacao: limite 250 conversas/24h. Com: ate ilimitado.

Docs: Cartao CNPJ, alvara ou extrato bancario. Prazo: 1-7 dias uteis.

📈 Comparativo: API Oficial vs Evolution

API Oficial (Meta)Evolution API
Aprovacao MetaSimNao precisa
Risco de BanNenhumAlto
EstabilidadeSLA MetaPode cair
Templates HSMSimNao
Custo$0.004-$0.13/msgGratis
ConexaoNumero dedicadoQR Code (WA Web)

📷 Instagram / Meta

Integracao com Instagram DMs, Comentarios e Messenger

Status da Conexao

Carregando...

Configuracoes

Leads Instagram
0
DMs Recebidos
0
Respostas IA
0
Tokens Usados
0

🔑 Access Token

⚙ Trocar por Token Permanente (Long-Lived)

Informe App ID e App Secret para converter o token em permanente. Encontre em Meta Developers.

Webhook URL

Configure esta URL no Meta Developers > Webhooks > Instagram para receber DMs e comentarios

💬 Campanhas Comment-to-DM

Envie DMs automaticamente quando alguem comentar com uma palavra-chave

DMs Enviados
0
Leads Capturados
0
Campanhas Ativas
0
Carregando campanhas...

🔗 Webhook para Comment-to-DM

ⓘ Como configurar o Webhook no Meta
  1. Acesse Meta Developers Dashboard
  2. Selecione seu App e va em Webhooks
  3. Adicione a permissao Instagram Messaging (pages_messaging)
  4. Configure a URL do webhook acima
  5. Marque o campo comments para receber notificacoes de comentarios
  6. Salve e ative o webhook

📋 Logs de DMs Enviados

Nenhum log ainda

Posts Recentes

Carregando...

🏢 Escritorio Virtual 1 online

Multiplayer em tempo real ÔÇö ande ate alguem para conversar

🏢 Recepcao
Pipeline
💼 Comercial
Conversas
🤝 Reuniao A
Agenda
👑 Reuniao B
Relatorios
🤖 Diretoria
Agentes IA
☕ Copa
Disparador
🛋 Lounge
Colaboradores
🖥 Servidores
WhatsApp

WASD mover | [T] chat | [E] interagir | [M] mensagens | [V] video | [B] mic | [R] editar salas | [ENTER] abrir sala

Configuracoes

Configuracoes gerais do sistema CRM

Base de Clientes

Clientes Pacto + CRM ÔÇö ativos, inativos, leads, visitantes

Agenda

Agendamentos de visitas, contatos, reunioes e tarefas

Equipe

Gerencie colaboradores do CRM

🏆 Gamifica├º├úo da Equipe

Ranking, badges, desafios diários e pontuação

🎯
⚔️
Desafio do Dia
Carregando...
0/0
0%
🎯
Convers├Áes
-/-
💬
Follow-ups
-/-
🔎
Qualifica├º├Áes
-/-
🌱
Leads Novos
-/-
Pontos da equipe hoje: 0
🏆 Ranking de Vendedores
Carregando...
🏅 Vitrine de Badges
Selecione um colaborador
📈 Evolu├º├úo de Pontos
📚 Sistema de Pontua├º├úo
🎯
Qualificação
+5 pts
📅
Visita Agendada
+10 pts
Matrícula
+50 pts
💬
Follow-up
+2 pts
🤝
Handoff Convertido
+25 pts
🌟
Desafio Diário
+20 pts

Log de Atividades

Historico de acoes dos colaboradores no CRM

🎯

Funil de Vendas

Gerencie o pipeline, mova leads entre etapas e analise taxas de conversao

Perguntas rapidas
🎯

Agente Funil de Vendas

Pergunte sobre o pipeline, mova leads ou analise conversoes

📊

Analisador

Analise metricas, compare periodos e receba insights para melhorar resultados

Perguntas rapidas
📊

Agente Analisador

Pergunte sobre metricas, performance e tendencias do CRM

🕵

Analise de Leads

Analisa todos os leads do CRM ÔÇö antigos e novos. Score de engajamento, leads recuperaveis, enriquecimento via BigData API

Acoes rapidas
🕵

Agente Analisador de Leads

Analise profunda de todos os leads, score de engajamento, leads recuperaveis e integração com BigData API

🗃

Base de Clientes

Banco de dados centralizado de clientes. Busque, salve notas, veja fichas completas e atualize cadastros

Acoes rapidas
🗃

Base de Clientes

Banco de dados centralizado ÔÇö qualquer agente pode buscar informacoes aqui. Salve notas, preferencias e observacoes sobre seus clientes.

🗨

Analise de Conversas

Analisa TODAS as conversas do CRM. Identifica padroes, perguntas frequentes, performance de agentes e leads sem resposta

Acoes rapidas
🗨

Analisador de Conversas

Analise profunda de todas as conversas ÔÇö antigas e novas. Identifique padroes, melhore prompts e encontre oportunidades perdidas.

🔗

Integrador Pacto

Faz a ponte entre o CRM e o sistema Pacto ERP. Consulta, sincroniza leads e verifica status da integracao

🔒 Exclusivo do Gerente
Acoes rapidas
🔗

Integrador Pacto ERP

Ponte entre o CRM e o Pacto. Consulte dados, sincronize leads e verifique a integracao com o sistema de gestao.

💲 Parcelas Vencidas em Aberto

Clientes com contratos vencidos ÔÇö o que falta para voltar

Total Clientes
-
com parcelas em aberto
Valor em Aberto
-
receita a recuperar
Media Dias Vencido
-
dias desde vencimento
Com Analise IA
-
perfis analisados
FAIXA:
Cliente Plano Venceu Dias Valor Acessos Status CRM Responsavel Acoes
Clique em Atualizar para carregar

👥 Clientes Pacto

Gestao completa de clientes do Pacto ERP
🏭 Framework IDIC ÔÇö Relacionamento com Cliente
🔍
IDENTIFICAR
Conhecer cada aluno individualmente ÔÇö nome, objetivo, restricoes, preferencias
📊
DIFERENCIAR
Segmentar por valor (VIP, 50+, Iniciante) e necessidades especificas
💬
INTERAGIR
Contato personalizado: WA, audio, aniversario, follow-up automatico
🤝
CUSTOMIZAR
Treino adaptado, plano exclusivo, dia especial 50+, aula personalizada
💰 Previsibilidade
Renovacoes monitoradas
🌟 Personalizacao
50+ e aniversariantes
⚡ Acao Estrategica
Disparos e sequencias
Total
--
Ativos
--
Inativos
--
Desistentes
--
Visitantes
--
Pagina 1 de 1
Nome ⇅Matricula ⇅Situacao ⇅Contrato ⇅TelefoneEmailEmpresaAcoes
Clique em Atualizar para carregar

🧠 Inteligencia IA ÔÇö Leads & Clientes

Classificacao automatica 24/7 ÔÇö ZERO tokens IA
Total Leads Ativos
--
Hot Leads
--
Clientes em Risco
--
Score Medio
--
🔥 TOP HOT LEADS
Clique em Atualizar
🚨 CLIENTES EM RISCO
Clique em Atualizar
🎯 DISTRIBUICAO DE OBJETIVOS DOS LEADS
Carregando...
📊 SEGMENTOS DETALHADOS
Carregando...

🔥 Hot Leads

Leads com maior probabilidade de conversao
Clique em Atualizar

🚨 Clientes em Risco

Clientes com risco de cancelamento ÔÇö acao urgente
Clique em Atualizar

📄 Planos DuxFit

Planos e modalidades do Pacto ERP com contagem de clientes

Clique em Atualizar para carregar

💰 Financeiro DuxFit

Analise financeira baseada nos dados do Pacto ERP
Carregando analise financeira...

🏋 Treino Pacto

Fichas de treino e planos do Pacto ERP

Clique em Atualizar para carregar

📅 Agenda Pacto

Horarios e eventos do Pacto ERP

Clique em Atualizar para carregar

📋 Avaliacao Fisica

Fichas de avaliacao fisica do Pacto ERP

Clique em Atualizar para carregar

📝 Notas Pacto

Anotacoes e observacoes

Clique em Atualizar para carregar

💳 PactoPay

Pagamentos e cobrancas do Pacto

Clique em Atualizar para carregar

🔄 Renovacoes de Contratos

Contratos vencendo, renovados e taxa de retencao mensal

Clique em Atualizar para carregar

📈 Evolucao Mensal de Clientes

Panorama completo de entradas, saidas e retornos por mes
🔎

Consulta CPF

Consulte dados cadastrais, enderecos, telefones e informacoes de pessoas por CPF

🔒 Exclusivo do Gerente
Acoes rapidas
🔎

Agente BigData

Consulte CPF, busque dados cadastrais e enriqueca informacoes de leads

📧

Email Marketing

Crie e envie campanhas de email marketing pela conta duxfitacademia@gmail.com

🔒 Exclusivo do Gerente
Acoes rapidas
📧

Agente Email Marketing

Crie campanhas, envie emails individuais ou em massa

🚀

Meta Ads

Analise campanhas, crie anuncios e otimize performance no Facebook/Instagram

🔒 Exclusivo do Gerente
Acoes rapidas
🚀

Agente Meta Ads

Analise campanhas, crie copys de anuncio e otimize ROAS

Copywriting

Crie textos persuasivos para anuncios, redes sociais, WhatsApp e email

🔒 Exclusivo do Gerente
Acoes rapidas

Agente Copywriting

Crie textos persuasivos usando tecnicas AIDA, PAS e gatilhos mentais

🗣

Chat dos Agentes

Acompanhe a comunicacao entre os agentes IA e peca relatorios consolidados

🔒 Exclusivo do Gerente
Acoes rapidas
🗣

Coordenador de Agentes

Monitore a comunicacao entre agentes e receba relatorios consolidados

Operacional

Cerebro autonomo que executa e monitora todas as operacoes: sync Pacto, classificacao, follow-ups, kanban e disparos.

Operacoes
🧠

Estrategista

Agente de inteligencia. Analisa todos os dados, cruza informacoes e propoe solucoes estrategicas.

Analises estrategicas

🎯 Cacador de Leads

Encontre negocios no Google Maps, varra conversas do CRM e classifique leads

🕐 Historico de Buscas

🔍💬 Varredura de Conversas CRM

Analisa TODAS as conversas existentes e classifica leads por nivel de interesse

🕶

Agente Cacador

Varre o WhatsApp buscando conversas antigas, contatos de grupos (ativos + que sairam), importa para o CRM e classifica leads automaticamente.

Acoes rapidas
📞

Agente de Chamadas

Envie audios personalizados da biblioteca de midia para leads. Simula ligacoes com mensagens de voz.

Acoes rapidas

Biblioteca de Midia

Audios, videos e imagens para os agentes usarem

📊 Classificacao por Ultimo Contato

Distribuicao de leads, clientes ativos e inativos por engajamento no WhatsApp

🏈 Leads

-
-
leads

✨ Clientes Ativos

-
-
ativos

😴 Inativos

-
-
inativos

💬 Feedback

Avaliacoes e sugestoes dos alunos

NPS Score
--
Media Nota
--
Total Feedbacks
0
Criticos Pendentes
0

🎬 Tour Virtual 360

Cenas panoramicas da academia para impressionar leads

Total Cenas
0
Hotspots
0
Link Publico
Abrir ↗
🌎

Nenhuma cena adicionada

Clique em "+ Nova Cena 360" para comecar

Cenas

📊 Segmentacao de Clientes

Distribuicao por Score

Filtros

Mostrando todos
Carregando...

🎯 Pipeline de Relacionamento

Carregando pipeline...

⚠️ Alunos em Risco

Carregando...

📱 Grupos WhatsApp

Leads organizados por grupo de WhatsApp

Grupos
0
Contatos
0
📱

Nenhum grupo encontrado

Adicione tags aos leads para que aparecam aqui

⏰ Sequencias de Follow-up

Automacoes multi-passo via WhatsApp

Execucoes Ativas

Carregando...

📥 Receptivo

Leads em qualificacao, negociacao e follow-up — prontos para converter
-
Total Receptivos
-
Qualificando
-
Negociando
-
Follow-up
Nome Telefone Status Score Ultimo Contato Acoes
Clique em Atualizar para carregar

Performance por Agente

Ultimas Acoes

Relatorio dos Agentes

Feedbacks

Campanhas Instagram Comment-to-DM

Seguidor comenta keyword no post -> Carol envia DM automatica

Nome Keyword Mensagem DM Ativo Enviadas Leads Acoes

Ultimos Disparos

Como funciona

  1. Crie uma campanha com uma keyword (ex: "quero", "preco", "horario")
  2. Publique um post/reel no Instagram com CTA: "Comenta QUERO que mando info no Direct!"
  3. Quando um seguidor comentar com a keyword, o CRM envia a DM automatica
  4. A Carol IA continua a conversa e qualifica o lead
  5. O lead aparece no Pipeline > Instagram
'; w.document.write(h);w.document.close(); } function exportPlanosCSV(){ var ativos=window._planosAtivos||[];var inativos=window._planosInativos||[]; var rows=[['Tipo','Nome','Matricula','Telefone','Email','Contrato']]; ativos.forEach(function(c){rows.push(['ATIVO',c.nome,c.matricula,c.telefone,c.email,c.contrato]);}); inativos.forEach(function(c){rows.push(['INATIVO',c.nome,c.matricula,c.telefone,c.email,c.contrato]);}); var csv=rows.map(function(r){return r.map(function(v){return '"'+(v||'').toString().replace(/"/g,'""')+'"';}).join(',');}).join('\n'); var a=document.createElement('a');a.href='data:text/csv;charset=utf-8,\uFEFF'+encodeURIComponent(csv); a.download='planos_duxfit_'+new Date().toISOString().slice(0,10)+'.csv';a.click(); } async function loadPactoPlanos(){ const cont=document.getElementById('pactoplanosContent'); cont.innerHTML='

Carregando planos e clientes...

'; try{ const r=await fetch('/api/pacto/planos-detalhado',{headers:{'Authorization':'Bearer '+localStorage.getItem('crm_token')}}); const d=await r.json(); const planos=d.planos||[]; const ativos=d.clientes_ativos||[]; const inativos=d.clientes_inativos||[]; let html='
'; html+='
Total Planos
'+planos.length+'
'; html+='
Total Clientes
'+(d.total_clientes||0)+'
'; html+='
Ativos
'+(d.total_ativos||0)+'
'; html+='
Inativos
'+(d.total_inativos||0)+'
'; html+='
'; // Planos table html+='

Planos Cadastrados

'; html+='
'; planos.forEach(function(p){ var de=p.vigenciade?new Date(p.vigenciade).toLocaleDateString('pt-BR'):''; var ate=p.vigenciaate?new Date(p.vigenciaate).toLocaleDateString('pt-BR'):''; html+=''; }); html+='
CodPlanoVigencia DeVigencia AteBolsaEmpresaAtivos
'+(p.codigo||'')+''+(p.descricao||'')+''+de+''+ate+''+(p.bolsa?'Sim':'Nao')+''+(p.empresa||'')+''+((p.ativos_count!==null&&p.ativos_count!==undefined)?p.ativos_count:'—')+'
'; // Guardar dados para PDF/CSV window._planosAtivos=ativos; window._planosInativos=inativos; window._planosList=planos; // Clientes ativos list (completo com busca) html+='
'; html+='
'; html+='

▶ Clientes Ativos ('+ativos.length+')

'; html+=''; html+='
'; html+='
'; // Clientes inativos list (completo com busca) html+='
'; html+='
'; html+='

▶ Clientes Inativos ('+inativos.length+')

'; html+=''; html+='
'; html+='
'; cont.innerHTML=html; }catch(e){cont.innerHTML='

Erro: '+e.message+'

';} } async function syncPlanos(){ const btn=document.getElementById('btnSyncPlanos'); btn.disabled=true; btn.textContent='Iniciando...'; try{ const r=await fetch('/api/pacto/sync-planos',{method:'POST',headers:{'Authorization':'Bearer '+localStorage.getItem('crm_token')}}); const d=await r.json(); if(d.error){btn.textContent='Erro: '+d.error;setTimeout(function(){btn.disabled=false;btn.textContent='­ƒöä Sync Planos';},5000);return;} if(d.started===false){ btn.textContent='Ja em execucao...'; _pollSyncStatus(btn); return; } // started=true: background sync iniciado btn.textContent='Sync em background (~45min)...'; _pollSyncStatus(btn); }catch(e){ btn.textContent='Erro: '+e.message; setTimeout(function(){btn.disabled=false;btn.textContent='­ƒöä Sync Planos';},5000); } } function _pollSyncStatus(btn){ var interval=setInterval(async function(){ try{ const r=await fetch('/api/pacto/sync-planos/status',{headers:{'Authorization':'Bearer '+localStorage.getItem('crm_token')}}); const d=await r.json(); const s=d.state||{}; if(s.running){ const pct=s.total_ativos>0?Math.round(s.processed/s.total_ativos*100):0; btn.textContent='Sync: '+s.processed+'/'+(s.total_ativos||'?')+' ('+pct+'%) | mapeados='+s.mapped; } else { clearInterval(interval); btn.disabled=false; if(s.last_run_at){ btn.textContent='OK '+s.mapped+' mapeados | '+Math.round((s.last_run_duration||0)/60)+'min'; loadPactoPlanos(); setTimeout(function(){btn.textContent='­ƒöä Sync Planos';},8000); } else { btn.textContent='­ƒöä Sync Planos'; } } }catch(e){clearInterval(interval);btn.disabled=false;btn.textContent='­ƒöä Sync Planos';} }, 10000); } async function loadPactoAgendaPage(){loadPactoAgendaTab('horarios');} async function loadPactoAgendaTab(tab){ ['horarios','eventos','usuarios'].forEach(function(t){ var b=document.getElementById('btnAg'+t.charAt(0).toUpperCase()+t.slice(1)); if(b) b.className=t===tab?'btn btn-primary':'btn'; }); const cont=document.getElementById('pactoAgendaContent'); cont.innerHTML='

Carregando...

'; try{ var url='/api/pacto/'+tab; const r=await fetch(url,{headers:{'Authorization':'Bearer '+localStorage.getItem('crm_token')}}); if(!r.ok){cont.innerHTML='

Endpoint /api/pacto/'+tab+' nao disponivel ('+r.status+'). Use o Analista IA para consultar.

';return;} const d=await r.json(); const items=d.content||d||[]; if(!items.length){cont.innerHTML='

Nenhum resultado para '+tab+'

';return;} cont.innerHTML='
'+Object.keys(items[0]).slice(0,6).map(function(k){return '';}).join('')+''+items.slice(0,50).map(function(item){return ''+Object.keys(items[0]).slice(0,6).map(function(k){return '';}).join('')+'';}).join('')+'
'+k+'
'+(item[k]!=null?item[k]:'')+'
'; }catch(e){cont.innerHTML='

Erro: '+e.message+'

';} } async function loadPactoAvaliacao(){ const cont=document.getElementById('pactoAvaliacaoContent'); cont.innerHTML='

Carregando fichas de avaliacao...

'; try{ const r=await fetch('/api/pacto/clientes-full?page=0&size=100',{headers:{'Authorization':'Bearer '+localStorage.getItem('crm_token')}}); const d=await r.json(); const items=(d.content||d||[]).filter(function(c){return (c.situacao||'').toUpperCase()==='ATIVO';}).slice(0,50); cont.innerHTML='

Selecione um cliente ativo para ver avaliacao fisica. Use o Analista IA para consultas detalhadas.

'+items.map(function(c){return '
'+c.nome+'
Mat: '+(c.matricula||'')+' | '+(c.telefone||'')+'
';}).join('')+'
'; }catch(e){cont.innerHTML='

Erro: '+e.message+'

';} } async function loadPactoNotas(){ const cont=document.getElementById('pactoNotasContent'); cont.innerHTML='

Carregando notas...

'; try{ const r=await fetch('/api/pacto/status',{headers:{'Authorization':'Bearer '+localStorage.getItem('crm_token')}}); cont.innerHTML='

Use o campo acima para criar notas sobre o Pacto ERP. As notas sao armazenadas localmente.

'; const saved=JSON.parse(localStorage.getItem('pactoNotas')||'[]'); if(saved.length){ cont.innerHTML+='
'+saved.map(function(n,i){return '
'+n.title+'

'+n.body+'

'+n.date+'
';}).join('')+'
'; } }catch(e){cont.innerHTML='

Erro: '+e.message+'

';} } function savePactoNota(){ const title=document.getElementById('pnTitle')?.value?.trim(); const body=document.getElementById('pnBody')?.value?.trim(); if(!title){alert('Titulo obrigatorio');return;} const saved=JSON.parse(localStorage.getItem('pactoNotas')||'[]'); saved.unshift({title:title,body:body||'',date:new Date().toLocaleString('pt-BR')}); localStorage.setItem('pactoNotas',JSON.stringify(saved)); document.getElementById('pnTitle').value=''; document.getElementById('pnBody').value=''; loadPactoNotas(); } function deletePactoNota(i){ const saved=JSON.parse(localStorage.getItem('pactoNotas')||'[]'); saved.splice(i,1); localStorage.setItem('pactoNotas',JSON.stringify(saved)); loadPactoNotas(); } async function loadPactoPay(){ const cont=document.getElementById('pactoPayContent'); cont.innerHTML='

Carregando PactoPay...

'; try{ cont.innerHTML='

PactoPay

Modulo de pagamentos integrado com o Pacto ERP.

Use o Analista IA para consultar titulos, boletos e cobrancas de clientes.

'; }catch(e){cont.innerHTML='

Erro: '+e.message+'

';} } let currentEvolucaoMes = ''; let currentEvolucaoMesLabel = ''; let _evolucaoDetalheData = []; async function loadEvolucaoClientes() { const loading = document.getElementById('evolucaoLoading'); const kpis = document.getElementById('evolucaoKpis'); const empty = document.getElementById('evolucaoEmpty'); if (loading) loading.style.display = 'block'; if (kpis) kpis.style.display = 'none'; if (empty) empty.style.display = 'none'; try { const r = await fetch(API + '/pacto/evolucao-clientes', {headers: authHeaders()}); if (!r.ok) throw new Error('HTTP ' + r.status); const data = await r.json(); if (!data || !data.meses || data.meses.length === 0) { if (loading) loading.style.display = 'none'; if (empty) empty.style.display = 'block'; return; } // KPIs const ativos = data.total_ativos_hoje || 0; const entradas = data.entradas_mes_atual || 0; const saidas = data.saidas_mes_atual || 0; const retornos = data.retornos_mes_atual || 0; const variacao = entradas - saidas; const kpiAtivos = document.getElementById('kpi-ativos-hoje'); const kpiEntradas = document.getElementById('kpi-entradas-mes'); const kpiSaidas = document.getElementById('kpi-saidas-mes'); const kpiRetornos = document.getElementById('kpi-retornos-mes'); const kpiVar = document.getElementById('kpi-variacao-mes'); const trancados = data.total_trancados_hoje || 0; const aVencer = data.total_a_vencer_hoje || 0; if (kpiAtivos) kpiAtivos.textContent = ativos.toLocaleString('pt-BR'); if (kpiEntradas) kpiEntradas.textContent = entradas; if (kpiSaidas) kpiSaidas.textContent = saidas; if (kpiRetornos) kpiRetornos.textContent = retornos; if (kpiVar) { kpiVar.textContent = (variacao >= 0 ? '+' : '') + variacao; kpiVar.style.color = variacao >= 0 ? '#10b981' : '#ef4444'; } const kpiTranc = document.getElementById('kpi-trancados-hoje'); const kpiAVencer = document.getElementById('kpi-avencer-hoje'); if (kpiTranc) kpiTranc.textContent = trancados; if (kpiAVencer) kpiAVencer.textContent = aVencer; // Set current month for KPI click handlers const lastMes = data.meses[data.meses.length - 1]; if (lastMes) { currentEvolucaoMes = lastMes.mes; currentEvolucaoMesLabel = lastMes.mes_label; } // Preparar dados dos graficos (ultimos 24 meses) const meses = data.meses.slice(-24); const labels = meses.map(m => m.mes_label); const totais = meses.map(m => m.total_ativo); const entradasArr = meses.map(m => m.entradas); const saidasArr = meses.map(m => m.saidas); const retornosArr = meses.map(m => m.retornos); const chartOpts = { responsive: true, maintainAspectRatio: true, plugins: { legend: { labels: { color: '#94a3b8', font: { size: 11 } } } }, scales: { x: { ticks: { color: '#64748b', font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' } }, y: { ticks: { color: '#64748b', font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' } } } }; // Destruir charts anteriores se existirem ['chartEvolucaoTotal', 'chartEntradasSaidas', 'chartDiasMes'].forEach(id => { const existing = Chart.getChart(id); if (existing) existing.destroy(); }); // Chart 1: Evolucao Total (line) const ctx1 = document.getElementById('chartEvolucaoTotal'); if (ctx1) { new Chart(ctx1, { type: 'line', data: { labels, datasets: [{ label: 'Total Ativos', data: totais, borderColor: '#10b981', backgroundColor: 'rgba(16,185,129,.1)', fill: true, tension: 0.4, pointRadius: 3, pointBackgroundColor: '#10b981' }] }, options: { ...chartOpts } }); } // Chart 2: Entradas vs Saidas (grouped bar + retornos line) const ctx2 = document.getElementById('chartEntradasSaidas'); if (ctx2) { new Chart(ctx2, { type: 'bar', data: { labels, datasets: [ { label: 'Entradas', data: entradasArr, backgroundColor: 'rgba(59,130,246,.7)', borderColor: '#3b82f6', borderWidth: 1 }, { label: 'Saidas', data: saidasArr, backgroundColor: 'rgba(239,68,68,.7)', borderColor: '#ef4444', borderWidth: 1 }, { label: 'Retornos', data: retornosArr, type: 'line', borderColor: '#a855f7', backgroundColor: 'transparent', tension: 0.4, pointRadius: 3, pointBackgroundColor: '#a855f7' } ] }, options: { ...chartOpts } }); } // Chart 3: Dias do mes atual const ctx3 = document.getElementById('chartDiasMes'); if (ctx3 && data.dias_mes_atual && data.dias_mes_atual.length > 0) { const dias = data.dias_mes_atual; new Chart(ctx3, { type: 'bar', data: { labels: dias.map(d => d.dia_label), datasets: [{ label: 'Entradas por Dia', data: dias.map(d => d.entradas), backgroundColor: 'rgba(59,130,246,.6)', borderColor: '#3b82f6', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#64748b', font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' } }, y: { ticks: { color: '#64748b', font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' }, beginAtZero: true } } } }); } // Tabela historico (24 meses mais recentes, ordem decrescente) const tbody = document.getElementById('evolucaoTableBody'); if (tbody) { const mesesTabela = [...data.meses].reverse().slice(0, 24); tbody.innerHTML = mesesTabela.map(m => { const varColor = m.variacao >= 0 ? '#10b981' : '#ef4444'; const varSign = m.variacao >= 0 ? '+' : ''; const rowStyle = m.variacao < 0 ? 'background:rgba(239,68,68,.04)' : ''; const cs = 'cursor:pointer;text-decoration:underline dotted'; return ` ${m.mes_label} ${m.entradas} ${m.saidas} ${m.retornos} ${m.trancamentos || 0} ${m.a_vencer || 0} ${varSign}${m.variacao} ${m.total_ativo.toLocaleString('pt-BR')} `; }).join(''); } if (loading) loading.style.display = 'none'; if (kpis) kpis.style.display = 'block'; } catch (err) { console.error('Erro loadEvolucaoClientes:', err); if (loading) loading.style.display = 'none'; if (empty) { empty.style.display = 'block'; empty.innerHTML = '

Erro ao carregar dados: ' + err.message + '

'; } } } // ==================== EXPORT EVOLUCAO PDF ==================== function exportEvolucaoPDF(btn) { if (typeof window.jspdf === 'undefined') { alert('Aguarde o carregamento da biblioteca PDF...'); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF('landscape', 'mm', 'a4'); // Title doc.setFontSize(18); doc.setTextColor(13, 122, 95); doc.text('Evolucao Mensal de Clientes - DuxFit', 14, 20); // Date doc.setFontSize(10); doc.setTextColor(100, 100, 100); const now = new Date(); doc.text('Gerado em: ' + now.toLocaleDateString('pt-BR') + ' as ' + now.toLocaleTimeString('pt-BR'), 14, 28); // KPIs const kpiData = [ ['Total Ativos Hoje', document.getElementById('kpi-ativos-hoje')?.textContent || '-'], ['Entradas Este Mes', document.getElementById('kpi-entradas-mes')?.textContent || '-'], ['Saidas Este Mes', document.getElementById('kpi-saidas-mes')?.textContent || '-'], ['Retornos Este Mes', document.getElementById('kpi-retornos-mes')?.textContent || '-'], ['Variacao Liquida', document.getElementById('kpi-variacao-mes')?.textContent || '-'], ['Trancados (Pausados)', document.getElementById('kpi-trancados-hoje')?.textContent || '-'], ['A Vencer Este Mes', document.getElementById('kpi-avencer-hoje')?.textContent || '-'] ]; doc.autoTable({ startY: 34, head: [['Indicador', 'Valor']], body: kpiData, theme: 'grid', headStyles: { fillColor: [13, 122, 95], textColor: 255, fontStyle: 'bold', fontSize: 10 }, bodyStyles: { fontSize: 10 }, columnStyles: { 0: { fontStyle: 'bold', cellWidth: 60 }, 1: { halign: 'center', cellWidth: 40 } }, margin: { left: 14 }, tableWidth: 100 }); // Table - extract from DOM const tbody = document.getElementById('evolucaoTableBody'); if (tbody) { const rows = []; tbody.querySelectorAll('tr').forEach(tr => { const cells = tr.querySelectorAll('td'); if (cells.length >= 8) { rows.push([ cells[0].textContent.trim(), cells[1].textContent.trim(), cells[2].textContent.trim(), cells[3].textContent.trim(), cells[4].textContent.trim(), cells[5].textContent.trim(), cells[6].textContent.trim(), cells[7].textContent.trim() ]); } }); if (rows.length > 0) { const tableStartY = doc.lastAutoTable ? doc.lastAutoTable.finalY + 10 : 80; doc.setFontSize(12); doc.setTextColor(50, 50, 50); doc.text('Historico Mensal (ultimos 24 meses)', 14, tableStartY); doc.autoTable({ startY: tableStartY + 4, head: [['Mes', 'Entradas', 'Saidas', 'Retornos', 'Trancados', 'A Vencer', 'Variacao', 'Total Ativo']], body: rows, theme: 'striped', headStyles: { fillColor: [13, 122, 95], textColor: 255, fontStyle: 'bold', fontSize: 9 }, bodyStyles: { fontSize: 9 }, columnStyles: { 0: { fontStyle: 'bold' }, 1: { halign: 'right', textColor: [59, 130, 246] }, 2: { halign: 'right', textColor: [239, 68, 68] }, 3: { halign: 'right', textColor: [168, 85, 247] }, 4: { halign: 'right', textColor: [245, 158, 11] }, 5: { halign: 'right', textColor: [249, 115, 22] }, 6: { halign: 'right' }, 7: { halign: 'right', fontStyle: 'bold' } }, didParseCell: function(data) { if (data.column.index === 6 && data.section === 'body') { const val = parseInt(data.cell.raw); data.cell.styles.textColor = val >= 0 ? [16, 185, 129] : [239, 68, 68]; } }, margin: { left: 14, right: 14 } }); } } // Footer const pageCount = doc.internal.getNumberOfPages(); for (let i = 1; i <= pageCount; i++) { doc.setPage(i); doc.setFontSize(8); doc.setTextColor(150, 150, 150); doc.text('DuxFit Academia - CRM VendedorGPT | Pagina ' + i + '/' + pageCount, 14, doc.internal.pageSize.height - 10); } doc.save('Evolucao_Mensal_DuxFit_' + now.toISOString().slice(0,10) + '.pdf'); } // ==================== EVOLUCAO DETALHE FUNCTIONS ==================== async function abrirEvolucaoDetalhe(mes, tipo, label) { const modal = document.getElementById('modalEvolucaoDetalhe'); const title = document.getElementById('evolucaoDetalheTitle'); const count = document.getElementById('evolucaoDetalheCount'); const loading = document.getElementById('evolucaoDetalheLoading'); const contentDiv = document.getElementById('evolucaoDetalheContent'); const tbody = document.getElementById('evolucaoDetalheTbody'); const search = document.getElementById('evolucaoDetalheSearch'); const thData = document.getElementById('thDataEvolucao'); if (!modal) return; const tipoLabels = { 'entradas': 'Entradas', 'saidas': 'Saidas', 'retornos': 'Retornos', 'ativos': 'Ativos', 'trancados': 'Trancados', 'avencer': 'A Vencer' }; const tipoColors = { 'entradas': '#3b82f6', 'saidas': '#ef4444', 'retornos': '#a855f7', 'ativos': '#10b981', 'trancados': '#f59e0b', 'avencer': '#f97316' }; title.textContent = (tipoLabels[tipo] || tipo) + ' - ' + (label || mes); title.style.color = tipoColors[tipo] || '#e2e8f0'; count.textContent = 'Carregando...'; loading.style.display = 'block'; loading.innerHTML = '⏳ Carregando...'; contentDiv.style.display = 'none'; if (search) search.value = ''; modal.style.display = 'block'; let url; if (['ativos', 'trancados', 'avencer'].includes(tipo)) { url = API + '/pacto/evolucao-clientes/detalhe?tipo=' + tipo; } else { url = API + '/pacto/evolucao-clientes/mes/' + mes + '/' + tipo; } try { const r = await fetch(url, { headers: authHeaders() }); if (!r.ok) throw new Error('HTTP ' + r.status); const data = await r.json(); const clientes = data.clientes || []; _evolucaoDetalheData = clientes; count.textContent = clientes.length + ' cliente(s)'; if (thData) { if (tipo === 'saidas') thData.textContent = 'Data Saida'; else if (tipo === 'retornos') thData.textContent = 'Data Retorno'; else if (tipo === 'avencer') thData.textContent = 'Data Fim'; else thData.textContent = 'Data Entrada'; } renderEvolucaoDetalhe(clientes); loading.style.display = 'none'; contentDiv.style.display = 'block'; } catch (err) { console.error('Erro abrirEvolucaoDetalhe:', err); loading.innerHTML = 'Erro: ' + err.message + ''; } } function renderEvolucaoDetalhe(clientes) { const tbody = document.getElementById('evolucaoDetalheTbody'); if (!tbody) return; if (!clientes || clientes.length === 0) { tbody.innerHTML = 'Nenhum cliente encontrado'; return; } let html = ''; clientes.forEach(function(c, i) { const dataCol = c.data_retorno || c.data_saida || c.data_entrada || c.data_fim || ''; const tel = c.telefone || ''; const telFormatted = tel.length >= 10 ? '(' + tel.substring(0,2) + ') ' + tel.substring(2) : tel; html += ''; html += '' + (i+1) + ''; html += '' + (c.nome || '-') + ''; html += '' + telFormatted + ''; html += '' + dataCol + ''; html += '' + (c.situacao || '-') + ''; html += '' + (c.plano || '-') + ''; html += ''; }); tbody.innerHTML = html; } function getSitColor(sit) { const s = (sit || '').toUpperCase(); if (s === 'ATIVO') return 'rgba(16,185,129,.2);color:#10b981'; if (s === 'INATIVO') return 'rgba(239,68,68,.2);color:#ef4444'; if (s === 'TRANCADO') return 'rgba(245,158,11,.2);color:#f59e0b'; if (s === 'DESISTENTE' || s === 'CANCELADO') return 'rgba(239,68,68,.2);color:#ef4444'; if (s === 'VISITANTE') return 'rgba(99,102,241,.2);color:#6366f1'; return 'rgba(148,163,184,.2);color:#94a3b8'; } function filtrarEvolucaoDetalhe() { const search = (document.getElementById('evolucaoDetalheSearch')?.value || '').toLowerCase(); if (!search) { renderEvolucaoDetalhe(_evolucaoDetalheData); document.getElementById('evolucaoDetalheCount').textContent = _evolucaoDetalheData.length + ' cliente(s)'; return; } const filtered = _evolucaoDetalheData.filter(function(c) { return (c.nome || '').toLowerCase().includes(search) || (c.telefone || '').includes(search) || (c.email || '').toLowerCase().includes(search); }); renderEvolucaoDetalhe(filtered); document.getElementById('evolucaoDetalheCount').textContent = filtered.length + ' de ' + _evolucaoDetalheData.length + ' cliente(s)'; } function fecharEvolucaoDetalhe() { const modal = document.getElementById('modalEvolucaoDetalhe'); if (modal) modal.style.display = 'none'; } // ==================== META DIARIA EDIT ==================== function getMetaConfig(cat){ try{ const all=JSON.parse(localStorage.getItem('metaDiariaConfig')||'{}'); return all[cat]||{target:10,active:true,templates:[]}; }catch(e){return {target:10,active:true,templates:[]};} } function setMetaConfig(cat,cfg){ try{ const all=JSON.parse(localStorage.getItem('metaDiariaConfig')||'{}'); all[cat]=cfg; localStorage.setItem('metaDiariaConfig',JSON.stringify(all)); // Also save to backend fetch(API+'/settings',{method:'PUT',headers:{...authHeaders(),'Content-Type':'application/json'}, body:JSON.stringify({key:'meta_diaria_config',value:JSON.stringify(all)})}).catch(()=>{}); }catch(e){} } let _metaEditCat=''; function openMetaEditModal(cat){ _metaEditCat=cat; const info=META_CATEGORY_INFO[cat]||{label:cat}; const cfg=getMetaConfig(cat); document.getElementById('metaEditTitle').textContent='Editar: '+info.label; document.getElementById('metaEditTarget').value=cfg.target||10; document.getElementById('metaEditActive').checked=cfg.active!==false; document.getElementById('metaEditTemplates').value=(cfg.templates||[]).join('\n'); document.getElementById('metaEditModal').style.display='flex'; } function closeMetaEditModal(){ document.getElementById('metaEditModal').style.display='none'; } function saveMetaEdit(){ const cat=_metaEditCat; const target=parseInt(document.getElementById('metaEditTarget').value)||10; const active=document.getElementById('metaEditActive').checked; const templates=document.getElementById('metaEditTemplates').value.split('\n').filter(t=>t.trim()); setMetaConfig(cat,{target,active,templates}); closeMetaEditModal(); loadMetaDiariaStats(); } // Load saved config from backend on init (async function(){ try{ const r=await fetch(API+'/settings',{headers:authHeaders()}); if(r.ok){ const d=await r.json(); if(d.meta_diaria_config) localStorage.setItem('metaDiariaConfig',d.meta_diaria_config);// localStorage.setItem('metaDiariaConfig',d.value); } }catch(e){} })(); // ÔöÇÔöÇÔöÇÔöÇ Instagram DM Campaigns ÔöÇÔöÇÔöÇÔöÇ async function loadIgDmCampaigns(){ try{ const [campR, statsR] = await Promise.all([ api('/instagram/dm/campaigns'), api('/instagram/dm/stats') ]); const camps = campR.campaigns || []; const stats = statsR; document.getElementById('igDmStatSent').textContent = stats.messages_sent || 0; document.getElementById('igDmStatLeads').textContent = stats.leads_captured || 0; document.getElementById('igDmStatActive').textContent = stats.campaigns_active || 0; const el = document.getElementById('igDmCampaignList'); if(camps.length === 0){ el.innerHTML = '
Nenhuma campanha criada. Clique em "+ Nova Campanha" para comecar.
'; } else { el.innerHTML = camps.map(c => `
${c.name} ${c.is_active?'Ativa':'Pausada'}
Keyword: ${c.trigger_keyword}
${(c.reply_message||'').substring(0,80)}${(c.reply_message||'').length>80?'...':''}
💬 ${c.messages_sent} enviados 👤 ${c.leads_captured} leads
`).join(''); } // Logs const logs = stats.recent_logs || []; const logsEl = document.getElementById('igDmLogsTable'); if(logs.length === 0){ logsEl.innerHTML = '
Nenhum log ainda
'; } else { logsEl.innerHTML = '' + logs.map(l => ``).join('') + '
UsuarioComentarioDM EnviadaStatusData
@${l.ig_username||'?'}${(l.comment_text||'').substring(0,40)}${(l.dm_sent||'').substring(0,40)}${l.status}${l.created_at?new Date(l.created_at).toLocaleString('pt-BR'):''}
'; } }catch(e){console.error('Erro loadIgDmCampaigns:',e)} } function openIgDmModal(editData){ document.getElementById('igDmEditId').value = editData ? editData.id : ''; document.getElementById('igDmModalTitle').textContent = editData ? 'Editar Campanha' : 'Nova Campanha DM'; document.getElementById('igDmName').value = editData ? editData.name : ''; document.getElementById('igDmKeyword').value = editData ? editData.trigger_keyword : ''; document.getElementById('igDmReply').value = editData ? editData.reply_message : ''; document.getElementById('igDmMedia').value = editData ? (editData.media_url||'') : ''; document.getElementById('igDmModal').style.display = 'flex'; } function closeIgDmModal(){ document.getElementById('igDmModal').style.display = 'none'; } async function saveIgDmCampaign(){ const editId = document.getElementById('igDmEditId').value; const data = { name: document.getElementById('igDmName').value.trim(), trigger_keyword: document.getElementById('igDmKeyword').value.trim(), reply_message: document.getElementById('igDmReply').value.trim(), media_url: document.getElementById('igDmMedia').value.trim() || null, }; if(!data.name || !data.trigger_keyword || !data.reply_message){ toast('Preencha nome, keyword e mensagem','error'); return; } try{ if(editId){ await api('/instagram/dm/campaigns/'+editId, {method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data)}); toast('Campanha atualizada!'); } else { await api('/instagram/dm/campaigns', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data)}); toast('Campanha criada!'); } closeIgDmModal(); loadIgDmCampaigns(); }catch(e){toast('Erro ao salvar campanha','error')} } async function toggleIgDmCampaign(id, active){ try{ await api('/instagram/dm/campaigns/'+id, {method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({is_active:active})}); toast(active ? 'Campanha ativada!' : 'Campanha pausada!'); loadIgDmCampaigns(); }catch(e){toast('Erro','error')} } async function editIgDmCampaign(id){ try{ const r = await api('/instagram/dm/campaigns'); const camp = (r.campaigns||[]).find(c=>c.id===id); if(camp) openIgDmModal(camp); }catch(e){toast('Erro','error')} } async function deleteIgDmCampaign(id){ if(!confirm('Tem certeza que deseja excluir esta campanha?')) return; try{ await api('/instagram/dm/campaigns/'+id, {method:'DELETE'}); toast('Campanha excluida!'); loadIgDmCampaigns(); }catch(e){toast('Erro','error')} } // Hook into loadInstagram const _origLoadInstagram = typeof loadInstagram === 'function' ? loadInstagram : null; async function loadInstagramWithDM(){ if(_origLoadInstagram) await _origLoadInstagram(); await loadIgDmCampaigns(); } // Override if(typeof loadInstagram !== 'undefined'){ const _oldLI = loadInstagram; loadInstagram = async function(){ await _oldLI(); await loadIgDmCampaigns(); }; } // ==================== ENGAJAMENTO ==================== var engCharts = {leads: null, ativos: null, inativos: null}; var engCategorized = {leads: null, ativos: null, inativos: null}; function classifyByLastContact(items) { var now = new Date(); var today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); var week = new Date(today); week.setDate(today.getDate() - 7); var month = new Date(today); month.setDate(today.getDate() - 30); var groups = {hoje: [], semana: [], mes: [], antigo: [], nunca: []}; items.forEach(function(l) { var ts = l.last_contact_at || l.last_message_at; if (!ts) { groups.nunca.push(l); return; } var d = new Date(ts); if (d >= today) groups.hoje.push(l); else if (d >= week) groups.semana.push(l); else if (d >= month) groups.mes.push(l); else groups.antigo.push(l); }); return groups; } function buildEngDonut(canvasId, groups, total, oldChart) { var ctx = document.getElementById(canvasId); if (!ctx) return null; if (oldChart) { try { oldChart.destroy(); } catch(e) {} } var colors = ["#22c55e","#3b82f6","#f59e0b","#ef4444","#6b7280"]; var labels = ["Hoje","Esta semana","Este mes","Mais de 30 dias","Nunca contatado"]; var keys = ["hoje","semana","mes","antigo","nunca"]; var data = keys.map(function(k){ return groups[k].length; }); return new Chart(ctx, { type: "doughnut", data: {labels: labels, datasets: [{data: data, backgroundColor: colors, borderColor: "rgba(0,0,0,0)", borderWidth: 0, hoverOffset: 8}]}, options: { cutout: "68%", responsive: true, maintainAspectRatio: false, plugins: { legend: {display: false}, tooltip: {callbacks: {label: function(c){ return " " + c.label + ": " + c.parsed + " (" + (total ? Math.round(c.parsed/total*100) : 0) + "%)"; }}} } } }); } function buildEngLegend(containerId, groups) { var el = document.getElementById(containerId); if (!el) return; var colors = ["#22c55e","#3b82f6","#f59e0b","#ef4444","#6b7280"]; var labels = ["Hoje","Esta semana","Este mes","Mais de 30 dias","Nunca contatado"]; var keys = ["hoje","semana","mes","antigo","nunca"]; var total = keys.reduce(function(s,k){ return s + groups[k].length; }, 0); el.innerHTML = ""; keys.forEach(function(k, i) { var n = groups[k].length; var pct = total ? Math.round(n/total*100) : 0; var row = document.createElement("div"); row.style.cssText = "display:flex;align-items:center;gap:8px;font-size:11px;cursor:pointer;padding:3px 4px;border-radius:6px;transition:.15s"; row.onmouseover = function(){ this.style.background="rgba(255,255,255,0.05)"; }; row.onmouseout = function(){ this.style.background=""; }; var dot = "
"; var lbl2 = "" + labels[i] + ""; var ns = "" + n + ""; var ps = "" + pct + "%"; row.innerHTML = dot + lbl2 + ns + ps; (function(lb, gi) { row.onclick = function() { showEngDetail(lb, gi); }; })(labels[i], groups[k]); el.appendChild(row); }); } function showEngDetail(label, items) { var panel = document.getElementById("eng-detail-panel"); var title = document.getElementById("eng-detail-title"); var list = document.getElementById("eng-detail-list"); title.textContent = label + " - " + items.length + " contatos"; if (!items.length) { list.innerHTML = "

Nenhum contato neste grupo

"; } else { list.innerHTML = items.slice(0,100).map(function(l) { var ts = l.last_contact_at || l.last_message_at; var date = ts ? new Date(ts).toLocaleDateString("pt-BR") : "Nunca"; var st = l.pacto_status ? l.pacto_status.replace(/_/g," ") : (l.status || ""); return "
" + "
" + "
👤
" + "
" + (l.name || "Sem nome") + "
" + "
" + (l.phone || "") + (st ? " ┬À " + st : "") + "
" + "
Ultimo contato
" + "
" + date + "
"; }).join(""); } panel.style.display = "block"; panel.scrollIntoView({behavior:"smooth", block:"start"}); } function wireEngDonutClick(chart, catKey) { if (!chart) return; var keys = ["hoje","semana","mes","antigo","nunca"]; var labels = ["Hoje","Esta semana","Este mes","Mais de 30 dias","Nunca contatado"]; chart.options.onClick = function(evt, elements) { if (!elements.length) return; var idx = elements[0].index; var groups = engCategorized[catKey]; if (groups) showEngDetail(labels[idx], groups[keys[idx]] || []); }; chart.update(); } async function loadEngajamento() { var token = localStorage.getItem("token"); var upd = document.getElementById("engajamento-updated"); if (upd) upd.textContent = "Carregando..."; try { var res = await fetch("/api/leads?limit=5000", {headers: {Authorization: "Bearer " + token}}); var data = await res.json(); var leads = Array.isArray(data) ? data : (data.leads || data.data || []); var leadsOnly = leads.filter(function(l){ return !l.pacto_status || (l.pacto_status !== "cliente_ativo" && l.pacto_status !== "ex_aluno"); }); var ativosOnly = leads.filter(function(l){ return l.pacto_status === "cliente_ativo" || l.situacao === "ATIVO"; }); var inativosOnly = leads.filter(function(l){ return l.pacto_status === "ex_aluno" || l.situacao === "INATIVO"; }); var gLeads = classifyByLastContact(leadsOnly); var gAtivos = classifyByLastContact(ativosOnly); var gInativos = classifyByLastContact(inativosOnly); engCategorized.leads = gLeads; engCategorized.ativos = gAtivos; engCategorized.inativos = gInativos; engCharts.leads = buildEngDonut("chartEngLeads", gLeads, leadsOnly.length, engCharts.leads); engCharts.ativos = buildEngDonut("chartEngAtivos", gAtivos, ativosOnly.length, engCharts.ativos); engCharts.inativos = buildEngDonut("chartEngInativos", gInativos, inativosOnly.length, engCharts.inativos); wireEngDonutClick(engCharts.leads, "leads"); wireEngDonutClick(engCharts.ativos, "ativos"); wireEngDonutClick(engCharts.inativos, "inativos"); document.getElementById("eng-leads-num").textContent = leadsOnly.length; document.getElementById("eng-ativos-num").textContent = ativosOnly.length; document.getElementById("eng-inativos-num").textContent = inativosOnly.length; document.getElementById("eng-leads-total").textContent = "Total: " + leadsOnly.length + " leads"; document.getElementById("eng-ativos-total").textContent = "Total: " + ativosOnly.length + " ativos"; document.getElementById("eng-inativos-total").textContent = "Total: " + inativosOnly.length + " inativos"; document.getElementById("eng-leads-badge").textContent = gLeads.hoje.length + " hoje"; document.getElementById("eng-ativos-badge").textContent = gAtivos.hoje.length + " hoje"; document.getElementById("eng-inativos-badge").textContent = gInativos.hoje.length + " hoje"; buildEngLegend("eng-leads-legend", gLeads); buildEngLegend("eng-ativos-legend", gAtivos); buildEngLegend("eng-inativos-legend", gInativos); if (upd) upd.textContent = "Atualizado em " + new Date().toLocaleTimeString("pt-BR"); } catch(e) { console.error("loadEngajamento error", e); if (upd) upd.textContent = "Erro ao carregar dados"; } } // ===================================================== // LEAD SCORING & FUNIL FUNCTIONS // ===================================================== let funilChartInstance = null; let funilVisible = false; function toggleFunil() { funilVisible = !funilVisible; const sec = document.getElementById('funilSection'); if (sec) { sec.style.display = funilVisible ? 'block' : 'none'; if (funilVisible) loadFunil(); } } async function loadFunil() { try { const r = await fetch(API+'/leads/funil', {headers: authHeaders()}); const d = await r.json(); if (!d.etapas) return; const funilEl = document.getElementById('funilVisual'); const maxCount = Math.max(...d.etapas.map(e => e.count), 1); const mainEtapas = d.etapas.filter(e => !['perdido','geladeira'].includes(e.status)); const lostEtapas = d.etapas.filter(e => ['perdido','geladeira'].includes(e.status)); funilEl.innerHTML = mainEtapas.map((e, i) => { const minW = 30, maxW = 96; const w = e.count > 0 ? minW + (maxW - minW) * (e.count / maxCount) : minW; return '
' + e.label + '' + e.count + ' (' + e.percentage + '%)
'; }).join('
'); if (lostEtapas.length > 0) { funilEl.innerHTML += '
' + lostEtapas.map(e => '
' + e.label + ': ' + e.count + '
').join('') + '
'; } const taxaEl = document.getElementById('funilTaxa'); if (taxaEl) taxaEl.textContent = 'Taxa: ' + (d.taxa_conversao || 0) + '%'; const ctx = document.getElementById('chartFunilBar'); if (!ctx) return; if (funilChartInstance) funilChartInstance.destroy(); funilChartInstance = new Chart(ctx, { type: 'bar', data: { labels: d.etapas.map(e => e.label), datasets: [{ label: 'Leads', data: d.etapas.map(e => e.count), backgroundColor: d.etapas.map(e => e.color + 'cc'), borderColor: d.etapas.map(e => e.color), borderWidth: 1, borderRadius: 4, }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#94a3b8', font: { size: 10 } }, grid: { color: 'rgba(255,255,255,0.04)' } }, y: { beginAtZero: true, ticks: { color: '#94a3b8' }, grid: { color: 'rgba(255,255,255,0.04)' } } } } }); } catch(e) { console.error('Erro ao carregar funil:', e); } } function filterByFunilStatus(status) { toast('Filtrando por: ' + status.replace(/_/g,' ')); } async function recalculateScores() { try { const r = await fetch(API+'/leads/recalculate-scores', { method: 'POST', headers: authHeaders() }); const d = await r.json(); if (d.started) { toast('Recalculando scores para ' + d.total + ' leads... Aguarde 10s.'); setTimeout(() => { loadAll(); if(funilVisible) loadFunil(); }, 5000); } } catch(e) { toast('Erro ao recalcular scores', 'error'); } } function getScoreBadge(score) { const s = score || 0; if (s >= 80) return '🔥 ' + s + ''; if (s >= 60) return '⭐ ' + s + ''; if (s >= 40) return '📊 ' + s + ''; return '' + s + ''; } // Patch loadLeadsDashboard to update Top Leads KPI setTimeout(function() { const origLD = loadLeadsDashboard; window.loadLeadsDashboard = async function() { await origLD.apply(this, arguments); try { const r2 = await fetch(API+'/leads/stats', {headers: authHeaders()}); const d2 = await r2.json(); const el = document.getElementById('ldKpiTopLeads'); if (el && d2.top_leads_count !== undefined) el.textContent = d2.top_leads_count; } catch(err) {} }; }, 100); // ===================================================== // ============================================================ // Feature: Exportacao PDF/Excel // ============================================================ async function exportLeads(event, format) { const btn = event.target; const origText = btn.textContent; btn.disabled = true; btn.textContent = 'Gerando...'; try { const r = await fetch(API + '/relatorios/leads/' + format, {headers: authHeaders()}); if (!r.ok) { toast('Erro ao gerar exportacao', 'error'); return; } const blob = await r.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'leads_' + new Date().toISOString().slice(0,10) + '.' + format; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast('Download iniciado!', 'success'); } catch(e) { toast('Erro: ' + e.message, 'error'); } finally { btn.disabled = false; btn.textContent = origText; } } async function exportPipeline(event, format) { const btn = event.target; const origText = btn.textContent; btn.disabled = true; btn.textContent = 'Gerando...'; try { const r = await fetch(API + '/relatorios/pipeline/' + format, {headers: authHeaders()}); if (!r.ok) { toast('Erro ao gerar exportacao', 'error'); return; } const blob = await r.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'pipeline_' + new Date().toISOString().slice(0,10) + '.' + format; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast('Download iniciado!', 'success'); } catch(e) { toast('Erro: ' + e.message, 'error'); } finally { btn.disabled = false; btn.textContent = origText; } } // ============================================================ // Feature: Notificacoes WebSocket em Tempo Real // ============================================================ let _notifCount = 0; let _notifWs = null; function initNotifications() { if (_notifWs && _notifWs.readyState < 2) return; // already open/connecting const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = proto + '//' + location.host + '/ws/notifications'; try { _notifWs = new WebSocket(wsUrl); _notifWs.onopen = () => { console.log('[WS] Notificacoes conectadas'); // ping keepalive setInterval(() => { if (_notifWs && _notifWs.readyState === 1) _notifWs.send('ping'); }, 30000); }; _notifWs.onmessage = (e) => { try { const msg = JSON.parse(e.data); if (msg === 'pong') return; if (msg.type === 'new_lead') { showToast('Novo lead: ' + (msg.lead && msg.lead.name ? msg.lead.name : msg.lead && msg.lead.phone ? msg.lead.phone : 'Desconhecido'), 'success'); _notifCount++; updateNotifBadge(); // Reload pipeline if visible if (document.getElementById('page-pipeline') && document.getElementById('page-pipeline').classList.contains('active')) { setTimeout(loadAll, 1000); } } else if (msg.type === 'new_message') { showToast('Nova mensagem de ' + (msg.from || 'contato'), 'info'); } } catch(err) {} }; _notifWs.onclose = () => { console.log('[WS] Desconectado, reconectando em 5s...'); setTimeout(initNotifications, 5000); }; _notifWs.onerror = () => { _notifWs.close(); }; } catch(e) { console.warn('[WS] Erro ao conectar:', e); setTimeout(initNotifications, 10000); } } function updateNotifBadge() { let badge = document.getElementById('notifBadge'); if (!badge) return; if (_notifCount > 0) { badge.textContent = _notifCount > 99 ? '99+' : _notifCount; badge.style.display = 'flex'; } else { badge.style.display = 'none'; } } function showToast(message, type) { // Remove old toasts with same type document.querySelectorAll('.crm-toast-' + (type||'info')).forEach(t => t.remove()); const t = document.createElement('div'); t.className = 'crm-toast-' + (type||'info'); const bg = type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'; t.style.cssText = 'position:fixed;bottom:20px;right:20px;background:' + bg + ';color:#fff;padding:12px 20px;border-radius:8px;font-size:.9rem;z-index:9999;box-shadow:0 4px 12px rgba(0,0,0,.3);animation:crm-slideIn .3s ease;max-width:320px;word-break:break-word;cursor:pointer'; t.textContent = message; t.onclick = () => t.remove(); document.body.appendChild(t); setTimeout(() => { if (t.parentNode) t.remove(); }, 4500); } // CSS animation if (!document.getElementById('crm-toast-style')) { const st = document.createElement('style'); st.id = 'crm-toast-style'; st.textContent = '@keyframes crm-slideIn{from{transform:translateX(110%);opacity:0}to{transform:translateX(0);opacity:1}}'; document.head.appendChild(st); } // Auto-init on load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(initNotifications, 2000)); } else { setTimeout(initNotifications, 2000); } // ÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉ // TEMPLATES DE MENSAGEM // ÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉ let _allTemplates = []; let _currentTmplCat = 'todos'; const TMPL_CAT_COLORS = { abordagem: '#22c55e', follow_up: '#3b82f6', oferta: '#f59e0b', cobranca: '#ef4444', outros: '#8b5cf6' }; const TMPL_CAT_LABELS = { abordagem: 'Abordagem', follow_up: 'Follow-up', oferta: 'Oferta', cobranca: 'Cobranca', outros: 'Outros' }; async function loadTemplatesMsg() { try { const url = _currentTmplCat === 'todos' ? '/api/templates-msg' : `/api/templates-msg?category=${_currentTmplCat}`; const data = await fetch(url, {headers: authHeaders()}).then(r => r.json()); _allTemplates = Array.isArray(data) ? data : []; renderTemplatesGrid(_allTemplates); } catch(e) { console.error('loadTemplatesMsg', e); _allTemplates = []; renderTemplatesGrid([]); } } function filterTemplates(cat) { _currentTmplCat = cat; document.querySelectorAll('.tmpl-tab').forEach(t => { const isActive = t.dataset.cat === cat; t.style.background = isActive ? 'rgba(99,102,241,0.15)' : 'var(--glass-bg)'; t.style.color = isActive ? '#6366f1' : 'var(--text2)'; t.style.borderColor = isActive ? '#6366f1' : 'var(--border)'; }); loadTemplatesMsg(); } function renderTemplatesGrid(templates) { const grid = document.getElementById('templatesGrid'); if (!grid) return; if (!templates.length) { grid.innerHTML = '
📝
Nenhum template encontrado. Crie o primeiro!
'; return; } grid.innerHTML = templates.map(t => { const color = TMPL_CAT_COLORS[t.category] || '#8b5cf6'; const label = TMPL_CAT_LABELS[t.category] || t.category; const preview = t.content.length > 120 ? t.content.slice(0, 120) + '...' : t.content; const vars = Array.isArray(t.variables) ? t.variables : []; const varChips = vars.slice(0, 4).map(v => `{${v.name}}`).join(''); return `
${escHtml(t.name)} ${label}
${escHtml(preview)}
${vars.length ? `
${varChips}
` : ''}
📋 Usado ${t.use_count || 0}x
`; }).join(''); } function openTemplateModal(tmpl) { document.getElementById('templateModal').style.display = 'flex'; document.getElementById('tmplModalTitle').textContent = tmpl ? 'Editar Template' : 'Novo Template'; document.getElementById('tmplEditId').value = tmpl ? tmpl.id : ''; document.getElementById('tmplName').value = tmpl ? tmpl.name : ''; document.getElementById('tmplCategory').value = tmpl ? (tmpl.category || 'outros') : 'outros'; document.getElementById('tmplContent').value = tmpl ? tmpl.content : ''; detectTmplVars(); } function closeTemplateModal() { document.getElementById('templateModal').style.display = 'none'; } function editTemplate(jsonStr) { try { const t = JSON.parse(jsonStr); openTemplateModal(t); } catch(e) { console.error(e); } } function detectTmplVars() { const content = document.getElementById('tmplContent').value; const vars = [...new Set((content.match(/\{(\w+)\}/g) || []).map(v => v.slice(1,-1)))]; const box = document.getElementById('tmplVarsPreview'); const chips = document.getElementById('tmplVarsChips'); if (vars.length) { box.style.display = 'block'; chips.innerHTML = vars.map(v => `{${v}}`).join(''); } else { box.style.display = 'none'; } } async function saveTemplate() { const id = document.getElementById('tmplEditId').value; const name = document.getElementById('tmplName').value.trim(); const category = document.getElementById('tmplCategory').value; const content = document.getElementById('tmplContent').value.trim(); if (!name || !content) { alert('Nome e mensagem sao obrigatorios!'); return; } const method = id ? 'PUT' : 'POST'; const url = id ? `/api/templates-msg/${id}` : '/api/templates-msg'; try { const r = await fetch(url, {method, headers: authHeaders(), body: JSON.stringify({name, category, content})}); if (!r.ok) throw new Error(await r.text()); closeTemplateModal(); loadTemplatesMsg(); } catch(e) { alert('Erro ao salvar: ' + e.message); } } async function deleteTemplate(id) { if (!confirm('Deletar este template?')) return; await fetch(`/api/templates-msg/${id}`, {method: 'DELETE', headers: authHeaders()}); loadTemplatesMsg(); } async function copyTemplate(id, content) { await navigator.clipboard.writeText(content).catch(() => {}); await fetch(`/api/templates-msg/${id}/use`, {method: 'POST', headers: authHeaders()}); showToast && showToast('Template copiado!', 'success'); } // ÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉ // DISPARO EM MASSA // ÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉ let _wizardStep = 1; let _currentJobId = null; let _jobPollInterval = null; let _previewDebounce = null; async function loadDisparoJobs() { const list = document.getElementById('disparoJobsList'); if (!list) return; try { const data = await fetch('/api/whatsapp/disparo-jobs', {headers: authHeaders()}).then(r => r.json()); if (!data.length) { list.innerHTML = '
Nenhum disparo realizado ainda.
'; return; } list.innerHTML = data.map(j => { const pct = j.total ? Math.round((j.enviados / j.total) * 100) : 0; const statusColor = {pending:'#8b5cf6', running:'#f59e0b', done:'#22c55e', error:'#ef4444'}[j.status] || '#6b7280'; const statusLabel = {pending:'Aguardando', running:'Enviando...', done:'Concluido', error:'Erro'}[j.status] || j.status; return `
${escHtml((j.mensagem||'').slice(0,60))}${(j.mensagem||'').length > 60 ? '...' : ''}
${statusLabel}
Total: ${j.total} Enviados: ${j.enviados} Erros: ${j.erros} ${new Date(j.created_at).toLocaleString('pt-BR')}
`; }).join(''); } catch(e) { list.innerHTML = '
Erro ao carregar historico.
'; } } async function abrirWizardDisparo() { _wizardStep = 1; _currentJobId = null; document.getElementById('disparoWizardModal').style.display = 'flex'; document.getElementById('wFiltroStatus').value = ''; document.getElementById('wFiltroTemp').value = ''; document.getElementById('wFiltroTag').value = ''; document.getElementById('wMensagem').value = ''; document.getElementById('wDelay').value = '3'; document.getElementById('progressDisparoBox').style.display = 'none'; document.getElementById('btnDisparar').style.display = 'block'; wizardShowStep(1); previewMassa(); // Load templates into dropdown const sel = document.getElementById('wTemplateSelect'); if (sel) { sel.innerHTML = ''; _allTemplates.forEach(t => { sel.innerHTML += ``; }); } } function fecharWizardDisparo() { document.getElementById('disparoWizardModal').style.display = 'none'; if (_jobPollInterval) { clearInterval(_jobPollInterval); _jobPollInterval = null; } } function wizardShowStep(step) { _wizardStep = step; [1,2,3].forEach(s => { const el = document.getElementById(`wizardStep${s}`); if (el) el.style.display = s === step ? 'block' : 'none'; const stepEl = document.getElementById(`wstep${s}`); if (stepEl) { stepEl.style.color = s === step ? '#f59e0b' : (s < step ? '#22c55e' : 'var(--text3)'); stepEl.style.borderBottomColor = s === step ? '#f59e0b' : (s < step ? '#22c55e' : 'var(--border)'); } }); const back = document.getElementById('wizardBtnBack'); const next = document.getElementById('wizardBtnNext'); const lbl = document.getElementById('wizardStepsLabel'); if (back) back.style.display = step > 1 ? 'block' : 'none'; if (next) next.style.display = step < 3 ? 'block' : 'none'; if (lbl) lbl.textContent = `Passo ${step} de 3: ${['Selecionar Leads','Criar Mensagem','Confirmar e Disparar'][step-1]}`; } function wizardNext() { if (_wizardStep === 1) { const count = parseInt(document.getElementById('previewCount').textContent); if (!count || count === 0) { alert('Nenhum lead encontrado com esses filtros!'); return; } wizardShowStep(2); } else if (_wizardStep === 2) { const msg = document.getElementById('wMensagem').value.trim(); if (!msg) { alert('Digite a mensagem!'); return; } // Build summary const count = document.getElementById('previewCount').textContent; const delay = parseInt(document.getElementById('wDelay').value) || 3; const mins = Math.ceil((count * delay) / 60); document.getElementById('confirmarSummary').innerHTML = `
📱 ${count} leads serao impactados
⏰ Delay entre envios: ${delay} segundos
🕐 Tempo estimado: ~${mins} minutos
${escHtml(msg.slice(0,200))}${msg.length>200?'...':''}
`; wizardShowStep(3); } } function wizardBack() { if (_wizardStep > 1) wizardShowStep(_wizardStep - 1); } function previewMassa() { clearTimeout(_previewDebounce); _previewDebounce = setTimeout(async () => { const filtro = buildFiltro(); try { const r = await fetch('/api/whatsapp/preview-massa', { method: 'POST', headers: authHeaders(), body: JSON.stringify({filtro}) }); const d = await r.json(); const el = document.getElementById('previewCount'); if (el) el.textContent = d.count || 0; } catch(e) {} }, 400); } function buildFiltro() { const status = document.getElementById('wFiltroStatus')?.value; const temperature = document.getElementById('wFiltroTemp')?.value; const tagVal = document.getElementById('wFiltroTag')?.value.trim(); const filtro = {}; if (status) filtro.status = status; if (temperature) filtro.temperature = temperature; if (tagVal) filtro.tags = [tagVal]; return filtro; } function aplicarTemplateDisparo() { const sel = document.getElementById('wTemplateSelect'); if (!sel || !sel.value) return; const opt = sel.options[sel.selectedIndex]; const content = opt.getAttribute('data-content') || ''; const ta = document.getElementById('wMensagem'); if (ta) { ta.value = content; previewPersonalized(); } } function previewPersonalized() { const msg = document.getElementById('wMensagem')?.value || ''; const box = document.getElementById('previewPersonalizedBox'); const txt = document.getElementById('previewPersonalizedText'); if (!msg.trim()) { if (box) box.style.display = 'none'; return; } const preview = msg.replace('{nome}', 'Carlos').replace('{telefone}', '5511999887766') .replace('{empresa}', 'DuxFit').replace('{atendente}', 'Ana'); if (box) box.style.display = 'block'; if (txt) txt.textContent = preview; } async function executarDisparo() { const mensagem = document.getElementById('wMensagem').value.trim(); if (!mensagem) { alert('Mensagem vazia!'); return; } const filtro = buildFiltro(); const delay = parseInt(document.getElementById('wDelay').value) || 3; document.getElementById('btnDisparar').style.display = 'none'; document.getElementById('progressDisparoBox').style.display = 'block'; document.getElementById('progressText').textContent = 'Iniciando disparo...'; try { const r = await fetch('/api/whatsapp/disparo-massa', { method: 'POST', headers: authHeaders(), body: JSON.stringify({filtro, mensagem, delay_segundos: delay}) }); const d = await r.json(); if (d.error || !d.job_id) throw new Error(d.detail || 'Erro ao iniciar'); _currentJobId = d.job_id; pollDisparoJob(_currentJobId, d.total); } catch(e) { alert('Erro: ' + e.message); document.getElementById('btnDisparar').style.display = 'block'; document.getElementById('progressDisparoBox').style.display = 'none'; } } function pollDisparoJob(jobId, total) { let lastEnviados = 0; _jobPollInterval = setInterval(async () => { try { const d = await fetch(`/api/whatsapp/disparo-jobs/${jobId}`, {headers: authHeaders()}).then(r => r.json()); const pct = total ? Math.round((d.enviados / total) * 100) : 0; document.getElementById('progressBar').style.width = pct + '%'; document.getElementById('progressPct').textContent = pct + '%'; document.getElementById('progressEnviados').textContent = d.enviados; document.getElementById('progressErros').textContent = d.erros; document.getElementById('progressText').textContent = d.status === 'running' ? `Enviando... (${d.enviados}/${total})` : (d.status === 'done' ? 'Disparo concluido!' : d.status); if (d.status === 'done' || d.status === 'error') { clearInterval(_jobPollInterval); loadDisparoJobs(); if (d.status === 'done') showToast && showToast(`Disparo concluido! ${d.enviados} mensagens enviadas.`, 'success'); } } catch(e) {} }, 3000); } function escHtml(s) { return String(s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } // ============ DAILY SUMMARY (CRM) ============ async function regenerateDailySummary() { var el = document.getElementById('dsSummaryContent'); if (el) el.textContent = 'Gerando...'; try { var token = getToken(); await fetch('/api/dashboard/generate-summary', {method:'POST', headers:{'Authorization':'Bearer '+token,'Content-Type':'application/json'}}); await loadDailySummary(); } catch(e) { if (el) el.textContent = 'Erro.'; } } // ============ FOLLOWUP SEQUENCES (CRM) ============ var _seqEditId=null, _seqStartId=null, _allLeadsForSeq=[]; function _ch(s){return String(s||'').replace(/&/g,'&').replace(//g,'>');} async function _crmApi(path, opts) { try { var h = Object.assign({'Content-Type':'application/json','Authorization':'Bearer '+(getToken()||'')}, (opts&&opts.headers)||{}); var r = await fetch(path, Object.assign({}, opts||{}, {headers:h})); if (!r.ok) return null; return await r.json(); } catch(e) { return null; } } async function loadFollowupSequences() { var data = await _crmApi('/api/followup/sequences'); var el = document.getElementById('seqList'); if (!el || !data) return; el.innerHTML = !data.length ? '

Nenhuma sequencia.

' : data.map(function(s){ var st=s.steps||[]; return '
'+_ch(s.name)+''+(s.active?'Ativa':'Inativa')+''+st.length+' passo(s) | '+(s.active_executions||0)+' ativos
'+(st.length?'
'+st.map(function(p){return 'Dia '+p.dias+': '+_ch((p.mensagem||'').substring(0,40))+(p.mensagem&&p.mensagem.length>40?'...':'')+'';}).join('')+'
':'')+ '
'; }).join(''); var ex = await _crmApi('/api/followup/executions'); var exEl = document.getElementById('seqExecList'); if (exEl && ex) { exEl.innerHTML = !ex.length ? 'Nenhuma execucao ativa.' : ''+ ex.map(function(e){var nr=e.next_run?new Date(e.next_run).toLocaleString('pt-BR'):'-';return '';}).join('')+'
LeadSequenciaPassoProximo Envio
'+_ch(e.lead_name||e.lead_phone||'-')+''+_ch(e.sequence_name||'-')+''+(e.step_index+1)+''+nr+'
'; } } function openNewSeqModal(){_seqEditId=null;document.getElementById('seqModalTitle').textContent='Nova Sequencia';document.getElementById('seqName').value='';document.getElementById('seqSteps').innerHTML='';addSeqStep();document.getElementById('seqModal').style.display='flex';} function editSeq(id,s){_seqEditId=id;document.getElementById('seqModalTitle').textContent='Editar Sequencia';document.getElementById('seqName').value=s.name||'';document.getElementById('seqSteps').innerHTML='';(s.steps||[]).forEach(function(p){addSeqStep(p.dias,p.mensagem);});document.getElementById('seqModal').style.display='flex';} function closeSeqModal(){document.getElementById('seqModal').style.display='none';} function addSeqStep(dias,mensagem){var c=document.getElementById('seqSteps');var d=document.createElement('div');d.style.cssText='display:flex;gap:8px;align-items:flex-start;margin-bottom:8px;background:var(--bg3);padding:8px;border-radius:8px';d.innerHTML='
';c.appendChild(d);} async function saveSeq(){var name=document.getElementById('seqName').value.trim();if(!name){alert('Informe o nome.');return;}var els=document.getElementById('seqSteps').children;var steps=[];for(var i=0;iNenhum lead.

';return;}el.innerHTML=leads.map(function(l){return '';}).join('');} function filterSeqLeads(){var q=document.getElementById('seqLeadSearch').value.toLowerCase();renderSeqLeads(_allLeadsForSeq.filter(function(l){return(l.name||'').toLowerCase().includes(q)||(l.phone||'').includes(q);}));} function closeStartModal(){document.getElementById('seqStartModal').style.display='none';} async function startSeqExecution(){var checks=document.querySelectorAll('#seqLeadList input[type=checkbox]:checked');var ids=Array.from(checks).map(function(c){return c.value;});if(!ids.length){alert('Selecione leads.');return;}var r=await _crmApi('/api/followup/sequences/'+_seqStartId+'/start',{method:'POST',body:JSON.stringify({lead_ids:ids})});if(r){closeStartModal();loadFollowupSequences();alert(r.message||'Iniciado!');}}

📝 Templates de Mensagem WhatsApp

Biblioteca de mensagens reutilizaveis para atendimento rapido e padronizado

📝
Carregando templates...

📢 Disparo em Massa Segmentado

Envie mensagens para grupos segmentados de leads via WhatsApp

Historico de Disparos

Carregando...

🌟 Perfil 360° do Cliente

Visao completa: IDIC, historico, temperatura e relacionamento

🎂 Aniversariantes do Mes

Dia especial: aula personalizada + bombom + WhatsApp especial

Aniversariantes do Mes
--
Dia Especial Planejados
0
Realizados
0

🎉 Clientes 50+ Anos

Envio de audio personalizado via WhatsApp para clientes acima de 50 anos

Total 50+ Anos
--
Ativos
--
Selecionados
0
🎤
Envio de Audio Personalizado
Mensagem especial para clientes com 50+ anos

Campanhas Instagram Comment-to-DM

Seguidor comenta keyword no post -> Carol envia DM automatica

Nome Keyword Mensagem DM Ativo Enviadas Leads Acoes

Ultimos Disparos

Como funciona

  1. Crie uma campanha com uma keyword (ex: "quero", "preco", "horario")
  2. Publique um post/reel no Instagram com CTA: "Comenta QUERO que mando info no Direct!"
  3. Quando um seguidor comentar com a keyword, o CRM envia a DM automatica
  4. A Carol IA continua a conversa e qualifica o lead
  5. O lead aparece no Pipeline > Instagram
'; w.document.write(h);w.document.close(); } function exportPlanosCSV(){ var ativos=window._planosAtivos||[];var inativos=window._planosInativos||[]; var rows=[['Tipo','Nome','Matricula','Telefone','Email','Contrato']]; ativos.forEach(function(c){rows.push(['ATIVO',c.nome,c.matricula,c.telefone,c.email,c.contrato]);}); inativos.forEach(function(c){rows.push(['INATIVO',c.nome,c.matricula,c.telefone,c.email,c.contrato]);}); var csv=rows.map(function(r){return r.map(function(v){return '"'+(v||'').toString().replace(/"/g,'""')+'"';}).join(',');}).join('\n'); var a=document.createElement('a');a.href='data:text/csv;charset=utf-8,\uFEFF'+encodeURIComponent(csv); a.download='planos_duxfit_'+new Date().toISOString().slice(0,10)+'.csv';a.click(); } async function loadPactoPlanos(){ const cont=document.getElementById('pactoplanosContent'); cont.innerHTML='

Carregando planos e clientes...

'; try{ const r=await fetch('/api/pacto/planos-detalhado',{headers:{'Authorization':'Bearer '+localStorage.getItem('crm_token')}}); const d=await r.json(); const planos=d.planos||[]; const ativos=d.clientes_ativos||[]; const inativos=d.clientes_inativos||[]; let html='
'; html+='
Total Planos
'+planos.length+'
'; html+='
Total Clientes
'+(d.total_clientes||0)+'
'; html+='
Ativos
'+(d.total_ativos||0)+'
'; html+='
Inativos
'+(d.total_inativos||0)+'
'; html+='
'; // Planos table html+='

Planos Cadastrados

'; html+='
'; planos.forEach(function(p){ var de=p.vigenciade?new Date(p.vigenciade).toLocaleDateString('pt-BR'):''; var ate=p.vigenciaate?new Date(p.vigenciaate).toLocaleDateString('pt-BR'):''; html+=''; }); html+='
CodPlanoVigencia DeVigencia AteBolsaEmpresaAtivos
'+(p.codigo||'')+''+(p.descricao||'')+''+de+''+ate+''+(p.bolsa?'Sim':'Nao')+''+(p.empresa||'')+''+((p.ativos_count!==null&&p.ativos_count!==undefined)?p.ativos_count:'—')+'
'; // Guardar dados para PDF/CSV window._planosAtivos=ativos; window._planosInativos=inativos; window._planosList=planos; // Clientes ativos list (completo com busca) html+='
'; html+='
'; html+='

▶ Clientes Ativos ('+ativos.length+')

'; html+=''; html+='
'; html+='
'; // Clientes inativos list (completo com busca) html+='
'; html+='
'; html+='

▶ Clientes Inativos ('+inativos.length+')

'; html+=''; html+='
'; html+='
'; cont.innerHTML=html; }catch(e){cont.innerHTML='

Erro: '+e.message+'

';} } async function syncPlanos(){ const btn=document.getElementById('btnSyncPlanos'); btn.disabled=true; btn.textContent='Iniciando...'; try{ const r=await fetch('/api/pacto/sync-planos',{method:'POST',headers:{'Authorization':'Bearer '+localStorage.getItem('crm_token')}}); const d=await r.json(); if(d.error){btn.textContent='Erro: '+d.error;setTimeout(function(){btn.disabled=false;btn.textContent='­ƒöä Sync Planos';},5000);return;} if(d.started===false){ btn.textContent='Ja em execucao...'; _pollSyncStatus(btn); return; } // started=true: background sync iniciado btn.textContent='Sync em background (~45min)...'; _pollSyncStatus(btn); }catch(e){ btn.textContent='Erro: '+e.message; setTimeout(function(){btn.disabled=false;btn.textContent='­ƒöä Sync Planos';},5000); } } function _pollSyncStatus(btn){ var interval=setInterval(async function(){ try{ const r=await fetch('/api/pacto/sync-planos/status',{headers:{'Authorization':'Bearer '+localStorage.getItem('crm_token')}}); const d=await r.json(); const s=d.state||{}; if(s.running){ const pct=s.total_ativos>0?Math.round(s.processed/s.total_ativos*100):0; btn.textContent='Sync: '+s.processed+'/'+(s.total_ativos||'?')+' ('+pct+'%) | mapeados='+s.mapped; } else { clearInterval(interval); btn.disabled=false; if(s.last_run_at){ btn.textContent='OK '+s.mapped+' mapeados | '+Math.round((s.last_run_duration||0)/60)+'min'; loadPactoPlanos(); setTimeout(function(){btn.textContent='­ƒöä Sync Planos';},8000); } else { btn.textContent='­ƒöä Sync Planos'; } } }catch(e){clearInterval(interval);btn.disabled=false;btn.textContent='­ƒöä Sync Planos';} }, 10000); } async function loadPactoAgendaPage(){loadPactoAgendaTab('horarios');} async function loadPactoAgendaTab(tab){ ['horarios','eventos','usuarios'].forEach(function(t){ var b=document.getElementById('btnAg'+t.charAt(0).toUpperCase()+t.slice(1)); if(b) b.className=t===tab?'btn btn-primary':'btn'; }); const cont=document.getElementById('pactoAgendaContent'); cont.innerHTML='

Carregando...

'; try{ var url='/api/pacto/'+tab; const r=await fetch(url,{headers:{'Authorization':'Bearer '+localStorage.getItem('crm_token')}}); if(!r.ok){cont.innerHTML='

Endpoint /api/pacto/'+tab+' nao disponivel ('+r.status+'). Use o Analista IA para consultar.

';return;} const d=await r.json(); const items=d.content||d||[]; if(!items.length){cont.innerHTML='

Nenhum resultado para '+tab+'

';return;} cont.innerHTML='
'+Object.keys(items[0]).slice(0,6).map(function(k){return '';}).join('')+''+items.slice(0,50).map(function(item){return ''+Object.keys(items[0]).slice(0,6).map(function(k){return '';}).join('')+'';}).join('')+'
'+k+'
'+(item[k]!=null?item[k]:'')+'
'; }catch(e){cont.innerHTML='

Erro: '+e.message+'

';} } async function loadPactoAvaliacao(){ const cont=document.getElementById('pactoAvaliacaoContent'); cont.innerHTML='

Carregando fichas de avaliacao...

'; try{ const r=await fetch('/api/pacto/clientes-full?page=0&size=100',{headers:{'Authorization':'Bearer '+localStorage.getItem('crm_token')}}); const d=await r.json(); const items=(d.content||d||[]).filter(function(c){return (c.situacao||'').toUpperCase()==='ATIVO';}).slice(0,50); cont.innerHTML='

Selecione um cliente ativo para ver avaliacao fisica. Use o Analista IA para consultas detalhadas.

'+items.map(function(c){return '
'+c.nome+'
Mat: '+(c.matricula||'')+' | '+(c.telefone||'')+'
';}).join('')+'
'; }catch(e){cont.innerHTML='

Erro: '+e.message+'

';} } async function loadPactoNotas(){ const cont=document.getElementById('pactoNotasContent'); cont.innerHTML='

Carregando notas...

'; try{ const r=await fetch('/api/pacto/status',{headers:{'Authorization':'Bearer '+localStorage.getItem('crm_token')}}); cont.innerHTML='

Use o campo acima para criar notas sobre o Pacto ERP. As notas sao armazenadas localmente.

'; const saved=JSON.parse(localStorage.getItem('pactoNotas')||'[]'); if(saved.length){ cont.innerHTML+='
'+saved.map(function(n,i){return '
'+n.title+'

'+n.body+'

'+n.date+'
';}).join('')+'
'; } }catch(e){cont.innerHTML='

Erro: '+e.message+'

';} } function savePactoNota(){ const title=document.getElementById('pnTitle')?.value?.trim(); const body=document.getElementById('pnBody')?.value?.trim(); if(!title){alert('Titulo obrigatorio');return;} const saved=JSON.parse(localStorage.getItem('pactoNotas')||'[]'); saved.unshift({title:title,body:body||'',date:new Date().toLocaleString('pt-BR')}); localStorage.setItem('pactoNotas',JSON.stringify(saved)); document.getElementById('pnTitle').value=''; document.getElementById('pnBody').value=''; loadPactoNotas(); } function deletePactoNota(i){ const saved=JSON.parse(localStorage.getItem('pactoNotas')||'[]'); saved.splice(i,1); localStorage.setItem('pactoNotas',JSON.stringify(saved)); loadPactoNotas(); } async function loadPactoPay(){ const cont=document.getElementById('pactoPayContent'); cont.innerHTML='

Carregando PactoPay...

'; try{ cont.innerHTML='

PactoPay

Modulo de pagamentos integrado com o Pacto ERP.

Use o Analista IA para consultar titulos, boletos e cobrancas de clientes.

'; }catch(e){cont.innerHTML='

Erro: '+e.message+'

';} } let currentEvolucaoMes = ''; let currentEvolucaoMesLabel = ''; let _evolucaoDetalheData = []; async function loadEvolucaoClientes() { const loading = document.getElementById('evolucaoLoading'); const kpis = document.getElementById('evolucaoKpis'); const empty = document.getElementById('evolucaoEmpty'); if (loading) loading.style.display = 'block'; if (kpis) kpis.style.display = 'none'; if (empty) empty.style.display = 'none'; try { const r = await fetch(API + '/pacto/evolucao-clientes', {headers: authHeaders()}); if (!r.ok) throw new Error('HTTP ' + r.status); const data = await r.json(); if (!data || !data.meses || data.meses.length === 0) { if (loading) loading.style.display = 'none'; if (empty) empty.style.display = 'block'; return; } // KPIs const ativos = data.total_ativos_hoje || 0; const entradas = data.entradas_mes_atual || 0; const saidas = data.saidas_mes_atual || 0; const retornos = data.retornos_mes_atual || 0; const variacao = entradas - saidas; const kpiAtivos = document.getElementById('kpi-ativos-hoje'); const kpiEntradas = document.getElementById('kpi-entradas-mes'); const kpiSaidas = document.getElementById('kpi-saidas-mes'); const kpiRetornos = document.getElementById('kpi-retornos-mes'); const kpiVar = document.getElementById('kpi-variacao-mes'); const trancados = data.total_trancados_hoje || 0; const aVencer = data.total_a_vencer_hoje || 0; if (kpiAtivos) kpiAtivos.textContent = ativos.toLocaleString('pt-BR'); if (kpiEntradas) kpiEntradas.textContent = entradas; if (kpiSaidas) kpiSaidas.textContent = saidas; if (kpiRetornos) kpiRetornos.textContent = retornos; if (kpiVar) { kpiVar.textContent = (variacao >= 0 ? '+' : '') + variacao; kpiVar.style.color = variacao >= 0 ? '#10b981' : '#ef4444'; } const kpiTranc = document.getElementById('kpi-trancados-hoje'); const kpiAVencer = document.getElementById('kpi-avencer-hoje'); if (kpiTranc) kpiTranc.textContent = trancados; if (kpiAVencer) kpiAVencer.textContent = aVencer; // Set current month for KPI click handlers const lastMes = data.meses[data.meses.length - 1]; if (lastMes) { currentEvolucaoMes = lastMes.mes; currentEvolucaoMesLabel = lastMes.mes_label; } // Preparar dados dos graficos (ultimos 24 meses) const meses = data.meses.slice(-24); const labels = meses.map(m => m.mes_label); const totais = meses.map(m => m.total_ativo); const entradasArr = meses.map(m => m.entradas); const saidasArr = meses.map(m => m.saidas); const retornosArr = meses.map(m => m.retornos); const chartOpts = { responsive: true, maintainAspectRatio: true, plugins: { legend: { labels: { color: '#94a3b8', font: { size: 11 } } } }, scales: { x: { ticks: { color: '#64748b', font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' } }, y: { ticks: { color: '#64748b', font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' } } } }; // Destruir charts anteriores se existirem ['chartEvolucaoTotal', 'chartEntradasSaidas', 'chartDiasMes'].forEach(id => { const existing = Chart.getChart(id); if (existing) existing.destroy(); }); // Chart 1: Evolucao Total (line) const ctx1 = document.getElementById('chartEvolucaoTotal'); if (ctx1) { new Chart(ctx1, { type: 'line', data: { labels, datasets: [{ label: 'Total Ativos', data: totais, borderColor: '#10b981', backgroundColor: 'rgba(16,185,129,.1)', fill: true, tension: 0.4, pointRadius: 3, pointBackgroundColor: '#10b981' }] }, options: { ...chartOpts } }); } // Chart 2: Entradas vs Saidas (grouped bar + retornos line) const ctx2 = document.getElementById('chartEntradasSaidas'); if (ctx2) { new Chart(ctx2, { type: 'bar', data: { labels, datasets: [ { label: 'Entradas', data: entradasArr, backgroundColor: 'rgba(59,130,246,.7)', borderColor: '#3b82f6', borderWidth: 1 }, { label: 'Saidas', data: saidasArr, backgroundColor: 'rgba(239,68,68,.7)', borderColor: '#ef4444', borderWidth: 1 }, { label: 'Retornos', data: retornosArr, type: 'line', borderColor: '#a855f7', backgroundColor: 'transparent', tension: 0.4, pointRadius: 3, pointBackgroundColor: '#a855f7' } ] }, options: { ...chartOpts } }); } // Chart 3: Dias do mes atual const ctx3 = document.getElementById('chartDiasMes'); if (ctx3 && data.dias_mes_atual && data.dias_mes_atual.length > 0) { const dias = data.dias_mes_atual; new Chart(ctx3, { type: 'bar', data: { labels: dias.map(d => d.dia_label), datasets: [{ label: 'Entradas por Dia', data: dias.map(d => d.entradas), backgroundColor: 'rgba(59,130,246,.6)', borderColor: '#3b82f6', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#64748b', font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' } }, y: { ticks: { color: '#64748b', font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' }, beginAtZero: true } } } }); } // Tabela historico (24 meses mais recentes, ordem decrescente) const tbody = document.getElementById('evolucaoTableBody'); if (tbody) { const mesesTabela = [...data.meses].reverse().slice(0, 24); tbody.innerHTML = mesesTabela.map(m => { const varColor = m.variacao >= 0 ? '#10b981' : '#ef4444'; const varSign = m.variacao >= 0 ? '+' : ''; const rowStyle = m.variacao < 0 ? 'background:rgba(239,68,68,.04)' : ''; const cs = 'cursor:pointer;text-decoration:underline dotted'; return ` ${m.mes_label} ${m.entradas} ${m.saidas} ${m.retornos} ${m.trancamentos || 0} ${m.a_vencer || 0} ${varSign}${m.variacao} ${m.total_ativo.toLocaleString('pt-BR')} `; }).join(''); } if (loading) loading.style.display = 'none'; if (kpis) kpis.style.display = 'block'; } catch (err) { console.error('Erro loadEvolucaoClientes:', err); if (loading) loading.style.display = 'none'; if (empty) { empty.style.display = 'block'; empty.innerHTML = '

Erro ao carregar dados: ' + err.message + '

'; } } } // ==================== EXPORT EVOLUCAO PDF ==================== function exportEvolucaoPDF(btn) { if (typeof window.jspdf === 'undefined') { alert('Aguarde o carregamento da biblioteca PDF...'); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF('landscape', 'mm', 'a4'); // Title doc.setFontSize(18); doc.setTextColor(13, 122, 95); doc.text('Evolucao Mensal de Clientes - DuxFit', 14, 20); // Date doc.setFontSize(10); doc.setTextColor(100, 100, 100); const now = new Date(); doc.text('Gerado em: ' + now.toLocaleDateString('pt-BR') + ' as ' + now.toLocaleTimeString('pt-BR'), 14, 28); // KPIs const kpiData = [ ['Total Ativos Hoje', document.getElementById('kpi-ativos-hoje')?.textContent || '-'], ['Entradas Este Mes', document.getElementById('kpi-entradas-mes')?.textContent || '-'], ['Saidas Este Mes', document.getElementById('kpi-saidas-mes')?.textContent || '-'], ['Retornos Este Mes', document.getElementById('kpi-retornos-mes')?.textContent || '-'], ['Variacao Liquida', document.getElementById('kpi-variacao-mes')?.textContent || '-'], ['Trancados (Pausados)', document.getElementById('kpi-trancados-hoje')?.textContent || '-'], ['A Vencer Este Mes', document.getElementById('kpi-avencer-hoje')?.textContent || '-'] ]; doc.autoTable({ startY: 34, head: [['Indicador', 'Valor']], body: kpiData, theme: 'grid', headStyles: { fillColor: [13, 122, 95], textColor: 255, fontStyle: 'bold', fontSize: 10 }, bodyStyles: { fontSize: 10 }, columnStyles: { 0: { fontStyle: 'bold', cellWidth: 60 }, 1: { halign: 'center', cellWidth: 40 } }, margin: { left: 14 }, tableWidth: 100 }); // Table - extract from DOM const tbody = document.getElementById('evolucaoTableBody'); if (tbody) { const rows = []; tbody.querySelectorAll('tr').forEach(tr => { const cells = tr.querySelectorAll('td'); if (cells.length >= 8) { rows.push([ cells[0].textContent.trim(), cells[1].textContent.trim(), cells[2].textContent.trim(), cells[3].textContent.trim(), cells[4].textContent.trim(), cells[5].textContent.trim(), cells[6].textContent.trim(), cells[7].textContent.trim() ]); } }); if (rows.length > 0) { const tableStartY = doc.lastAutoTable ? doc.lastAutoTable.finalY + 10 : 80; doc.setFontSize(12); doc.setTextColor(50, 50, 50); doc.text('Historico Mensal (ultimos 24 meses)', 14, tableStartY); doc.autoTable({ startY: tableStartY + 4, head: [['Mes', 'Entradas', 'Saidas', 'Retornos', 'Trancados', 'A Vencer', 'Variacao', 'Total Ativo']], body: rows, theme: 'striped', headStyles: { fillColor: [13, 122, 95], textColor: 255, fontStyle: 'bold', fontSize: 9 }, bodyStyles: { fontSize: 9 }, columnStyles: { 0: { fontStyle: 'bold' }, 1: { halign: 'right', textColor: [59, 130, 246] }, 2: { halign: 'right', textColor: [239, 68, 68] }, 3: { halign: 'right', textColor: [168, 85, 247] }, 4: { halign: 'right', textColor: [245, 158, 11] }, 5: { halign: 'right', textColor: [249, 115, 22] }, 6: { halign: 'right' }, 7: { halign: 'right', fontStyle: 'bold' } }, didParseCell: function(data) { if (data.column.index === 6 && data.section === 'body') { const val = parseInt(data.cell.raw); data.cell.styles.textColor = val >= 0 ? [16, 185, 129] : [239, 68, 68]; } }, margin: { left: 14, right: 14 } }); } } // Footer const pageCount = doc.internal.getNumberOfPages(); for (let i = 1; i <= pageCount; i++) { doc.setPage(i); doc.setFontSize(8); doc.setTextColor(150, 150, 150); doc.text('DuxFit Academia - CRM VendedorGPT | Pagina ' + i + '/' + pageCount, 14, doc.internal.pageSize.height - 10); } doc.save('Evolucao_Mensal_DuxFit_' + now.toISOString().slice(0,10) + '.pdf'); } // ==================== EVOLUCAO DETALHE FUNCTIONS ==================== async function abrirEvolucaoDetalhe(mes, tipo, label) { const modal = document.getElementById('modalEvolucaoDetalhe'); const title = document.getElementById('evolucaoDetalheTitle'); const count = document.getElementById('evolucaoDetalheCount'); const loading = document.getElementById('evolucaoDetalheLoading'); const contentDiv = document.getElementById('evolucaoDetalheContent'); const tbody = document.getElementById('evolucaoDetalheTbody'); const search = document.getElementById('evolucaoDetalheSearch'); const thData = document.getElementById('thDataEvolucao'); if (!modal) return; const tipoLabels = { 'entradas': 'Entradas', 'saidas': 'Saidas', 'retornos': 'Retornos', 'ativos': 'Ativos', 'trancados': 'Trancados', 'avencer': 'A Vencer' }; const tipoColors = { 'entradas': '#3b82f6', 'saidas': '#ef4444', 'retornos': '#a855f7', 'ativos': '#10b981', 'trancados': '#f59e0b', 'avencer': '#f97316' }; title.textContent = (tipoLabels[tipo] || tipo) + ' - ' + (label || mes); title.style.color = tipoColors[tipo] || '#e2e8f0'; count.textContent = 'Carregando...'; loading.style.display = 'block'; loading.innerHTML = '⏳ Carregando...'; contentDiv.style.display = 'none'; if (search) search.value = ''; modal.style.display = 'block'; let url; if (['ativos', 'trancados', 'avencer'].includes(tipo)) { url = API + '/pacto/evolucao-clientes/detalhe?tipo=' + tipo; } else { url = API + '/pacto/evolucao-clientes/mes/' + mes + '/' + tipo; } try { const r = await fetch(url, { headers: authHeaders() }); if (!r.ok) throw new Error('HTTP ' + r.status); const data = await r.json(); const clientes = data.clientes || []; _evolucaoDetalheData = clientes; count.textContent = clientes.length + ' cliente(s)'; if (thData) { if (tipo === 'saidas') thData.textContent = 'Data Saida'; else if (tipo === 'retornos') thData.textContent = 'Data Retorno'; else if (tipo === 'avencer') thData.textContent = 'Data Fim'; else thData.textContent = 'Data Entrada'; } renderEvolucaoDetalhe(clientes); loading.style.display = 'none'; contentDiv.style.display = 'block'; } catch (err) { console.error('Erro abrirEvolucaoDetalhe:', err); loading.innerHTML = 'Erro: ' + err.message + ''; } } function renderEvolucaoDetalhe(clientes) { const tbody = document.getElementById('evolucaoDetalheTbody'); if (!tbody) return; if (!clientes || clientes.length === 0) { tbody.innerHTML = 'Nenhum cliente encontrado'; return; } let html = ''; clientes.forEach(function(c, i) { const dataCol = c.data_retorno || c.data_saida || c.data_entrada || c.data_fim || ''; const tel = c.telefone || ''; const telFormatted = tel.length >= 10 ? '(' + tel.substring(0,2) + ') ' + tel.substring(2) : tel; html += ''; html += '' + (i+1) + ''; html += '' + (c.nome || '-') + ''; html += '' + telFormatted + ''; html += '' + dataCol + ''; html += '' + (c.situacao || '-') + ''; html += '' + (c.plano || '-') + ''; html += ''; }); tbody.innerHTML = html; } function getSitColor(sit) { const s = (sit || '').toUpperCase(); if (s === 'ATIVO') return 'rgba(16,185,129,.2);color:#10b981'; if (s === 'INATIVO') return 'rgba(239,68,68,.2);color:#ef4444'; if (s === 'TRANCADO') return 'rgba(245,158,11,.2);color:#f59e0b'; if (s === 'DESISTENTE' || s === 'CANCELADO') return 'rgba(239,68,68,.2);color:#ef4444'; if (s === 'VISITANTE') return 'rgba(99,102,241,.2);color:#6366f1'; return 'rgba(148,163,184,.2);color:#94a3b8'; } function filtrarEvolucaoDetalhe() { const search = (document.getElementById('evolucaoDetalheSearch')?.value || '').toLowerCase(); if (!search) { renderEvolucaoDetalhe(_evolucaoDetalheData); document.getElementById('evolucaoDetalheCount').textContent = _evolucaoDetalheData.length + ' cliente(s)'; return; } const filtered = _evolucaoDetalheData.filter(function(c) { return (c.nome || '').toLowerCase().includes(search) || (c.telefone || '').includes(search) || (c.email || '').toLowerCase().includes(search); }); renderEvolucaoDetalhe(filtered); document.getElementById('evolucaoDetalheCount').textContent = filtered.length + ' de ' + _evolucaoDetalheData.length + ' cliente(s)'; } function fecharEvolucaoDetalhe() { const modal = document.getElementById('modalEvolucaoDetalhe'); if (modal) modal.style.display = 'none'; } // ==================== META DIARIA EDIT ==================== function getMetaConfig(cat){ try{ const all=JSON.parse(localStorage.getItem('metaDiariaConfig')||'{}'); return all[cat]||{target:10,active:true,templates:[]}; }catch(e){return {target:10,active:true,templates:[]};} } function setMetaConfig(cat,cfg){ try{ const all=JSON.parse(localStorage.getItem('metaDiariaConfig')||'{}'); all[cat]=cfg; localStorage.setItem('metaDiariaConfig',JSON.stringify(all)); // Also save to backend fetch(API+'/settings',{method:'PUT',headers:{...authHeaders(),'Content-Type':'application/json'}, body:JSON.stringify({key:'meta_diaria_config',value:JSON.stringify(all)})}).catch(()=>{}); }catch(e){} } let _metaEditCat=''; function openMetaEditModal(cat){ _metaEditCat=cat; const info=META_CATEGORY_INFO[cat]||{label:cat}; const cfg=getMetaConfig(cat); document.getElementById('metaEditTitle').textContent='Editar: '+info.label; document.getElementById('metaEditTarget').value=cfg.target||10; document.getElementById('metaEditActive').checked=cfg.active!==false; document.getElementById('metaEditTemplates').value=(cfg.templates||[]).join('\n'); document.getElementById('metaEditModal').style.display='flex'; } function closeMetaEditModal(){ document.getElementById('metaEditModal').style.display='none'; } function saveMetaEdit(){ const cat=_metaEditCat; const target=parseInt(document.getElementById('metaEditTarget').value)||10; const active=document.getElementById('metaEditActive').checked; const templates=document.getElementById('metaEditTemplates').value.split('\n').filter(t=>t.trim()); setMetaConfig(cat,{target,active,templates}); closeMetaEditModal(); loadMetaDiariaStats(); } // Load saved config from backend on init (async function(){ try{ const r=await fetch(API+'/settings',{headers:authHeaders()}); if(r.ok){ const d=await r.json(); if(d.meta_diaria_config) localStorage.setItem('metaDiariaConfig',d.meta_diaria_config);// localStorage.setItem('metaDiariaConfig',d.value); } }catch(e){} })(); // ÔöÇÔöÇÔöÇÔöÇ Instagram DM Campaigns ÔöÇÔöÇÔöÇÔöÇ async function loadIgDmCampaigns(){ try{ const [campR, statsR] = await Promise.all([ api('/instagram/dm/campaigns'), api('/instagram/dm/stats') ]); const camps = campR.campaigns || []; const stats = statsR; document.getElementById('igDmStatSent').textContent = stats.messages_sent || 0; document.getElementById('igDmStatLeads').textContent = stats.leads_captured || 0; document.getElementById('igDmStatActive').textContent = stats.campaigns_active || 0; const el = document.getElementById('igDmCampaignList'); if(camps.length === 0){ el.innerHTML = '
Nenhuma campanha criada. Clique em "+ Nova Campanha" para comecar.
'; } else { el.innerHTML = camps.map(c => `
${c.name} ${c.is_active?'Ativa':'Pausada'}
Keyword: ${c.trigger_keyword}
${(c.reply_message||'').substring(0,80)}${(c.reply_message||'').length>80?'...':''}
💬 ${c.messages_sent} enviados 👤 ${c.leads_captured} leads
`).join(''); } // Logs const logs = stats.recent_logs || []; const logsEl = document.getElementById('igDmLogsTable'); if(logs.length === 0){ logsEl.innerHTML = '
Nenhum log ainda
'; } else { logsEl.innerHTML = '' + logs.map(l => ``).join('') + '
UsuarioComentarioDM EnviadaStatusData
@${l.ig_username||'?'}${(l.comment_text||'').substring(0,40)}${(l.dm_sent||'').substring(0,40)}${l.status}${l.created_at?new Date(l.created_at).toLocaleString('pt-BR'):''}
'; } }catch(e){console.error('Erro loadIgDmCampaigns:',e)} } function openIgDmModal(editData){ document.getElementById('igDmEditId').value = editData ? editData.id : ''; document.getElementById('igDmModalTitle').textContent = editData ? 'Editar Campanha' : 'Nova Campanha DM'; document.getElementById('igDmName').value = editData ? editData.name : ''; document.getElementById('igDmKeyword').value = editData ? editData.trigger_keyword : ''; document.getElementById('igDmReply').value = editData ? editData.reply_message : ''; document.getElementById('igDmMedia').value = editData ? (editData.media_url||'') : ''; document.getElementById('igDmModal').style.display = 'flex'; } function closeIgDmModal(){ document.getElementById('igDmModal').style.display = 'none'; } async function saveIgDmCampaign(){ const editId = document.getElementById('igDmEditId').value; const data = { name: document.getElementById('igDmName').value.trim(), trigger_keyword: document.getElementById('igDmKeyword').value.trim(), reply_message: document.getElementById('igDmReply').value.trim(), media_url: document.getElementById('igDmMedia').value.trim() || null, }; if(!data.name || !data.trigger_keyword || !data.reply_message){ toast('Preencha nome, keyword e mensagem','error'); return; } try{ if(editId){ await api('/instagram/dm/campaigns/'+editId, {method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data)}); toast('Campanha atualizada!'); } else { await api('/instagram/dm/campaigns', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data)}); toast('Campanha criada!'); } closeIgDmModal(); loadIgDmCampaigns(); }catch(e){toast('Erro ao salvar campanha','error')} } async function toggleIgDmCampaign(id, active){ try{ await api('/instagram/dm/campaigns/'+id, {method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({is_active:active})}); toast(active ? 'Campanha ativada!' : 'Campanha pausada!'); loadIgDmCampaigns(); }catch(e){toast('Erro','error')} } async function editIgDmCampaign(id){ try{ const r = await api('/instagram/dm/campaigns'); const camp = (r.campaigns||[]).find(c=>c.id===id); if(camp) openIgDmModal(camp); }catch(e){toast('Erro','error')} } async function deleteIgDmCampaign(id){ if(!confirm('Tem certeza que deseja excluir esta campanha?')) return; try{ await api('/instagram/dm/campaigns/'+id, {method:'DELETE'}); toast('Campanha excluida!'); loadIgDmCampaigns(); }catch(e){toast('Erro','error')} } // Hook into loadInstagram const _origLoadInstagram = typeof loadInstagram === 'function' ? loadInstagram : null; async function loadInstagramWithDM(){ if(_origLoadInstagram) await _origLoadInstagram(); await loadIgDmCampaigns(); } // Override if(typeof loadInstagram !== 'undefined'){ const _oldLI = loadInstagram; loadInstagram = async function(){ await _oldLI(); await loadIgDmCampaigns(); }; } // ==================== ENGAJAMENTO ==================== var engCharts = {leads: null, ativos: null, inativos: null}; var engCategorized = {leads: null, ativos: null, inativos: null}; function classifyByLastContact(items) { var now = new Date(); var today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); var week = new Date(today); week.setDate(today.getDate() - 7); var month = new Date(today); month.setDate(today.getDate() - 30); var groups = {hoje: [], semana: [], mes: [], antigo: [], nunca: []}; items.forEach(function(l) { var ts = l.last_contact_at || l.last_message_at; if (!ts) { groups.nunca.push(l); return; } var d = new Date(ts); if (d >= today) groups.hoje.push(l); else if (d >= week) groups.semana.push(l); else if (d >= month) groups.mes.push(l); else groups.antigo.push(l); }); return groups; } function buildEngDonut(canvasId, groups, total, oldChart) { var ctx = document.getElementById(canvasId); if (!ctx) return null; if (oldChart) { try { oldChart.destroy(); } catch(e) {} } var colors = ["#22c55e","#3b82f6","#f59e0b","#ef4444","#6b7280"]; var labels = ["Hoje","Esta semana","Este mes","Mais de 30 dias","Nunca contatado"]; var keys = ["hoje","semana","mes","antigo","nunca"]; var data = keys.map(function(k){ return groups[k].length; }); return new Chart(ctx, { type: "doughnut", data: {labels: labels, datasets: [{data: data, backgroundColor: colors, borderColor: "rgba(0,0,0,0)", borderWidth: 0, hoverOffset: 8}]}, options: { cutout: "68%", responsive: true, maintainAspectRatio: false, plugins: { legend: {display: false}, tooltip: {callbacks: {label: function(c){ return " " + c.label + ": " + c.parsed + " (" + (total ? Math.round(c.parsed/total*100) : 0) + "%)"; }}} } } }); } function buildEngLegend(containerId, groups) { var el = document.getElementById(containerId); if (!el) return; var colors = ["#22c55e","#3b82f6","#f59e0b","#ef4444","#6b7280"]; var labels = ["Hoje","Esta semana","Este mes","Mais de 30 dias","Nunca contatado"]; var keys = ["hoje","semana","mes","antigo","nunca"]; var total = keys.reduce(function(s,k){ return s + groups[k].length; }, 0); el.innerHTML = ""; keys.forEach(function(k, i) { var n = groups[k].length; var pct = total ? Math.round(n/total*100) : 0; var row = document.createElement("div"); row.style.cssText = "display:flex;align-items:center;gap:8px;font-size:11px;cursor:pointer;padding:3px 4px;border-radius:6px;transition:.15s"; row.onmouseover = function(){ this.style.background="rgba(255,255,255,0.05)"; }; row.onmouseout = function(){ this.style.background=""; }; var dot = "
"; var lbl2 = "" + labels[i] + ""; var ns = "" + n + ""; var ps = "" + pct + "%"; row.innerHTML = dot + lbl2 + ns + ps; (function(lb, gi) { row.onclick = function() { showEngDetail(lb, gi); }; })(labels[i], groups[k]); el.appendChild(row); }); } function showEngDetail(label, items) { var panel = document.getElementById("eng-detail-panel"); var title = document.getElementById("eng-detail-title"); var list = document.getElementById("eng-detail-list"); title.textContent = label + " - " + items.length + " contatos"; if (!items.length) { list.innerHTML = "

Nenhum contato neste grupo

"; } else { list.innerHTML = items.slice(0,100).map(function(l) { var ts = l.last_contact_at || l.last_message_at; var date = ts ? new Date(ts).toLocaleDateString("pt-BR") : "Nunca"; var st = l.pacto_status ? l.pacto_status.replace(/_/g," ") : (l.status || ""); return "
" + "
" + "
👤
" + "
" + (l.name || "Sem nome") + "
" + "
" + (l.phone || "") + (st ? " ┬À " + st : "") + "
" + "
Ultimo contato
" + "
" + date + "
"; }).join(""); } panel.style.display = "block"; panel.scrollIntoView({behavior:"smooth", block:"start"}); } function wireEngDonutClick(chart, catKey) { if (!chart) return; var keys = ["hoje","semana","mes","antigo","nunca"]; var labels = ["Hoje","Esta semana","Este mes","Mais de 30 dias","Nunca contatado"]; chart.options.onClick = function(evt, elements) { if (!elements.length) return; var idx = elements[0].index; var groups = engCategorized[catKey]; if (groups) showEngDetail(labels[idx], groups[keys[idx]] || []); }; chart.update(); } async function loadEngajamento() { var token = localStorage.getItem("token"); var upd = document.getElementById("engajamento-updated"); if (upd) upd.textContent = "Carregando..."; try { var res = await fetch("/api/leads?limit=5000", {headers: {Authorization: "Bearer " + token}}); var data = await res.json(); var leads = Array.isArray(data) ? data : (data.leads || data.data || []); var leadsOnly = leads.filter(function(l){ return !l.pacto_status || (l.pacto_status !== "cliente_ativo" && l.pacto_status !== "ex_aluno"); }); var ativosOnly = leads.filter(function(l){ return l.pacto_status === "cliente_ativo" || l.situacao === "ATIVO"; }); var inativosOnly = leads.filter(function(l){ return l.pacto_status === "ex_aluno" || l.situacao === "INATIVO"; }); var gLeads = classifyByLastContact(leadsOnly); var gAtivos = classifyByLastContact(ativosOnly); var gInativos = classifyByLastContact(inativosOnly); engCategorized.leads = gLeads; engCategorized.ativos = gAtivos; engCategorized.inativos = gInativos; engCharts.leads = buildEngDonut("chartEngLeads", gLeads, leadsOnly.length, engCharts.leads); engCharts.ativos = buildEngDonut("chartEngAtivos", gAtivos, ativosOnly.length, engCharts.ativos); engCharts.inativos = buildEngDonut("chartEngInativos", gInativos, inativosOnly.length, engCharts.inativos); wireEngDonutClick(engCharts.leads, "leads"); wireEngDonutClick(engCharts.ativos, "ativos"); wireEngDonutClick(engCharts.inativos, "inativos"); document.getElementById("eng-leads-num").textContent = leadsOnly.length; document.getElementById("eng-ativos-num").textContent = ativosOnly.length; document.getElementById("eng-inativos-num").textContent = inativosOnly.length; document.getElementById("eng-leads-total").textContent = "Total: " + leadsOnly.length + " leads"; document.getElementById("eng-ativos-total").textContent = "Total: " + ativosOnly.length + " ativos"; document.getElementById("eng-inativos-total").textContent = "Total: " + inativosOnly.length + " inativos"; document.getElementById("eng-leads-badge").textContent = gLeads.hoje.length + " hoje"; document.getElementById("eng-ativos-badge").textContent = gAtivos.hoje.length + " hoje"; document.getElementById("eng-inativos-badge").textContent = gInativos.hoje.length + " hoje"; buildEngLegend("eng-leads-legend", gLeads); buildEngLegend("eng-ativos-legend", gAtivos); buildEngLegend("eng-inativos-legend", gInativos); if (upd) upd.textContent = "Atualizado em " + new Date().toLocaleTimeString("pt-BR"); } catch(e) { console.error("loadEngajamento error", e); if (upd) upd.textContent = "Erro ao carregar dados"; } } // ===================================================== // LEAD SCORING & FUNIL FUNCTIONS // ===================================================== let funilChartInstance = null; let funilVisible = false; function toggleFunil() { funilVisible = !funilVisible; const sec = document.getElementById('funilSection'); if (sec) { sec.style.display = funilVisible ? 'block' : 'none'; if (funilVisible) loadFunil(); } } async function loadFunil() { try { const r = await fetch(API+'/leads/funil', {headers: authHeaders()}); const d = await r.json(); if (!d.etapas) return; const funilEl = document.getElementById('funilVisual'); const maxCount = Math.max(...d.etapas.map(e => e.count), 1); const mainEtapas = d.etapas.filter(e => !['perdido','geladeira'].includes(e.status)); const lostEtapas = d.etapas.filter(e => ['perdido','geladeira'].includes(e.status)); funilEl.innerHTML = mainEtapas.map((e, i) => { const minW = 30, maxW = 96; const w = e.count > 0 ? minW + (maxW - minW) * (e.count / maxCount) : minW; return '
' + e.label + '' + e.count + ' (' + e.percentage + '%)
'; }).join('
'); if (lostEtapas.length > 0) { funilEl.innerHTML += '
' + lostEtapas.map(e => '
' + e.label + ': ' + e.count + '
').join('') + '
'; } const taxaEl = document.getElementById('funilTaxa'); if (taxaEl) taxaEl.textContent = 'Taxa: ' + (d.taxa_conversao || 0) + '%'; const ctx = document.getElementById('chartFunilBar'); if (!ctx) return; if (funilChartInstance) funilChartInstance.destroy(); funilChartInstance = new Chart(ctx, { type: 'bar', data: { labels: d.etapas.map(e => e.label), datasets: [{ label: 'Leads', data: d.etapas.map(e => e.count), backgroundColor: d.etapas.map(e => e.color + 'cc'), borderColor: d.etapas.map(e => e.color), borderWidth: 1, borderRadius: 4, }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#94a3b8', font: { size: 10 } }, grid: { color: 'rgba(255,255,255,0.04)' } }, y: { beginAtZero: true, ticks: { color: '#94a3b8' }, grid: { color: 'rgba(255,255,255,0.04)' } } } } }); } catch(e) { console.error('Erro ao carregar funil:', e); } } function filterByFunilStatus(status) { toast('Filtrando por: ' + status.replace(/_/g,' ')); } async function recalculateScores() { try { const r = await fetch(API+'/leads/recalculate-scores', { method: 'POST', headers: authHeaders() }); const d = await r.json(); if (d.started) { toast('Recalculando scores para ' + d.total + ' leads... Aguarde 10s.'); setTimeout(() => { loadAll(); if(funilVisible) loadFunil(); }, 5000); } } catch(e) { toast('Erro ao recalcular scores', 'error'); } } function getScoreBadge(score) { const s = score || 0; if (s >= 80) return '🔥 ' + s + ''; if (s >= 60) return '⭐ ' + s + ''; if (s >= 40) return '📊 ' + s + ''; return '' + s + ''; } // Patch loadLeadsDashboard to update Top Leads KPI setTimeout(function() { const origLD = loadLeadsDashboard; window.loadLeadsDashboard = async function() { await origLD.apply(this, arguments); try { const r2 = await fetch(API+'/leads/stats', {headers: authHeaders()}); const d2 = await r2.json(); const el = document.getElementById('ldKpiTopLeads'); if (el && d2.top_leads_count !== undefined) el.textContent = d2.top_leads_count; } catch(err) {} }; }, 100); // ===================================================== // ============================================================ // Feature: Exportacao PDF/Excel // ============================================================ async function exportLeads(event, format) { const btn = event.target; const origText = btn.textContent; btn.disabled = true; btn.textContent = 'Gerando...'; try { const r = await fetch(API + '/relatorios/leads/' + format, {headers: authHeaders()}); if (!r.ok) { toast('Erro ao gerar exportacao', 'error'); return; } const blob = await r.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'leads_' + new Date().toISOString().slice(0,10) + '.' + format; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast('Download iniciado!', 'success'); } catch(e) { toast('Erro: ' + e.message, 'error'); } finally { btn.disabled = false; btn.textContent = origText; } } async function exportPipeline(event, format) { const btn = event.target; const origText = btn.textContent; btn.disabled = true; btn.textContent = 'Gerando...'; try { const r = await fetch(API + '/relatorios/pipeline/' + format, {headers: authHeaders()}); if (!r.ok) { toast('Erro ao gerar exportacao', 'error'); return; } const blob = await r.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'pipeline_' + new Date().toISOString().slice(0,10) + '.' + format; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast('Download iniciado!', 'success'); } catch(e) { toast('Erro: ' + e.message, 'error'); } finally { btn.disabled = false; btn.textContent = origText; } } // ============================================================ // Feature: Notificacoes WebSocket em Tempo Real // ============================================================ let _notifCount = 0; let _notifWs = null; function initNotifications() { if (_notifWs && _notifWs.readyState < 2) return; // already open/connecting const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = proto + '//' + location.host + '/ws/notifications'; try { _notifWs = new WebSocket(wsUrl); _notifWs.onopen = () => { console.log('[WS] Notificacoes conectadas'); // ping keepalive setInterval(() => { if (_notifWs && _notifWs.readyState === 1) _notifWs.send('ping'); }, 30000); }; _notifWs.onmessage = (e) => { try { const msg = JSON.parse(e.data); if (msg === 'pong') return; if (msg.type === 'new_lead') { showToast('Novo lead: ' + (msg.lead && msg.lead.name ? msg.lead.name : msg.lead && msg.lead.phone ? msg.lead.phone : 'Desconhecido'), 'success'); _notifCount++; updateNotifBadge(); // Reload pipeline if visible if (document.getElementById('page-pipeline') && document.getElementById('page-pipeline').classList.contains('active')) { setTimeout(loadAll, 1000); } } else if (msg.type === 'new_message') { showToast('Nova mensagem de ' + (msg.from || 'contato'), 'info'); } } catch(err) {} }; _notifWs.onclose = () => { console.log('[WS] Desconectado, reconectando em 5s...'); setTimeout(initNotifications, 5000); }; _notifWs.onerror = () => { _notifWs.close(); }; } catch(e) { console.warn('[WS] Erro ao conectar:', e); setTimeout(initNotifications, 10000); } } function updateNotifBadge() { let badge = document.getElementById('notifBadge'); if (!badge) return; if (_notifCount > 0) { badge.textContent = _notifCount > 99 ? '99+' : _notifCount; badge.style.display = 'flex'; } else { badge.style.display = 'none'; } } function showToast(message, type) { // Remove old toasts with same type document.querySelectorAll('.crm-toast-' + (type||'info')).forEach(t => t.remove()); const t = document.createElement('div'); t.className = 'crm-toast-' + (type||'info'); const bg = type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'; t.style.cssText = 'position:fixed;bottom:20px;right:20px;background:' + bg + ';color:#fff;padding:12px 20px;border-radius:8px;font-size:.9rem;z-index:9999;box-shadow:0 4px 12px rgba(0,0,0,.3);animation:crm-slideIn .3s ease;max-width:320px;word-break:break-word;cursor:pointer'; t.textContent = message; t.onclick = () => t.remove(); document.body.appendChild(t); setTimeout(() => { if (t.parentNode) t.remove(); }, 4500); } // CSS animation if (!document.getElementById('crm-toast-style')) { const st = document.createElement('style'); st.id = 'crm-toast-style'; st.textContent = '@keyframes crm-slideIn{from{transform:translateX(110%);opacity:0}to{transform:translateX(0);opacity:1}}'; document.head.appendChild(st); } // Auto-init on load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(initNotifications, 2000)); } else { setTimeout(initNotifications, 2000); } // ÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉ // TEMPLATES DE MENSAGEM // ÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉ let _allTemplates = []; let _currentTmplCat = 'todos'; const TMPL_CAT_COLORS = { abordagem: '#22c55e', follow_up: '#3b82f6', oferta: '#f59e0b', cobranca: '#ef4444', outros: '#8b5cf6' }; const TMPL_CAT_LABELS = { abordagem: 'Abordagem', follow_up: 'Follow-up', oferta: 'Oferta', cobranca: 'Cobranca', outros: 'Outros' }; async function loadTemplatesMsg() { try { const url = _currentTmplCat === 'todos' ? '/api/templates-msg' : `/api/templates-msg?category=${_currentTmplCat}`; const data = await fetch(url, {headers: authHeaders()}).then(r => r.json()); _allTemplates = Array.isArray(data) ? data : []; renderTemplatesGrid(_allTemplates); } catch(e) { console.error('loadTemplatesMsg', e); _allTemplates = []; renderTemplatesGrid([]); } } function filterTemplates(cat) { _currentTmplCat = cat; document.querySelectorAll('.tmpl-tab').forEach(t => { const isActive = t.dataset.cat === cat; t.style.background = isActive ? 'rgba(99,102,241,0.15)' : 'var(--glass-bg)'; t.style.color = isActive ? '#6366f1' : 'var(--text2)'; t.style.borderColor = isActive ? '#6366f1' : 'var(--border)'; }); loadTemplatesMsg(); } function renderTemplatesGrid(templates) { const grid = document.getElementById('templatesGrid'); if (!grid) return; if (!templates.length) { grid.innerHTML = '
📝
Nenhum template encontrado. Crie o primeiro!
'; return; } grid.innerHTML = templates.map(t => { const color = TMPL_CAT_COLORS[t.category] || '#8b5cf6'; const label = TMPL_CAT_LABELS[t.category] || t.category; const preview = t.content.length > 120 ? t.content.slice(0, 120) + '...' : t.content; const vars = Array.isArray(t.variables) ? t.variables : []; const varChips = vars.slice(0, 4).map(v => `{${v.name}}`).join(''); return `
${escHtml(t.name)} ${label}
${escHtml(preview)}
${vars.length ? `
${varChips}
` : ''}
📋 Usado ${t.use_count || 0}x
`; }).join(''); } function openTemplateModal(tmpl) { document.getElementById('templateModal').style.display = 'flex'; document.getElementById('tmplModalTitle').textContent = tmpl ? 'Editar Template' : 'Novo Template'; document.getElementById('tmplEditId').value = tmpl ? tmpl.id : ''; document.getElementById('tmplName').value = tmpl ? tmpl.name : ''; document.getElementById('tmplCategory').value = tmpl ? (tmpl.category || 'outros') : 'outros'; document.getElementById('tmplContent').value = tmpl ? tmpl.content : ''; detectTmplVars(); } function closeTemplateModal() { document.getElementById('templateModal').style.display = 'none'; } function editTemplate(jsonStr) { try { const t = JSON.parse(jsonStr); openTemplateModal(t); } catch(e) { console.error(e); } } function detectTmplVars() { const content = document.getElementById('tmplContent').value; const vars = [...new Set((content.match(/\{(\w+)\}/g) || []).map(v => v.slice(1,-1)))]; const box = document.getElementById('tmplVarsPreview'); const chips = document.getElementById('tmplVarsChips'); if (vars.length) { box.style.display = 'block'; chips.innerHTML = vars.map(v => `{${v}}`).join(''); } else { box.style.display = 'none'; } } async function saveTemplate() { const id = document.getElementById('tmplEditId').value; const name = document.getElementById('tmplName').value.trim(); const category = document.getElementById('tmplCategory').value; const content = document.getElementById('tmplContent').value.trim(); if (!name || !content) { alert('Nome e mensagem sao obrigatorios!'); return; } const method = id ? 'PUT' : 'POST'; const url = id ? `/api/templates-msg/${id}` : '/api/templates-msg'; try { const r = await fetch(url, {method, headers: authHeaders(), body: JSON.stringify({name, category, content})}); if (!r.ok) throw new Error(await r.text()); closeTemplateModal(); loadTemplatesMsg(); } catch(e) { alert('Erro ao salvar: ' + e.message); } } async function deleteTemplate(id) { if (!confirm('Deletar este template?')) return; await fetch(`/api/templates-msg/${id}`, {method: 'DELETE', headers: authHeaders()}); loadTemplatesMsg(); } async function copyTemplate(id, content) { await navigator.clipboard.writeText(content).catch(() => {}); await fetch(`/api/templates-msg/${id}/use`, {method: 'POST', headers: authHeaders()}); showToast && showToast('Template copiado!', 'success'); } // ÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉ // DISPARO EM MASSA // ÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉÔòÉ let _wizardStep = 1; let _currentJobId = null; let _jobPollInterval = null; let _previewDebounce = null; async function loadDisparoJobs() { const list = document.getElementById('disparoJobsList'); if (!list) return; try { const data = await fetch('/api/whatsapp/disparo-jobs', {headers: authHeaders()}).then(r => r.json()); if (!data.length) { list.innerHTML = '
Nenhum disparo realizado ainda.
'; return; } list.innerHTML = data.map(j => { const pct = j.total ? Math.round((j.enviados / j.total) * 100) : 0; const statusColor = {pending:'#8b5cf6', running:'#f59e0b', done:'#22c55e', error:'#ef4444'}[j.status] || '#6b7280'; const statusLabel = {pending:'Aguardando', running:'Enviando...', done:'Concluido', error:'Erro'}[j.status] || j.status; return `
${escHtml((j.mensagem||'').slice(0,60))}${(j.mensagem||'').length > 60 ? '...' : ''}
${statusLabel}
Total: ${j.total} Enviados: ${j.enviados} Erros: ${j.erros} ${new Date(j.created_at).toLocaleString('pt-BR')}
`; }).join(''); } catch(e) { list.innerHTML = '
Erro ao carregar historico.
'; } } async function abrirWizardDisparo() { _wizardStep = 1; _currentJobId = null; document.getElementById('disparoWizardModal').style.display = 'flex'; document.getElementById('wFiltroStatus').value = ''; document.getElementById('wFiltroTemp').value = ''; document.getElementById('wFiltroTag').value = ''; document.getElementById('wMensagem').value = ''; document.getElementById('wDelay').value = '3'; document.getElementById('progressDisparoBox').style.display = 'none'; document.getElementById('btnDisparar').style.display = 'block'; wizardShowStep(1); previewMassa(); // Load templates into dropdown const sel = document.getElementById('wTemplateSelect'); if (sel) { sel.innerHTML = ''; _allTemplates.forEach(t => { sel.innerHTML += ``; }); } } function fecharWizardDisparo() { document.getElementById('disparoWizardModal').style.display = 'none'; if (_jobPollInterval) { clearInterval(_jobPollInterval); _jobPollInterval = null; } } function wizardShowStep(step) { _wizardStep = step; [1,2,3].forEach(s => { const el = document.getElementById(`wizardStep${s}`); if (el) el.style.display = s === step ? 'block' : 'none'; const stepEl = document.getElementById(`wstep${s}`); if (stepEl) { stepEl.style.color = s === step ? '#f59e0b' : (s < step ? '#22c55e' : 'var(--text3)'); stepEl.style.borderBottomColor = s === step ? '#f59e0b' : (s < step ? '#22c55e' : 'var(--border)'); } }); const back = document.getElementById('wizardBtnBack'); const next = document.getElementById('wizardBtnNext'); const lbl = document.getElementById('wizardStepsLabel'); if (back) back.style.display = step > 1 ? 'block' : 'none'; if (next) next.style.display = step < 3 ? 'block' : 'none'; if (lbl) lbl.textContent = `Passo ${step} de 3: ${['Selecionar Leads','Criar Mensagem','Confirmar e Disparar'][step-1]}`; } function wizardNext() { if (_wizardStep === 1) { const count = parseInt(document.getElementById('previewCount').textContent); if (!count || count === 0) { alert('Nenhum lead encontrado com esses filtros!'); return; } wizardShowStep(2); } else if (_wizardStep === 2) { const msg = document.getElementById('wMensagem').value.trim(); if (!msg) { alert('Digite a mensagem!'); return; } // Build summary const count = document.getElementById('previewCount').textContent; const delay = parseInt(document.getElementById('wDelay').value) || 3; const mins = Math.ceil((count * delay) / 60); document.getElementById('confirmarSummary').innerHTML = `
📱 ${count} leads serao impactados
⏰ Delay entre envios: ${delay} segundos
🕐 Tempo estimado: ~${mins} minutos
${escHtml(msg.slice(0,200))}${msg.length>200?'...':''}
`; wizardShowStep(3); } } function wizardBack() { if (_wizardStep > 1) wizardShowStep(_wizardStep - 1); } function previewMassa() { clearTimeout(_previewDebounce); _previewDebounce = setTimeout(async () => { const filtro = buildFiltro(); try { const r = await fetch('/api/whatsapp/preview-massa', { method: 'POST', headers: authHeaders(), body: JSON.stringify({filtro}) }); const d = await r.json(); const el = document.getElementById('previewCount'); if (el) el.textContent = d.count || 0; } catch(e) {} }, 400); } function buildFiltro() { const status = document.getElementById('wFiltroStatus')?.value; const temperature = document.getElementById('wFiltroTemp')?.value; const tagVal = document.getElementById('wFiltroTag')?.value.trim(); const filtro = {}; if (status) filtro.status = status; if (temperature) filtro.temperature = temperature; if (tagVal) filtro.tags = [tagVal]; return filtro; } function aplicarTemplateDisparo() { const sel = document.getElementById('wTemplateSelect'); if (!sel || !sel.value) return; const opt = sel.options[sel.selectedIndex]; const content = opt.getAttribute('data-content') || ''; const ta = document.getElementById('wMensagem'); if (ta) { ta.value = content; previewPersonalized(); } } function previewPersonalized() { const msg = document.getElementById('wMensagem')?.value || ''; const box = document.getElementById('previewPersonalizedBox'); const txt = document.getElementById('previewPersonalizedText'); if (!msg.trim()) { if (box) box.style.display = 'none'; return; } const preview = msg.replace('{nome}', 'Carlos').replace('{telefone}', '5511999887766') .replace('{empresa}', 'DuxFit').replace('{atendente}', 'Ana'); if (box) box.style.display = 'block'; if (txt) txt.textContent = preview; } async function executarDisparo() { const mensagem = document.getElementById('wMensagem').value.trim(); if (!mensagem) { alert('Mensagem vazia!'); return; } const filtro = buildFiltro(); const delay = parseInt(document.getElementById('wDelay').value) || 3; document.getElementById('btnDisparar').style.display = 'none'; document.getElementById('progressDisparoBox').style.display = 'block'; document.getElementById('progressText').textContent = 'Iniciando disparo...'; try { const r = await fetch('/api/whatsapp/disparo-massa', { method: 'POST', headers: authHeaders(), body: JSON.stringify({filtro, mensagem, delay_segundos: delay}) }); const d = await r.json(); if (d.error || !d.job_id) throw new Error(d.detail || 'Erro ao iniciar'); _currentJobId = d.job_id; pollDisparoJob(_currentJobId, d.total); } catch(e) { alert('Erro: ' + e.message); document.getElementById('btnDisparar').style.display = 'block'; document.getElementById('progressDisparoBox').style.display = 'none'; } } function pollDisparoJob(jobId, total) { let lastEnviados = 0; _jobPollInterval = setInterval(async () => { try { const d = await fetch(`/api/whatsapp/disparo-jobs/${jobId}`, {headers: authHeaders()}).then(r => r.json()); const pct = total ? Math.round((d.enviados / total) * 100) : 0; document.getElementById('progressBar').style.width = pct + '%'; document.getElementById('progressPct').textContent = pct + '%'; document.getElementById('progressEnviados').textContent = d.enviados; document.getElementById('progressErros').textContent = d.erros; document.getElementById('progressText').textContent = d.status === 'running' ? `Enviando... (${d.enviados}/${total})` : (d.status === 'done' ? 'Disparo concluido!' : d.status); if (d.status === 'done' || d.status === 'error') { clearInterval(_jobPollInterval); loadDisparoJobs(); if (d.status === 'done') showToast && showToast(`Disparo concluido! ${d.enviados} mensagens enviadas.`, 'success'); } } catch(e) {} }, 3000); } function escHtml(s) { return String(s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } // ============ DAILY SUMMARY (CRM) ============ async function regenerateDailySummary() { var el = document.getElementById('dsSummaryContent'); if (el) el.textContent = 'Gerando...'; try { var token = getToken(); await fetch('/api/dashboard/generate-summary', {method:'POST', headers:{'Authorization':'Bearer '+token,'Content-Type':'application/json'}}); await loadDailySummary(); } catch(e) { if (el) el.textContent = 'Erro.'; } } // ============ FOLLOWUP SEQUENCES (CRM) ============ var _seqEditId=null, _seqStartId=null, _allLeadsForSeq=[]; function _ch(s){return String(s||'').replace(/&/g,'&').replace(//g,'>');} async function _crmApi(path, opts) { try { var h = Object.assign({'Content-Type':'application/json','Authorization':'Bearer '+(getToken()||'')}, (opts&&opts.headers)||{}); var r = await fetch(path, Object.assign({}, opts||{}, {headers:h})); if (!r.ok) return null; return await r.json(); } catch(e) { return null; } } async function loadFollowupSequences() { var data = await _crmApi('/api/followup/sequences'); var el = document.getElementById('seqList'); if (!el || !data) return; el.innerHTML = !data.length ? '

Nenhuma sequencia.

' : data.map(function(s){ var st=s.steps||[]; return '
'+_ch(s.name)+''+(s.active?'Ativa':'Inativa')+''+st.length+' passo(s) | '+(s.active_executions||0)+' ativos
'+(st.length?'
'+st.map(function(p){return 'Dia '+p.dias+': '+_ch((p.mensagem||'').substring(0,40))+(p.mensagem&&p.mensagem.length>40?'...':'')+'';}).join('')+'
':'')+ '
'; }).join(''); var ex = await _crmApi('/api/followup/executions'); var exEl = document.getElementById('seqExecList'); if (exEl && ex) { exEl.innerHTML = !ex.length ? 'Nenhuma execucao ativa.' : ''+ ex.map(function(e){var nr=e.next_run?new Date(e.next_run).toLocaleString('pt-BR'):'-';return '';}).join('')+'
LeadSequenciaPassoProximo Envio
'+_ch(e.lead_name||e.lead_phone||'-')+''+_ch(e.sequence_name||'-')+''+(e.step_index+1)+''+nr+'
'; } } function openNewSeqModal(){_seqEditId=null;document.getElementById('seqModalTitle').textContent='Nova Sequencia';document.getElementById('seqName').value='';document.getElementById('seqSteps').innerHTML='';addSeqStep();document.getElementById('seqModal').style.display='flex';} function editSeq(id,s){_seqEditId=id;document.getElementById('seqModalTitle').textContent='Editar Sequencia';document.getElementById('seqName').value=s.name||'';document.getElementById('seqSteps').innerHTML='';(s.steps||[]).forEach(function(p){addSeqStep(p.dias,p.mensagem);});document.getElementById('seqModal').style.display='flex';} function closeSeqModal(){document.getElementById('seqModal').style.display='none';} function addSeqStep(dias,mensagem){var c=document.getElementById('seqSteps');var d=document.createElement('div');d.style.cssText='display:flex;gap:8px;align-items:flex-start;margin-bottom:8px;background:var(--bg3);padding:8px;border-radius:8px';d.innerHTML='
';c.appendChild(d);} async function saveSeq(){var name=document.getElementById('seqName').value.trim();if(!name){alert('Informe o nome.');return;}var els=document.getElementById('seqSteps').children;var steps=[];for(var i=0;iNenhum lead.

';return;}el.innerHTML=leads.map(function(l){return '';}).join('');} function filterSeqLeads(){var q=document.getElementById('seqLeadSearch').value.toLowerCase();renderSeqLeads(_allLeadsForSeq.filter(function(l){return(l.name||'').toLowerCase().includes(q)||(l.phone||'').includes(q);}));} function closeStartModal(){document.getElementById('seqStartModal').style.display='none';} async function startSeqExecution(){var checks=document.querySelectorAll('#seqLeadList input[type=checkbox]:checked');var ids=Array.from(checks).map(function(c){return c.value;});if(!ids.length){alert('Selecione leads.');return;}var r=await _crmApi('/api/followup/sequences/'+_seqStartId+'/start',{method:'POST',body:JSON.stringify({lead_ids:ids})});if(r){closeStartModal();loadFollowupSequences();alert(r.message||'Iniciado!');}}

📝 Templates de Mensagem WhatsApp

Biblioteca de mensagens reutilizaveis para atendimento rapido e padronizado

📝
Carregando templates...

📢 Disparo em Massa Segmentado

Envie mensagens para grupos segmentados de leads via WhatsApp

Historico de Disparos

Carregando...

🌟 Perfil 360° do Cliente

Visao completa: IDIC, historico, temperatura e relacionamento

🎂 Aniversariantes do Mes

Dia especial: aula personalizada + bombom + WhatsApp especial

Aniversariantes do Mes
--
Dia Especial Planejados
0
Realizados
0

🎉 Clientes 50+ Anos

Envio de audio personalizado via WhatsApp para clientes acima de 50 anos

Total 50+ Anos
--
Ativos
--
Selecionados
0
🎤
Envio de Audio Personalizado
Mensagem especial para clientes com 50+ anos

Campanhas Instagram Comment-to-DM

Seguidor comenta keyword no post -> Carol envia DM automatica

Nome Keyword Mensagem DM Ativo Enviadas Leads Acoes

Ultimos Disparos

Como funciona

  1. Crie uma campanha com uma keyword (ex: "quero", "preco", "horario")
  2. Publique um post/reel no Instagram com CTA: "Comenta QUERO que mando info no Direct!"
  3. Quando um seguidor comentar com a keyword, o CRM envia a DM automatica
  4. A Carol IA continua a conversa e qualifica o lead
  5. O lead aparece no Pipeline > Instagram
Status do Agente
ATIVO
Vigiando CRM e WhatsApp 24/7
Followups Hoje
0
Mensagens de cadência enviadas
Leads em Espera
0
Minerados pelo SDR-PRO

🤖 Configuração do Cérebro 360

📲 Conexão WhatsApp Web

Escaneie para vincular o WhatsApp da Academia

Aguardando Conexão

📝 Logs de Atividade do Agente

[18:15:02] Agente 360 Inicializado...
[18:15:05] Sincronizando com Pacto ERP...
[18:15:10] SDR-PRO: 12 novos leads minerados no Instagram.
[18:15:15] Aguardando comando para iniciar disparos...