Como o CRM 1-a-1 funciona por dentro
Referência para quem mantém o sistema: arquitetura, arquivos, dados, fluxos e publicação. O que mudou em cada versão está no histórico de versões.
1. Visão geral
O CRM 1-a-1 organiza a prospecção de reuniões individuais (1-a-1) entre membros do BNI. Cada pessoa tem a própria conta e enxerga só os próprios contatos. Com ele é possível:
- acompanhar cada contato por um funil de sete etapas;
- ver no radar os follow-ups atrasados ou marcados para hoje;
- abrir o WhatsApp já com uma mensagem personalizada;
- registrar como foi cada 1-a-1 (formato, observações e nota de 1 a 5 estrelas);
- importar listas por CSV ou planilha Excel;
- acompanhar os resultados no painel “Meus 1-a-1”.
Novas contas só entram depois de aprovadas por um administrador.
2. Arquitetura
O sistema tem duas partes:
- Tela (front-end): HTML, CSS e JavaScript puros, sem framework e sem etapa de build. Os arquivos são servidos como estão e rodam inteiros no navegador.
- Servidor (back-end): PocketBase, que guarda contas e contatos num banco SQLite, faz a autenticação e aplica as regras de acesso. A tela conversa com ele só pela API REST, em JSON.
O próprio PocketBase entrega os arquivos da tela, a partir da pasta pública dele. Um proxy HTTPS fica na frente. Tela e API estão no mesmo domínio, então não há problema de CORS.
Navegador ──HTTPS──► proxy ──► PocketBase
├─ / arquivos da tela (index.html, css/, js/)
├─ /api/... API REST (contas, contatos, arquivos)
└─ banco SQLite (users, contatos)
O endereço do servidor fica fixo no index.html, no campo oculto pb-url.
3. Estrutura dos arquivos
Os scripts são “clássicos” (não são módulos ES): todos compartilham o escopo global, e os botões chamam as funções direto pelo onclick. A ordem de carregamento importa e é a da tabela abaixo. O inicio.js vem por último porque é o único que executa código ao carregar.
| Arquivo | Responsabilidade | Principais funções |
|---|---|---|
index.html | Estrutura das telas (entrada e app) e de todos os modais | — |
css/estilo.css | Todo o visual. Cores como variáveis em :root | — |
js/config.js | Constantes, contatos de exemplo, etapas do funil, mensagem padrão, sugestões de observação e o objeto estado | lerObsModelos |
js/utils.js | Utilidades gerais | $, esc, toast, mostrarCarregando, hojeISO, formatarData, situacaoPrazo, copiarTexto, estadoParaSigla |
js/api.js | Comunicação com o PocketBase, cadastro, login e sessão | pb, entrar, cadastrar, carregarContatosPB, salvarSessao |
js/conta.js | Mensagem padrão do WhatsApp, menu da conta, foto/logo e aprovações (admin) | montarMensagem, linkWhatsApp, alterarAvatar, aprovarUsuario, recusarUsuario |
js/importacao.js | Importação por CSV, por planilha Excel e dos contatos de exemplo | parseCSV, importarMeusContatos, lerPlanilhaExcel, autoMapearColunas, importarDaPlanilha |
js/contatos.js | Lista, busca, filtros, funil, radar, paginação, seleção em massa, criação, edição, exclusão e backup | renderizar, filtrados, renderizarLista, renderizarRadar, salvarContato, confirmarExclusaoEmMassa |
js/visualizacao.js | Ficha do contato, estrelas, campos que mudam conforme a etapa, sugestões de observação | abrirVisualizacao, salvarRapido, atualizarBlocosStatus, marcarContatado |
js/painel.js | Painel “Meus 1-a-1” | renderizarPainel11 |
js/rotas.js | Links diretos: cada janela aberta escreve o endereço depois do #, e abrir o endereço vai direto para ela | definirRota, limparRota, aplicarRota, copiarLinkContato |
js/agenda.js | Integração com o UM Planner: conexão (Minha agenda) e envio do 1-a-1 marcado como tarefa | carregarAgenda, salvarAgenda, enviarParaAgenda, tarefaDaAgenda, assinaturaAgenda, assinaturaAgendaAntiga, desenharInfoAgenda |
js/inicio.js | Atalho Esc, animação da tela de entrada e retomada da sessão ao abrir a página | calcularStats, salvarUltimasStats |
versoes.html | Histórico de versões | — |
documentacao.html | Esta página | — |
4. Modelo de dados
Coleção users (contas)
É a coleção de autenticação padrão do PocketBase, com estes campos a mais:
| Campo | Tipo | Uso |
|---|---|---|
name | texto | Nome informado no cadastro |
email | Login | |
avatar | arquivo | Foto ou logo exibida no topo |
aprovado | booleano | Conta liberada pelo admin. Sem isso, o login para na tela “Aguardando aprovação” |
admin | booleano | Mostra o menu “Aprovações pendentes” |
mensagem_padrao | texto | Modelo da mensagem do WhatsApp. Vazio = usa o padrão do sistema |
obs_modelos | texto (JSON) | Sugestões de observação personalizadas. Vazio = usa as três padrão |
Formato de cada item de obs_modelos:
{"id":"boa", "rotulo":"Conversa produtiva", "classe":"obs-boa", "estrelas":5, "texto":"... {nome} ..."}
Coleção contatos
| Campo | Tipo | Uso |
|---|---|---|
owner | relação → users | Dono do contato. Sempre a conta logada |
nome | texto, obrigatório | — |
empresa, grupo, cidade | texto | grupo é o capítulo do BNI |
estado | texto | Sigla da UF (RS, SC…) |
categoria | texto | Áreas de atuação separadas por |. Subnível depois de > (só a parte antes do > vira opção no filtro) |
telefone | texto | Formato livre |
whatsapp | texto | Só números, com 55 e DDD (ex.: 5551999998888) |
perfil | texto/URL | Link do perfil no site do BNI |
status | seleção | Etapa do funil (tabela abaixo) |
proximo_contato | data | Próximo follow-up. Na etapa “1-a-1 marcado”, é a data da reunião |
reuniao_tipo | texto | online ou presencial |
reuniao_local | texto | URL da reunião ou endereço |
reuniao_hora | texto HH:MM | Horário do 1-a-1 (v38) |
agenda_assinatura | texto | O que já foi mandado para a agenda do UM Planner (título, data, horário, formato, local e, desde a v41, o WhatsApp). Serve para só reenviar quando algo da reunião mudou (v38) |
classificacao | número 0–5 | Nota do 1-a-1 concluído (0 = sem nota) |
obs | texto longo | Observações |
Coleção agenda_conexoes (v38)
Uma linha por pessoa, com a chave de integração do UM Planner. É uma coleção própria, com regra de dono, e não um campo em users: assim a chave não fica legível para outra conta.
| Campo | Tipo | Uso |
|---|---|---|
owner | relação → users (única) | Dono da conexão |
chave | texto | Chave ump_… gerada no UM Planner |
conta | texto | Nome da conta do UM Planner, devolvido no teste da chave, só para mostrar |
Regras: listar, ver, editar e excluir só o dono (owner = @request.auth.id); criar só com owner igual à própria conta. O script de setup do banco cria a coleção já com essas regras.
Etapas do funil (status)
| Valor | Rótulo | Observação |
|---|---|---|
a_contatar | A contatar | Etapa inicial e valor usado quando a importação traz uma etapa inválida |
contatado | Contatado | Definida sozinha ao clicar em WhatsApp num contato “A contatar” |
conversando | Conversando | — |
reuniao | 1-a-1 marcado | Mostra formato e local da reunião |
concluido | 1-a-1 concluído | Mostra a classificação por estrelas |
parceiro | Parceiro / cliente | Fica fora do radar |
sem_interesse | Sem interesse | Fica fora do radar |
Para criar uma etapa nova: acrescentar em STATUS (config.js), nos dois <select> de etapa do index.html, numa classe .tag-<valor> do CSS e nas opções do campo status no PocketBase.
5. Regras de acesso no servidor
| Coleção / ação | O que o servidor deve garantir |
|---|---|
contatos: listar, ver, editar, excluir | Só o dono, e só com conta aprovada: owner = @request.auth.id && @request.auth.aprovado = true |
contatos: criar | Conta aprovada, e o owner enviado tem que ser a própria conta: @request.auth.aprovado = true && @request.body.owner = @request.auth.id |
users: criar (cadastro) | Aberto a visitantes, mas sem poder definir aprovado nem admin |
users: editar | A própria conta pode mudar nome, foto, mensagem e sugestões, mas não aprovado nem admin. Admin pode aprovar outras contas |
users: listar pendentes e excluir | Só admin (@request.auth.admin = true) |
agenda_conexoes: tudo | Só o dono. As regras já são criadas assim pelo script de setup do banco (v38) |
6. Fluxos principais
6.1 Cadastro e aprovação
- A pessoa informa nome, e-mail e senha (mínimo de 8 caracteres). A conta é criada com
aprovado = false. - A tela mostra “Aguardando aprovação”.
- O admin abre menu → Aprovações pendentes e escolhe Aprovar (
aprovado = true) ou Recusar (a conta é excluída). - A partir daí, o login funciona normalmente.
6.2 Login e sessão
- O login usa
auth-with-password. Se a conta não estiver aprovada, a tela volta para “Aguardando aprovação”. - O token da sessão fica no navegador. Ao abrir a página de novo,
auth-refreshrenova o token e recarrega os dados da conta, sem pedir a senha. - Se a renovação falhar (token vencido ou conta removida), a sessão é apagada e a tela de login aparece.
- Sair apaga a sessão do navegador.
6.3 Lista de contatos
- Ao entrar, todos os contatos da conta são carregados de uma vez, 500 por página da API, e busca, filtros e ordenação rodam no navegador. Depois de cada gravação, a lista é recarregada inteira do servidor.
- Ordem: primeiro os atrasados, depois os de hoje, os com data futura e, por fim, os sem data. Dentro de cada grupo, pela data.
- Cor do card: vermelho para atrasado, amarelo para hoje, verde para agendado.
- Busca: procura em nome, empresa, grupo, cidade, categoria e observações, sem diferenciar maiúsculas. As últimas 9 buscas (de 2 letras ou mais) viram sugestões no campo.
- Filtros:
- etapa, pelas células do funil;
- estado, com o RS sempre primeiro;
- cidade, que depende do estado escolhido;
- área, extraída da categoria;
- classificação mínima, que só aparece quando algum contato tem nota.
- Paginação: 25, 50, 100 ou 200 por página.
- “descartar” no card apaga a data do próximo contato.
6.4 Radar de follow-up
- Vencendo: contatos com próximo contato atrasado ou para hoje, exceto “Parceiro” e “Sem interesse”. Mostra até 8, dos mais antigos para os mais recentes.
- Janela de contato aberta: contatos em “Contatado” ou “Conversando” sem próximo contato definido. Mostra até 6.
- O radar pode ser recolhido. A escolha fica guardada no navegador.
6.5 WhatsApp e mensagem padrão
O botão abre https://wa.me/<whatsapp>?text=<mensagem>. A mensagem sai do modelo da conta (ou do padrão do sistema), com estes campos trocados pelos dados do contato:
| Campo | Vira |
|---|---|
{primeiro_nome} | Primeira palavra do nome |
{nome} | Nome completo |
{empresa}, {grupo}, {cidade} | O campo correspondente |
{categoria} | Só a primeira área da categoria |
Quando o contato não tem o dado, o trecho da linha em volta do campo some junto. Assim não sobra “Pelo que vi, você está no grupo de .”. Depois disso, espaços e linhas em branco repetidos são limpos.
Clicar em WhatsApp num contato “A contatar” move o contato para “Contatado” automaticamente.
6.6 Ficha do contato
- Clicar num card abre a ficha. Nela dá para mudar a etapa, a data, as observações e, conforme a etapa:
- 1-a-1 marcado: o campo de data passa a se chamar “Data para 1-a-1” e aparecem formato (online ou presencial) e local (URL ou endereço);
- 1-a-1 concluído: aparecem as estrelas de 1 a 5.
- Sugestões de observação preenchem o texto (
{nome}vira o primeiro nome) e, se a sugestão tiver nota, marcam as estrelas. Elas são editáveis em “✎ editar” e ficam salvas na conta (obs_modelos). - “Editar tudo” abre o formulário completo: nome, empresa, grupo, cidade, estado, WhatsApp, etapa, data e observações.
- Os botões ⧉ copiam cada campo.
6.7 Importação
Por CSV. A tela traz um prompt pronto para levar a uma IA junto com a lista bagunçada. A IA devolve um CSV com estas colunas:
nome,empresa,grupo,cidade,estado,categoria,telefone,whatsapp,perfil,status,proximo_contato,obs
Separador é vírgula, e texto com vírgula vai entre aspas duplas. Linhas sem nome são ignoradas. Na importação:
- etapa inválida vira
a_contatar; - o estado vai para maiúsculas;
- o WhatsApp fica só com os números.
Por planilha Excel (.xlsx/.xls). O arquivo é lido no próprio navegador, pela biblioteca SheetJS.
- A aba com mais linhas vem selecionada.
- As colunas são ligadas aos campos automaticamente por sinônimos do cabeçalho (ex.: “Celular” → WhatsApp; “Capítulo” → grupo; “UF” → estado), e a pessoa pode corrigir antes de importar.
- Estado escrito por extenso vira sigla.
Contatos de exemplo. Há três contatos de teste embutidos no código. A importação deles pula os que já existem (compara pelo WhatsApp ou, sem WhatsApp, pelo nome).
Todas as importações gravam um contato por requisição, em sequência, e no fim mostram quantos entraram e quantos deram erro.
6.8 Exclusão em massa
Em ☑ Selecionar, os cards viram caixas de seleção, e dá para marcar a página inteira. Para confirmar, é preciso digitar EXCLUIR. Cada contato é excluído com uma requisição própria.
6.9 Painel “Meus 1-a-1”
- Concluídos nos últimos 7 e 30 dias: contatos em “1-a-1 concluído” cuja última alteração caiu nesse período.
- Total concluídos e marcados (a fazer), pela etapa atual.
- Média das notas e distribuição de 1 a 5 estrelas, só entre os concluídos com nota.
- Formato dos encontros (online × presencial), entre os marcados e concluídos com formato informado.
6.10 Agenda no UM Planner (v38)
Conectar:
- No UM Planner, a pessoa gera uma chave de integração em Configurações → IA e integrações → Integrações.
- No CRM, cola a chave em menu → Minha agenda (UM Planner).
- “Testar e salvar” confere a chave no UM Planner (
teste: true) antes de guardar emagenda_conexoes. A conexão é carregada no login e na retomada da sessão (carregarAgenda).
Enviar: depois de salvar na ficha (salvarRapido) ou em “Editar tudo” (salvarContato), enviarParaAgenda manda o contato quando:
- a etapa é 1-a-1 marcado;
- existe data;
- há agenda conectada;
- a assinatura mudou.
A chamada vai direto do navegador para o UM Planner:
POST https://umplanner.com/api/umplanner/receber-tarefa
{ "chave": "ump_…", "origem": "crm-bni", "idExterno": "<id do contato>",
"titulo": "1-a-1: Nome (Empresa)", "data": "AAAA-MM-DD", "horario": "HH:MM",
"formato": "online|presencial", "endereco": "…", "url": "https://…",
"anotacoes": "link da ficha no CRM, contato, empresa, telefone, WhatsApp (wa.me), grupo, cidade, perfil, observações",
"clienteNome": "Nome", "clienteWhatsapp": "5551999998888", "soAtualizar": true|ausente }
- O cliente (v41):
clienteNomeeclienteWhatsapp(owhatsappdo contato, só dígitos com 55) preenchem o Retorno por WhatsApp da reunião no UM Planner (v160 de lá), onde o número ganha o botão “Abrir” (wa.me). O que a pessoa corrigir lá não é trocado pelo CRM. - Link da ficha (v40): a 2ª linha das anotações é “Abrir no CRM:
<servidor>/#contato/<id>” (ver 6.11). O endereço sai deestado.url, não da aba: aberto com dois cliques (file://), o link também aponta para o servidor. O UM Planner transforma o endereço em link clicável. Só abre para quem está logado na conta dona do contato. - Local: se
reuniao_localcomeça com http(s), vai comourl; senão, comoendereco. - Sem duplicar: o UM Planner usa
origem+idExternopara atualizar sempre a mesma tarefa. - Quando reenvia: só quando título, data, horário, formato, local ou WhatsApp mudam (
agenda_assinatura, o WhatsApp entrou na v41). Mudar só a observação não reenvia. - Contatos mandados antes da v41: a assinatura guardada não tem o WhatsApp (
assinaturaAgendaAntiga). Se o resto é igual, o próximo salvamento manda comsoAtualizar: true: o UM Planner só completa a tarefa que já existe, e uma tarefa apagada lá respondeacao: "ignorada", sem voltar e sem aviso. Se a reunião mudou, o reenvio é o normal, como sempre. - Se o envio falhar, o contato continua salvo no CRM e o aviso diz o motivo. Chave revogada (401) pede uma chave nova.
- Sem agenda conectada, o primeiro salvamento de 1-a-1 marcado da sessão lembra de conectar.
- Na ficha, uma linha embaixo do formato diz se o 1-a-1 já está na agenda (
desenharInfoAgenda).
6.11 Links diretos (v39)
Toda janela aberta escreve o seu endereço depois do #. Copiar esse endereço (ou usar o botão 🔗 Link da ficha) e abrir depois vai direto para a janela.
| Endereço | Abre |
|---|---|
#contato/<id> | Ficha do contato |
#editar/<id> | Edição completa (“Editar tudo”) |
#novo | Novo contato |
#meus-1a1 | Painel Meus 1-a-1 |
#agenda | Minha agenda (UM Planner) |
#mensagem | Mensagem padrão do WhatsApp |
#importar | Importar contatos |
#aprovacoes | Aprovações pendentes (só admin) |
- Antes do login: um link aberto com a pessoa deslogada abre a janela depois que ela entra, porque o
#fica no endereço (aplicarRota, no fim domostrarApp). - Fechar a janela apaga o
#, e só se ele ainda for daquela janela. O Esc fecha todas de uma vez, e trocar da ficha para “Editar tudo” fecha uma e abre outra. - Histórico do navegador: usa
history.replaceState, nuncapushState. O endereço acompanha a janela sem encher o histórico, e o voltar do navegador continua como antes. - Endereço trocado na mesma aba (evento
hashchange): fecha o que estava aberto e abre o que o novo endereço pede. - Contato de outra conta ou excluído: aparece um aviso e nada abre. Os contatos são de cada pessoa, então o link só funciona para quem está logado na conta dona do contato.
6.12 Backup
menu → Backup baixa todos os contatos da conta num arquivo prospeccao-bni-backup-AAAA-MM-DD.json. Hoje não existe importação desse arquivo pela tela.
7. Chamadas à API
Todas passam pela função pb() (api.js), que envia JSON e o cabeçalho Authorization com o token da sessão. Qualquer resposta de erro vira uma exceção com a mensagem do PocketBase.
| Método | Caminho | Uso |
|---|---|---|
| POST | /api/collections/users/records | Cadastro |
| POST | /api/collections/users/auth-with-password | Login |
| POST | /api/collections/users/auth-refresh | Retomar a sessão ao abrir a página |
| PATCH | /api/collections/users/records/:id | Mensagem padrão, sugestões, foto (multipart, campo avatar) e aprovação pelo admin |
| GET | /api/collections/users/records?filter=aprovado=false | Contas pendentes (admin) |
| DELETE | /api/collections/users/records/:id | Recusar cadastro (admin) |
| GET | /api/files/users/:id/:arquivo | Exibir a foto da conta |
| GET | /api/collections/contatos/records?perPage=500&page=N&sort=-updated | Carregar todos os contatos |
| POST | /api/collections/contatos/records | Criar contato e importações |
| PATCH | /api/collections/contatos/records/:id | Salvar, mudar etapa, descartar data |
| DELETE | /api/collections/contatos/records/:id | Excluir (individual e em massa) |
| GET/POST/PATCH/DELETE | /api/collections/agenda_conexoes/records | Conexão com a agenda do UM Planner |
| POST | https://umplanner.com/api/umplanner/receber-tarefa | Outro sistema: testar a chave e criar/atualizar a tarefa do 1-a-1 (não passa por pb(), e sim por chamarUMPlanner) |
9. Dependências externas
- Google Fonts: Montserrat e Inter. Sem elas, a tela usa a fonte do sistema.
- SheetJS 0.18.5 (cdnjs): leitura de planilhas Excel. Sem ela, a importação por CSV continua funcionando e a de Excel avisa o erro.
- wa.me: link oficial do WhatsApp para abrir a conversa.
- UM Planner (umplanner.com, v122 ou mais nova; v160 ou mais nova para o nome e o WhatsApp do cliente chegarem ao Retorno): recebe os 1-a-1 marcados. Fora do ar, o CRM continua funcionando e só avisa que o envio falhou.
Não há npm, bundler nem outra dependência de build.
10. Versões e publicação
O número da versão é inteiro e sobe de um em um. Ele aparece no rodapé do CRM e fica marcado de três formas no index.html:
- no comentário
<!-- VERSAO: NN -->, lido pela conferência pós-publicação; - no rodapé visível (
span.app-versao); - no sufixo
?v=NNdos links de CSS e JavaScript. É ele que obriga o navegador a buscar os arquivos novos depois de uma atualização, em vez de usar os antigos do cache.
Passo a passo de uma versão nova
- Fazer as mudanças em
app/e testar abrindoapp/index.htmlno navegador. Ele conversa com o servidor de verdade. - Registrar a versão em
versoes.htmle, se mudou algo técnico, atualizar esta documentação e o número no comentário do topo dela. - Rodar
empacotar.ps1 -Versao NNno Windows. Ele:- carimba a versão;
- confere que todo arquivo pedido pelo
index.htmlexiste; - recusa publicar se estas páginas públicas contiverem detalhes de infraestrutura;
- gera a lista de conferência SHA-256 e o pacote
crm_bni_vNN.zip.
- No servidor, rodar o script de deploy. Ele:
- busca o pacote de número mais alto e confere a integridade de cada arquivo;
- guarda uma cópia do que estava no ar;
- publica só os arquivos da lista;
- mantém as últimas 4 versões;
- confirma que o site responde com a versão nova.
Versão que muda o banco (campo ou coleção nova, como a v38): depois do deploy, rodar o script de setup do banco (setup-bni.sh). Ele chega junto no pacote, pede a senha do superusuário e só acrescenta o que falta. Rodar de novo é seguro.
Voltar uma versão: rodar o script de deploy com o número desejado (ex.: deploy-bni.sh 36).
11. Como manter o código
- Idioma: nomes de funções, variáveis e textos em português do Brasil, como no resto do código.
- Texto do usuário no HTML: sempre passar por
esc()antes de colocar eminnerHTML. Todo dado vindo do servidor ou de importação é tratado como texto não confiável. - Função nova chamada por botão: declarar no nível de cima do arquivo, para ficar global. É assim que o
onclickencontra a função. - Arquivo JS novo: incluir no
index.html, na posição certa, antes doinicio.js, e com?v=NN. O empacotador carimba a versão sozinho. - Nomes globais: não repetir
const/letentre arquivos. Como todos dividem o mesmo escopo, a repetição trava o carregamento. - Cores: usar as variáveis de
:rootemestilo.css. - Campo novo no contato: acrescentar no
setup-bni.sh(nunca só pelo painel, para ficar registrado), incluir emcarregarContatosPB(api.js) e nos pontos de gravação (salvarContato,salvarRapido, importações).
12. Limitações conhecidas
- “Editar tudo” só oferece RS e SC no campo estado. Um contato de outro estado aberto nesse formulário perde o estado ao salvar. O formulário também não edita categoria, telefone nem perfil.
- Datas vindas de planilha Excel podem chegar como número de série (ex.: 45890) e ser recusadas pelo servidor. A linha é contada como erro.
- O CSV não aceita quebra de linha dentro de um campo entre aspas, nem separador
;(o padrão do Excel em português). - Nas sugestões de observação, só a primeira ocorrência de
{nome}é trocada. - Os números de 7 e 30 dias do painel usam a data da última alteração do contato. Editar a observação de um 1-a-1 antigo faz ele contar de novo.
- Importação e exclusão em massa fazem uma requisição por contato, em sequência: com centenas de itens, demoram. A importação de CSV e Excel não detecta contatos duplicados.
- A busca diferencia acentos (“goncalves” não encontra “Gonçalves”).
- Mudar a etapa pelo WhatsApp ou descartar uma data atualiza a tela antes de confirmar no servidor. Se a gravação falhar, não aparece aviso.
- Nomes fixos no código: a mensagem padrão do sistema, o destaque do RS no filtro e o grupo destacado na tela de entrada estão escritos direto no código, e não configuráveis por conta.
Documentação da versão 41 · Histórico de versões · Voltar ao CRM