From 4f736562f3af15a01dd07e9f7cd12f77db01a679 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Sat, 1 Aug 2026 10:12:25 -0300 Subject: [PATCH] Carry over pre-existing WABA outreach automation WIP This work already existed uncommitted in the main working tree (not authored by this branch) and is required as a runtime dependency for the campaign outreach port (zpro-client, templates, outreach routes/UI). Copied in as-is, untouched, so this feature branch has a working baseline; the original uncommitted state in the main checkout is left exactly as it was. --- .env.example | 15 +- README.md | 223 +++++-- docs/01-visao-geral.md | 13 +- docs/08-automacao-envio.md | 133 ++++ logo-gti-256x256.png | Bin 0 -> 12541 bytes package-lock.json | 15 + package.json | 1 + prospeccao/cnpj/README.md | 15 +- public/css/app.css | 863 ++++++++++++++++++------- public/img/logo-gti.png | Bin 0 -> 12541 bytes public/index.html | 145 ++++- public/js/api.js | 5 + public/js/app.js | 553 +++++++++++++++- public/js/tratamento.js | 31 +- public/login.html | 10 +- scripts/enviar-outreach.mjs | 24 + scripts/servidor.mjs | 2 + src/capture/runner.mjs | 4 +- src/db.mjs | 141 +++- src/env.mjs | 13 +- src/outreach/run.mjs | 214 ++++++ src/outreach/scheduler.mjs | 29 + src/outreach/templates.mjs | 65 ++ src/outreach/waba-template-library.mjs | 104 +++ src/outreach/zpro-client.mjs | 212 ++++++ src/server/index.mjs | 2 + src/server/routes/meta.mjs | 19 +- src/server/routes/outreach.mjs | 205 ++++++ tests/db.test.mjs | 18 +- tests/outreach-routes.test.mjs | 253 ++++++++ tests/outreach.test.mjs | 276 ++++++++ tests/waba-template-library.test.mjs | 40 ++ tests/zpro-client.test.mjs | 236 +++++++ 33 files changed, 3537 insertions(+), 342 deletions(-) create mode 100644 docs/08-automacao-envio.md create mode 100644 logo-gti-256x256.png create mode 100644 package-lock.json create mode 100644 public/img/logo-gti.png create mode 100644 scripts/enviar-outreach.mjs create mode 100644 src/outreach/run.mjs create mode 100644 src/outreach/scheduler.mjs create mode 100644 src/outreach/templates.mjs create mode 100644 src/outreach/waba-template-library.mjs create mode 100644 src/outreach/zpro-client.mjs create mode 100644 src/server/routes/outreach.mjs create mode 100644 tests/outreach-routes.test.mjs create mode 100644 tests/outreach.test.mjs create mode 100644 tests/waba-template-library.test.mjs create mode 100644 tests/zpro-client.test.mjs diff --git a/.env.example b/.env.example index a8a643f..580b1d1 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,17 @@ # Senha pedida para confirmar a limpeza total da base (Configuracoes > Zona de risco) #GTI_RESET_PASSWORD=troque-esta-senha +# Credenciais da API externa do Z-PRO (Admin > API no painel do Z-PRO), usadas +# pela automacao de envio WhatsApp. Normalmente configuradas direto na tela +# (Configuracoes > Credenciais Z-PRO) — essas variaveis aqui sao so um +# fallback (util em deploy via Docker/Proxmox) e so valem se os campos da +# tela estiverem vazios. Sem credenciais em nenhum dos dois lugares a +# automacao fica sem efeito mesmo com outreach.ativo=true. +#ZPRO_BASE_URL=seu-dominio-zpro.com.br +#ZPRO_API_ID= +#ZPRO_BEARER_TOKEN= +#ZPRO_WHATSAPP_ID= + # Endpoints das fontes gratuitas (padroes ja funcionam; mude so se precisar) #OVERPASS_API_URL=https://overpass-api.de/api/interpreter,https://overpass.kumi.systems/api/interpreter #NOMINATIM_API_URL=https://nominatim.openstreetmap.org/search @@ -23,5 +34,5 @@ # Ritmo das consultas ao OpenStreetMap (ms) #OVERPASS_REQUEST_DELAY_MS=5000 -#OVERPASS_TIMEOUT_MS=45000 -#OVERPASS_TEXT_SEARCH=true +#OVERPASS_TIMEOUT_MS=15000 +#OVERPASS_TEXT_SEARCH=false diff --git a/README.md b/README.md index eca8b00..974fb54 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,192 @@ -# GTI Máquina Comercial +# GTI Maquina Comercial -Aplicativo local do Grupo GTI para **capturar** e **tratar** leads B2B em qualquer -estado/cidade do Brasil, com foco na base oficial de CNPJs da Receita Federal. +Aplicativo local do Grupo GTI para capturar, qualificar, tratar e acionar leads B2B via WhatsApp. O sistema roda no computador do usuario, usa SQLite como banco local e integra com Z-PRO/GChat para envio manual ou automatico. -## Como usar (dia a dia) +URL local padrao: `http://127.0.0.1:3900` -1. Dê dois cliques em **`GTI-Comercial.exe`** (ou em `INICIAR-GTI.bat`). -2. O navegador abre em `http://127.0.0.1:3900` com duas abas: - - **📡 Capturar Leads** — escolha estado, cidades, nichos e fontes e rode a captura - com progresso ao vivo. Se a base CNPJ do estado ainda não existe, o botão - "Extrair base agora" prepara tudo (~40–60 min, uma única vez por estado). - - **📊 Tratar Leads** — dashboard com totais e rankings por cidade/nicho, navegador - de leads com filtros (qualidade, WhatsApp, exportado...) e o botão - **Gerar relatório / exportação** que produz os arquivos para envio. +Login inicial: `admin` / `admin` -Tudo roda somente neste computador (porta local 127.0.0.1). Nenhum dado sai da máquina, -exceto as consultas às fontes públicas durante a captura. +## Uso Diario -## Exportações disponíveis +1. Abra `GTI-Comercial.exe` ou `INICIAR-GTI.bat`. +2. Acesse `http://127.0.0.1:3900` e faca login. +3. Use a aba `Capturar Leads` para escolher UF, cidades, nichos e fontes. +4. Use a aba `Tratar Leads` para filtrar, priorizar, abrir detalhes e exportar leads. +5. Use o botao de WhatsApp na primeira coluna da tabela para iniciar uma mensagem para um lead especifico. +6. Use `Configuracoes` para ajustar Z-PRO, templates WABA, limites de envio, usuarios e exportacoes. -| Tipo | Para quê | +## Principais Recursos + +- Captura de leads B2B por UF, cidade e nicho. +- Base CNPJ da Receita Federal como fonte principal. +- Fontes complementares: OpenStreetMap/Overpass, Nominatim, Wikidata, BrasilAPI e Google Places. +- Deduplicacao por CNPJ, telefone/WhatsApp ou empresa+cidade. +- Scores de qualidade e potencial comercial. +- Dashboard com totais, rankings, funil e maior oportunidade. +- Tabela de tratamento com filtros, paginação e detalhe completo do lead. +- Acao rapida de WhatsApp por lead, fixa na primeira coluna da tabela. +- Envio manual por Z-PRO para numero especifico. +- Envio oficial por template WABA aprovado. +- Sincronizacao de templates WABA do GChat. +- Pre-visualizacao de templates com variaveis resolvidas por lead. +- Automacao de envio por produto recomendado, com limite diario. +- Exportacoes para Excel/CSV, oportunidades Z-PRO e disparo em massa. +- Login local com usuarios `admin` e `comercial`. + +## Fluxo Comercial + +1. Capture ou importe a base de leads. +2. Filtre por cidade, nicho, qualidade, WhatsApp, prioridade ou status de exportacao. +3. Revise o lead na tabela ou no modal de detalhes. +4. Clique no botao de WhatsApp do lead para abrir o envio manual com o numero preenchido. +5. Escolha texto livre ou template oficial. +6. Se usar template, selecione o modelo salvo e confira a pre-visualizacao. +7. Envie pelo Z-PRO e acompanhe o log de envios. + +Ao abrir o envio manual a partir de um lead, o app busca os dados completos desse cliente. Variaveis como `{{cnpj}}`, `{{empresa}}`, `{{businessName}}`, `{{produto}}`, `{{cidade}}`, `{{uf}}`, `{{segmento}}`, `{{whatsapp}}` e `{{email}}` sao preenchidas com os dados reais do lead na pre-visualizacao e no envio. + +## WhatsApp, Z-PRO e GChat + +O app suporta dois tipos de envio: + +| Canal | Uso | Observacao | +|---|---|---| +| Nao oficial/texto livre | Mensagem manual simples | Usa endpoint externo do Z-PRO para enviar texto | +| Oficial/template WABA | Mensagem com template aprovado | Usa template Meta/WABA via Z-PRO | + +Configuracoes importantes ficam em `Configuracoes`: + +- Base URL Z-PRO. +- API ID Z-PRO. +- Token/Bearer token. +- Rota do template (`template`, `templateBody` ou equivalente configurado). +- URL de templates do GChat. +- Token GChat opcional para sincronizar modelos. +- Limite diario de envios. +- Templates automaticos por produto. + +O seletor de templates WABA permite carregar modelos sincronizados do GChat. O sistema identifica variaveis posicionais como `{{1}}`, `{{2}}`, `{{3}}` no corpo do template e mostra os campos necessarios. + +## Variaveis Suportadas + +As mensagens e templates podem usar variaveis internas da Maquina Comercial/GChat: + +| Variavel | Valor esperado | |---|---| -| 📊 Planilha (CSV + Excel) | Lista completa com score — números como texto, sem notação científica | -| 🗂️ Z-PRO Oportunidades (JSONL) | Um payload `createOpportunity` por linha, pronto para importar | -| 📨 Z-PRO Disparo em massa (CSV) | `dataInput` para `BulkSendMessageWithVariable` + instruções | +| `{{name}}` | Nome do contato quando existir; fallback para empresa | +| `{{empresa}}` | Nome da empresa | +| `{{businessName}}` | Nome da empresa no padrao GChat | +| `{{firstName}}` | Primeiro nome | +| `{{lastName}}` | Sobrenome, quando existir | +| `{{produto}}` | Produto recomendado/interesse | +| `{{protocol}}` | Assunto/protocolo; fallback para produto | +| `{{cidade}}` | Cidade | +| `{{uf}}` | UF | +| `{{segmento}}` | Nicho/segmento | +| `{{cnpj}}` | CNPJ | +| `{{email}}` | E-mail | +| `{{phoneNumber}}` | WhatsApp/telefone | +| `{{whatsapp}}` | WhatsApp/telefone | +| `{{kanban}}` | Produto/interesse | +| `{{user}}` | Remetente | +| `{{userEmail}}` | E-mail do remetente | +| `{{greeting}}` | Saudacao | -Leads exportados ficam marcados — o filtro "ainda não exportados" evita reenvio. -Os IDs padrão do Z-PRO (pipeline/etapa/responsável) ficam no ⚙️ Configurações. +Tambem existem placeholders legados: `{empresa}`, `{cidade}`, `{produto}` e `{segmento}`. -> O `.xls` gerado é uma tabela HTML (o Excel avisa sobre o formato ao abrir — é normal, -> basta confirmar). Use o `.csv` se preferir. +## Exportacoes -## Qualidade dos leads (score) +| Tipo | Saida | Para que serve | +|---|---|---| +| Planilha | CSV + XLS | Analise comercial e tratamento externo | +| Z-PRO Oportunidades | JSONL | Payloads para criacao de oportunidades | +| Z-PRO Disparo em massa | CSV | `dataInput` para disparo em massa | -Score: +35 telefone válido, +25 CNPJ válido, +25 empresa válida, +5 cidade, -+5 nicho, +5 CNAE. **Bom ≥ 85**, médio 60–84, ruim < 60. Empresa vazia, com lixo -ou começando com número é sempre **ruim** (regra crítica). Duplicidade: CNPJ → -WhatsApp → empresa+cidade. +Leads exportados ficam marcados. Use o filtro de nao exportados para evitar retrabalho. -## Fontes de captura +## Score e Priorizacao -- **Base CNPJ (Receita Federal)** — principal: todas as empresas ativas da UF, ~97% com telefone. -- **OpenStreetMap** (Overpass e Nominatim) — grátis, complementar. -- **Wikidata** — grátis, volume baixo. -- **BrasilAPI** — enriquecimento opcional da base CNPJ. -- **Google Places** — paga; preencha `GOOGLE_MAPS_API_KEY` no `.env`. +O app calcula scores para ajudar o time comercial a decidir quem abordar primeiro. -## Novo estado (ex.: expandir para Sergipe) +Regras principais: -1. Na aba Captura, escolha **SE — Sergipe**. -2. Clique em **Extrair base agora** e aguarde (usa os zips já baixados em - `prospeccao/cnpj/rfb-raw/`). -3. Pronto: capture qualquer cidade de SE normalmente. Nada muda na interface. +- WhatsApp/telefone valido aumenta a prioridade. +- CNPJ valido aumenta a confiabilidade. +- Empresa valida, cidade, nicho, CNAE, website e e-mail melhoram o score. +- Nichos com maior aderencia comercial recebem mais peso. +- Leads sem WhatsApp podem continuar cadastrados, mas aparecem com prioridade menor. +- Duplicidade segue a ordem CNPJ, telefone, empresa+cidade. -Os zips brutos da Receita (7 GB, Brasil inteiro) já estão no projeto. Para atualizá-los -no futuro (a Receita publica mensalmente): `bash scripts/baixar-base-rfb.sh`. +## Fontes de Captura -## Uso avançado (linha de comando) +- Base CNPJ da Receita Federal: fonte principal para empresas ativas por UF. +- OpenStreetMap/Overpass: fonte aberta complementar. +- Nominatim: geocodificacao e apoio geografico. +- Wikidata: fonte aberta de baixo volume. +- BrasilAPI: enriquecimento de dados cadastrais. +- Google Places: fonte opcional paga, exige `GOOGLE_MAPS_API_KEY` no `.env`. + +## Comandos ```bash -npm start # sobe o servidor -npm test # roda os testes +npm start +npm test +npm run capture +npm run extract +npm run recalc-scores +npm run outreach +npm run build:launcher +``` + +Exemplos: + +```bash +node scripts/servidor.mjs node scripts/capturar-leads.mjs --sources=cnpj-base --uf=BA --cities="Ilheus" -node scripts/extrair-base-cnpj.mjs --uf=SP # extrai base de outra UF +node scripts/extrair-base-cnpj.mjs --uf=SP +node scripts/enviar-outreach.mjs ``` -## Estrutura do projeto +## Estrutura -``` -GTI-Comercial.exe inicia tudo (servidor + navegador) -scripts/ entradas de linha de comando -src/ código do servidor, fontes de captura, exportações -public/ interface (HTML/CSS/JS) -data/gti.db banco local (SQLite) com todos os leads -data/exports/ arquivos de exportação gerados -prospeccao/cnpj/ base CNPJ extraída por UF + zips brutos da Receita -docs/ playbook comercial, LGPD e referência da API Z-PRO +```text +GTI-Comercial.exe Launcher local do app +INICIAR-GTI.bat Inicializacao alternativa no Windows +scripts/ Entradas de linha de comando +src/ Codigo do servidor e regras de negocio +src/server/ HTTP, rotas, sessoes e autenticacao +src/capture/ Pipeline de captura +src/sources/ Fontes de dados +src/treatment/ Consultas e dashboard de tratamento +src/export/ Exportacoes Excel/CSV/Z-PRO +src/outreach/ Z-PRO, templates WABA e automacao WhatsApp +public/ Interface HTML/CSS/JS +tests/ Testes automatizados +data/gti.db Banco SQLite local, nao versionado +data/exports/ Arquivos exportados, nao versionado +prospeccao/cnpj/ Base CNPJ e dados brutos da Receita +docs/ Documentacao complementar +.env Segredos locais, nao versionado ``` ## Requisitos -- Windows com **Node.js 24+** (https://nodejs.org) -- Para extrair novas UFs: `unzip` (vem com o Git para Windows) ou o `tar` nativo do Windows +- Windows. +- Node.js 24 ou superior. +- Para extrair novas UFs, `unzip`, Git Bash ou `tar` nativo do Windows. -## Log e problemas +## Seguranca e Dados -- Log do servidor: `data/server.log` -- Se a tela não abrir: rode `node scripts/servidor.mjs` num terminal e veja a mensagem. +- O banco principal fica em `data/gti.db`. +- Segredos locais devem ficar no `.env` ou nas configuracoes locais do app. +- Tokens Z-PRO/GChat nao devem ser commitados. +- A zona de risco em Configuracoes apaga leads, capturas e exportacoes; a acao e irreversivel. +- Nao ha backup automatico do banco local. + +## Problemas Comuns + +- Se a tela nao abrir, reinicie por `GTI-Comercial.exe` ou `INICIAR-GTI.bat`. +- Se preferir depurar, rode `node scripts/servidor.mjs` no terminal. +- Log do servidor: `data/server.log`. +- Se a sincronizacao de templates GChat falhar com erro de token, gere/salve um token GChat valido novamente. +- Se um template WABA falhar no Z-PRO, teste outro template aprovado e confira rota, idioma e variaveis obrigatorias. diff --git a/docs/01-visao-geral.md b/docs/01-visao-geral.md index 360cbb4..f957433 100644 --- a/docs/01-visao-geral.md +++ b/docs/01-visao-geral.md @@ -24,9 +24,12 @@ GTI-Comercial.exe → servidor Node local (porta 3900) → navegador └── Tratamento: dashboard, filtros, relatórios e exportações Z-PRO ``` -Sem n8n, sem Docker, sem serviços externos obrigatórios: tudo roda no computador -da GTI. O envio das mensagens é feito por fora (Z-PRO), a partir dos arquivos -gerados pelo módulo de tratamento. +Sem n8n, sem serviços externos obrigatórios: tudo roda no computador da GTI hoje +(e vai rodar num container Docker no Proxmox da empresa no futuro, sem mudança de +código). O primeiro contato via WhatsApp pode ser automático (ver +[08-automacao-envio.md](08-automacao-envio.md)); campanhas maiores e o +acompanhamento seguem por fora, no Z-PRO, a partir dos arquivos gerados pelo +módulo de tratamento. ## Premissas @@ -53,3 +56,7 @@ gerados pelo módulo de tratamento. - Acesso protegido por login (usuário/senha): o primeiro acesso vem com `admin`/`admin`, que pode cadastrar novos usuários (comercial ou admin) pela tela de Usuários. +- Automação de primeiro contato via WhatsApp (opcional, desligada por padrão): + manda uma mensagem com o template do produto recomendado pra cada lead ainda + não exportado, registra o que foi mandado e aplica tags no contato do Z-PRO — + ver [08-automacao-envio.md](08-automacao-envio.md). diff --git a/docs/08-automacao-envio.md b/docs/08-automacao-envio.md new file mode 100644 index 0000000..bfbe958 --- /dev/null +++ b/docs/08-automacao-envio.md @@ -0,0 +1,133 @@ +# Automação de envio WhatsApp (Z-PRO) + +Manda automaticamente a primeira mensagem de WhatsApp pro produto/serviço +recomendado de cada lead, usando um template por produto, e registra tudo +(quem recebeu, qual produto, quando) pra nunca mandar a mesma coisa duas +vezes pro mesmo lead. + +## Como funciona + +- Roda **sozinha, dentro do próprio servidor**, a cada 20 minutos, enquanto + o app estiver ligado — não depende de Agendador de Tarefas do Windows nem + de cron. Isso é de propósito: o projeto hoje roda local, mas vai ser + migrado pra um container Docker no Proxmox da empresa, e um agendador do + sistema operacional teria que ser reconfigurado nesse dia. O timer interno + funciona idêntico nos dois ambientes. +- Só considera leads **ainda não exportados** (`exported_at` vazio). Assim + que um lead vira oportunidade manual no Z-PRO (exportação normal da tela + de Tratamento), a automação para de mandar mensagem pra ele — quem assumiu + o lead foi um vendedor, não o robô. +- Cada lead tem uma lista de produtos recomendados por nicho (ex.: ERP 5⭐, + G-Delivery 3⭐). Hoje a automação só tenta o **produto principal** (o de + maior estrela), uma única vez por lead. Existe uma segunda etapa pronta + (mas desligada) pra tentar o próximo produto da lista se não houver + resposta depois de alguns dias — ver `outreach.tentarProximoProduto` + abaixo. +- Manda uma mensagem por número, com controle nosso de quem já recebeu o + quê — não pelo `bulkSendMessageWithVariable` usado na exportação manual + em massa, que depende de um CSV com substituição posicional de variável. + Cada produto tem um **canal** próprio (ver seção abaixo). +- Depois de mandar, aplica duas tags no contato do Z-PRO: + `GTI P1`/`GTI P2`/`GTI P3`/`GTI P4` (prioridade do lead) e + `GTI Oferta: ` (o que foi ofertado) — pra quem for atender depois + já ver esse contexto direto na conversa. + +## Canal oficial x não-oficial + +Cada produto/template tem um seletor de canal em Configurações: + +- **Não-oficial (texto livre)** — usa `bulkIndividual`, funciona numa + conexão QR Code/Baileys do Z-PRO. Manda o texto do template com + `{empresa}`/`{cidade}`/`{segmento}` substituídos. É o padrão, e o risco + aqui é o de sempre com WhatsApp não-oficial: possibilidade de bloqueio do + número por comportamento automatizado (por isso o limite diário e o + intervalo aleatório entre envios). +- **Oficial (Meta, template aprovado)** — usa a API oficial do WhatsApp + (Cloud API/WABA) via Z-PRO. Aqui **não dá pra mandar texto livre**: o + WhatsApp só permite iniciar uma conversa com um **template pré-aprovado** + pelo Meta Business Manager (fora da janela de 24h de uma conversa já + iniciada pelo cliente). Por isso o campo vira "Nome do template aprovado" + (tem que ser **exatamente** o nome cadastrado no Meta) + "Idioma" (ex.: + `pt_BR`). + + **A aprovação do template em si é feita fora deste app** — no Meta + Business Manager (ou no painel do Z-PRO, se ele tiver essa tela) — o + Z-PRO só expõe o *envio* usando um template que já existe, não a criação. + Cadastre e espere aprovar lá antes de configurar o nome aqui. + + Limitação atual: o envio oficial aqui é só nome+idioma, sem variáveis + dentro do corpo do template aprovado (a API do Z-PRO tem endpoints + `templateBody`/`templateMarketingBody` que parecem aceitar variáveis, mas + o formato exato não está documentado na collection — se for necessário, + precisa testar contra a instância real antes de automatizar). + +## Configurar pela primeira vez + +1. **Credenciais do Z-PRO**: no painel do Z-PRO, vá em Admin > API e gere um + token (Base URL, API ID, Token e WhatsApp ID). Na tela, abra + **⚙️ Configurações > Credenciais Z-PRO (WhatsApp)**, preencha os 4 campos + e clique em **Testar conexão** (salva e faz uma chamada real e barata pra + confirmar que as credenciais estão certas, sem mandar nenhuma mensagem). + Sem essas 4 credenciais a automação não manda nada, mesmo ligada. + + Alternativa pra quem prefere não guardar o token no banco local (ex.: em + produção no Docker/Proxmox, gerenciando segredo por variável de + ambiente): deixe os 4 campos da tela em branco e defina + `ZPRO_BASE_URL`/`ZPRO_API_ID`/`ZPRO_BEARER_TOKEN`/`ZPRO_WHATSAPP_ID` no + `.env` — a tela vazia cai automaticamente pro `.env`. Se a tela tiver + algum valor preenchido, ele tem prioridade sobre o `.env`. +2. Na mesma tela, em **Automação de envio (WhatsApp)**: revise o texto de + cada template (um por produto — `{empresa}`, `{cidade}` e `{segmento}` + são substituídos automaticamente), ajuste o **limite por dia** se + quiser, e marque **Ativada**. Salve. + +Antes de ativar, vale mandar um teste manual (próxima seção) pro seu +próprio WhatsApp. + +## Configurações (`settings`, chave → o que faz) + +| Chave | Padrão | O que faz | +|---|---|---| +| `outreach.ativo` | `false` | Interruptor geral. Começa desligado de propósito. | +| `outreach.mensagensPorDia` | `10` | Máximo de envios automáticos por dia (soma de todos os produtos). | +| `outreach.diasEntreProdutos` | `5` | Dias de espera antes de tentar o próximo produto (só usado se a cascata estiver ligada). | +| `outreach.tentarProximoProduto` | `false` | Liga a cascata: quando `false`, só tenta o produto principal uma vez. | +| `outreach.horaInicio` / `outreach.horaFim` | `08:00` / `18:00` | Só manda mensagem dentro desse horário. | + +Tudo isso é editável pela tela de Configurações (os dois primeiros) ou +diretamente via `PUT /api/settings` (todos). + +## Disparo manual (fora do ciclo automático) + +Pra testar ou forçar um lote sem esperar o próximo ciclo de 20 min: + +```bash +npm run outreach +``` + +Respeita as mesmas regras do ciclo automático (`outreach.ativo`, horário +comercial, limite diário) — não manda nada que a tela não mostraria. + +## Enviar pra um número específico + +Em **Configurações > Enviar mensagem manual**: digite o número (com DDI+DDD, +ex.: `5511999999999`), escolha o canal (texto livre ou template oficial já +aprovado) e clique em Enviar. Não depende de lead cadastrado nem de produto. + +No canal **Não-oficial (texto livre)**, o app usa `createTicket` para criar uma +conversa visível no painel do Z-PRO. O ticket usa o canal informado em +`WhatsApp ID` e o usuário de `Responsável` nas configurações. + +No canal **Oficial**, o app usa o endpoint de template aprovado (`template`) e +também espera um `ticketId` na resposta. + +Toda tentativa fica registrada no mesmo histórico dos envios automáticos +(marcado como origem "manual") e **conta pro limite diário**, já que o risco de +bloqueio é do número de WhatsApp usado, não de quem disparou a mensagem. + +## Onde fica o histórico + +Toda tentativa de envio (sucesso ou erro) vira uma linha na tabela +`outreach_sends` (lead, produto, mensagem exata mandada, status, quando). +Um resumo do dia (enviados hoje, por produto, últimos 20 envios) fica +disponível em `GET /api/outreach/status`. diff --git a/logo-gti-256x256.png b/logo-gti-256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..574db002abadd86b930f1d6fc3502cbd9941f749 GIT binary patch literal 12541 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 M$f$m-k}?bVKQWeKL;wH) literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..be20ffa --- /dev/null +++ b/package-lock.json @@ -0,0 +1,15 @@ +{ + "name": "gti-maquina-comercial", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gti-maquina-comercial", + "version": "2.0.0", + "engines": { + "node": ">=24" + } + } + } +} diff --git a/package.json b/package.json index 2a9ac77..ab811a3 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "capture": "node scripts/capturar-leads.mjs", "extract": "node scripts/extrair-base-cnpj.mjs", "recalc-scores": "node scripts/recalcular-scores.mjs", + "outreach": "node scripts/enviar-outreach.mjs", "build:launcher": "powershell -File scripts/build-launcher.ps1" }, "engines": { diff --git a/prospeccao/cnpj/README.md b/prospeccao/cnpj/README.md index 708c859..a59050e 100644 --- a/prospeccao/cnpj/README.md +++ b/prospeccao/cnpj/README.md @@ -11,16 +11,23 @@ O captador aceita CSV com cabecalho e tenta reconhecer colunas comuns como: - `cnae`, `cnae_fiscal` ou `cnae principal` - `logradouro`, `numero`, `bairro` -Comando para usar a base local: +Comando para extrair a base de uma UF a partir dos zips brutos da Receita +(gera o CSV `estabelecimentos-.csv` usado pela captura): ```bash -npm run prospect -- --source=cnpj-base --send +npm run extract -- --uf=BA ``` -Com enriquecimento pela BrasilAPI, use: +Comando para capturar leads usando a base já extraída: ```bash -npm run prospect -- --source=brasilapi --send +npm run capture -- --sources=cnpj-base --uf=BA --cities="Ilheus" +``` + +Com enriquecimento pela BrasilAPI, inclua a fonte `brasilapi`: + +```bash +npm run capture -- --sources=cnpj-base,brasilapi --uf=BA --cities="Ilheus" ``` O modo `brasilapi` usa os CNPJs encontrados nos CSVs locais para consultar dados atualizados por CNPJ. diff --git a/public/css/app.css b/public/css/app.css index ee6378f..481bc15 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -1,323 +1,696 @@ -/* GTI Máquina Comercial — tokens de design (paleta validada, light/dark) */ +/* GTI Maquina Comercial - interface principal */ :root { - --page: #f9f9f7; - --surface: #fcfcfb; - --ink: #0b0b0b; - --ink-2: #52514e; - --muted: #898781; - --grid: #e1e0d9; - --border: rgba(11, 11, 11, 0.10); - --accent: #2a78d6; /* series-1 azul */ + --page: #f5f7fb; + --page-2: #eef2f7; + --surface: #ffffff; + --surface-2: #f9fafc; + --ink: #162033; + --ink-2: #4c5870; + --muted: #7b8495; + --grid: #dce2ec; + --border: rgba(22, 32, 51, 0.11); + --accent: #f06a25; + --accent-2: #1f6feb; --accent-ink: #ffffff; - --bar: #5598e7; /* sequential 350 — barras de ranking */ - --good: #0ca30c; - --good-text: #006300; - --warning: #fab219; - --critical: #d03b3b; - --shadow: 0 1px 3px rgba(11, 11, 11, 0.08); + --bar: #f06a25; + --good: #10a66a; + --good-text: #057046; + --warning: #d99a1f; + --critical: #d63d4a; + --critical-bg: #fff1f2; + --radius-sm: 10px; + --radius: 16px; + --radius-lg: 24px; + --shadow: 0 14px 40px rgba(22, 32, 51, 0.08); + --shadow-soft: 0 8px 24px rgba(22, 32, 51, 0.06); } + @media (prefers-color-scheme: dark) { :root { - --page: #0d0d0d; - --surface: #1a1a19; - --ink: #ffffff; - --ink-2: #c3c2b7; - --muted: #898781; - --grid: #2c2c2a; - --border: rgba(255, 255, 255, 0.10); - --accent: #3987e5; - --bar: #3987e5; - --good-text: #0ca30c; - --shadow: none; + --page: #0d1118; + --page-2: #111827; + --surface: #171c27; + --surface-2: #1d2430; + --ink: #f5f7fb; + --ink-2: #c8d0dd; + --muted: #8e98aa; + --grid: #2c3443; + --border: rgba(255, 255, 255, 0.11); + --accent: #ff7a33; + --accent-2: #60a5fa; + --bar: #ff7a33; + --good-text: #55d99c; + --critical-bg: rgba(214, 61, 74, 0.12); + --shadow: 0 18px 50px rgba(0, 0, 0, 0.28); + --shadow-soft: 0 10px 28px rgba(0, 0, 0, 0.22); } } * { box-sizing: border-box; } -html, body { margin: 0; padding: 0; } +html, body { margin: 0; padding: 0; min-height: 100%; } body { - background: var(--page); color: var(--ink); - font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; + font: 14px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + background: + radial-gradient(circle at 12% -10%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 34rem), + radial-gradient(circle at 86% -6%, color-mix(in srgb, var(--accent-2) 12%, transparent), transparent 30rem), + linear-gradient(180deg, var(--page), var(--page-2)); } +button, input, select, textarea { font: inherit; } +button { color: inherit; } -/* ---------- topo ---------- */ +/* topo */ .topbar { - display: flex; align-items: center; gap: 20px; - padding: 10px 20px; - background: var(--surface); - border-bottom: 1px solid var(--grid); - position: sticky; top: 0; z-index: 20; + position: sticky; + top: 0; + z-index: 20; + display: grid; + grid-template-columns: auto minmax(220px, 1fr) auto auto auto auto; + align-items: center; + gap: 12px; + padding: 12px 22px; + background: color-mix(in srgb, var(--surface) 88%, transparent); + border-bottom: 1px solid var(--border); + box-shadow: 0 8px 30px rgba(22, 32, 51, 0.04); + backdrop-filter: blur(14px); } -.brand { display: flex; align-items: baseline; gap: 8px; } -.brand-mark { - background: var(--accent); color: var(--accent-ink); - font-weight: 800; padding: 3px 9px; border-radius: 6px; letter-spacing: 0.5px; +.brand { display: flex; align-items: center; gap: 10px; min-width: max-content; } +.brand-mark { height: 32px; width: auto; display: block; border-radius: 8px; } +.brand-mark-lg { height: 62px; } +.brand-name { font-weight: 800; letter-spacing: -0.02em; color: var(--ink); } +.tabs { + display: inline-flex; + gap: 6px; + padding: 4px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--surface-2); + width: fit-content; } -.brand-name { font-weight: 600; color: var(--ink-2); } -.tabs { display: flex; gap: 4px; flex: 1; } .tab { - border: none; background: none; color: var(--ink-2); - font: inherit; font-weight: 600; padding: 8px 16px; - border-radius: 8px; cursor: pointer; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--ink-2); + cursor: pointer; + font-weight: 750; + padding: 8px 14px; + transition: 0.18s ease; } -.tab:hover { background: var(--grid); } -.tab.active { background: var(--accent); color: var(--accent-ink); } +.tab:hover { color: var(--ink); background: color-mix(in srgb, var(--accent) 9%, transparent); } +.tab.active { background: var(--ink); color: var(--page); box-shadow: var(--shadow-soft); } -main { max-width: 1280px; margin: 0 auto; padding: 16px 20px 60px; } +main { max-width: 1320px; margin: 0 auto; padding: 24px 22px 70px; } .tab-panel { display: none; } .tab-panel.active { display: block; } -/* ---------- cartões e grades ---------- */ +/* layout */ +.captura-wrap { max-width: 820px; margin: 0 auto; } .card { - background: var(--surface); + position: relative; + overflow: hidden; + background: color-mix(in srgb, var(--surface) 96%, transparent); border: 1px solid var(--border); - border-radius: 12px; - padding: 16px 18px; - box-shadow: var(--shadow); - margin-bottom: 16px; -} -.card h2 { margin: 0 0 12px; font-size: 16px; } -.captura-wrap { max-width: 720px; margin: 0 auto; } -.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } -@media (max-width: 1100px) { - .grid-3 { grid-template-columns: 1fr 1fr; } -} -@media (max-width: 980px) { - .grid-3 { grid-template-columns: 1fr; } + border-radius: var(--radius-lg); + box-shadow: var(--shadow-soft); + padding: 20px; + margin-bottom: 18px; } +.card h2 { margin: 0 0 14px; font-size: 18px; letter-spacing: -0.02em; } +.grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; } -/* ---------- formulários ---------- */ +/* formularios */ .field { margin-bottom: 18px; } -.field > label:first-child, .field > summary { font-weight: 600; display: block; margin-bottom: 6px; } -.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } +.field > label:first-child, +.field > summary, +label.settings-title { + display: block; + margin-bottom: 7px; + color: var(--ink); + font-weight: 800; + letter-spacing: -0.01em; +} +.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } .row.space-between { justify-content: space-between; } .row.wrap { flex-wrap: wrap; } -.radio { display: flex; align-items: center; gap: 6px; font-weight: 400; cursor: pointer; margin: 4px 0; } -select, input, textarea { - font: inherit; color: var(--ink); - background: var(--page); - border: 1px solid var(--grid); - border-radius: 8px; padding: 7px 10px; +label { color: var(--ink-2); } +label > input, +label > select, +label > textarea { display: block; margin-top: 5px; } +.radio { + display: inline-flex; + align-items: center; + gap: 8px; + margin: 4px 0; + color: var(--ink-2); + cursor: pointer; + user-select: none; } -select:focus, input:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: 1px; } -input[type="number"] { width: 90px; } -label > input[type="number"] { display: block; margin-top: 4px; } +input[type="radio"], input[type="checkbox"] { accent-color: var(--accent); } +select, input, textarea { + width: auto; + color: var(--ink); + background: var(--surface); + border: 1px solid var(--grid); + border-radius: 12px; + padding: 10px 12px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35); + transition: border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease; +} +textarea { width: 100%; resize: vertical; min-height: 84px; } +select:hover, input:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--grid)); } +select:focus, input:focus, textarea:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 15%, transparent); +} +input[type="number"] { width: 96px; } details summary { cursor: pointer; } +/* botoes */ .btn, .btn-ghost, .btn-danger { - font: inherit; font-weight: 600; cursor: pointer; - border-radius: 8px; padding: 8px 14px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 40px; + border-radius: 12px; + padding: 9px 15px; border: 1px solid var(--grid); - background: var(--surface); color: var(--ink); + background: var(--surface); + color: var(--ink); + cursor: pointer; + font-weight: 800; + text-decoration: none; + transition: 0.18s ease; } -.btn:hover, .btn-ghost:hover { border-color: var(--accent); } -.btn-primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); } -.btn-primary:hover { filter: brightness(1.1); } -.btn-lg { width: 100%; padding: 12px; font-size: 15px; } -.btn-ghost { border-color: transparent; color: var(--ink-2); } -.btn-danger { color: var(--critical); border-color: var(--critical); background: none; } -.btn-danger:hover { background: var(--critical); color: #fff; } -button:disabled { opacity: 0.5; cursor: not-allowed; } +.btn:hover, .btn-ghost:hover { transform: translateY(-1px); border-color: var(--accent); box-shadow: var(--shadow-soft); } +.btn-primary { background: linear-gradient(135deg, var(--accent), #ff8b47); border-color: transparent; color: var(--accent-ink); } +.btn-primary:hover { filter: saturate(1.08); } +.btn-lg { width: 100%; min-height: 48px; font-size: 15px; } +.btn-sm { min-height: 32px; padding: 6px 10px; font-size: 12.5px; } +.btn-ghost { border-color: transparent; background: transparent; color: var(--ink-2); } +.btn-danger { color: var(--critical); border-color: color-mix(in srgb, var(--critical) 45%, var(--grid)); background: color-mix(in srgb, var(--critical) 6%, transparent); } +.btn-danger:hover { background: var(--critical); color: #fff; border-color: var(--critical); } +button:disabled { opacity: 0.55; cursor: not-allowed; transform: none !important; box-shadow: none !important; } -.zona-risco { border: 1px solid var(--critical); border-radius: 10px; padding: 10px 12px; margin-top: 14px; } -.zona-risco label { color: var(--critical); font-weight: 700; } +/* configuracoes */ +#modal-settings { max-width: 900px; } +#modal-settings .modal-body { max-height: min(86vh, 900px); overflow-y: auto; } +.settings-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 16px; + align-items: start; + padding-bottom: 14px; + border-bottom: 1px solid var(--border); + margin-bottom: 16px; +} +.settings-header h2 { margin: 0 0 4px; font-size: 24px; letter-spacing: -0.03em; } +.settings-header p { margin: 0; } +#outreach-status { cursor: default; white-space: nowrap; } +.settings-card, +.settings-section { + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface-2); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.34) inset; + margin-bottom: 12px; +} +.settings-card { padding: 16px; } +.settings-principal { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 18px; + align-items: center; + border-color: color-mix(in srgb, var(--accent) 38%, var(--border)); + background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 10%, var(--surface)), var(--surface-2)); +} +.settings-controls { display: flex; gap: 12px; align-items: end; flex-wrap: wrap; justify-content: flex-end; } +.toggle { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 42px; + border: 1px solid var(--grid); + border-radius: 999px; + padding: 8px 13px; + background: var(--surface); + color: var(--ink); + font-weight: 800; +} +.settings-section { padding: 0 16px 16px; } +.settings-section > summary { + position: relative; + list-style: none; + padding: 15px 30px 15px 0; + color: var(--ink); + font-weight: 850; + letter-spacing: -0.01em; +} +.settings-section > summary::-webkit-details-marker { display: none; } +.settings-section > summary::after { + content: "+"; + position: absolute; + right: 0; + top: 13px; + display: grid; + place-items: center; + width: 24px; + height: 24px; + border-radius: 50%; + color: var(--ink-2); + background: var(--surface); + border: 1px solid var(--grid); +} +.settings-section[open] > summary::after { content: "-"; } +.settings-section > .muted { margin-top: -4px; } +.settings-section .lista { margin-top: 10px; } +.settings-section input[type="text"], +.settings-section input[type="password"], +.settings-section textarea, +.settings-section select { min-width: 220px; } -/* ---------- login ---------- */ -.login-body { display: flex; min-height: 100vh; } -.login-wrap { margin: auto; width: 100%; max-width: 360px; padding: 20px; } -.login-card { display: flex; flex-direction: column; gap: 14px; } -.login-brand { justify-content: center; margin-bottom: 4px; } +.manual-send-card { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + padding: 14px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); +} +.manual-send-card .full { grid-column: 1 / -1; } +.manual-channel { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.manual-channel .radio { + border: 1px solid var(--grid); + border-radius: 999px; + padding: 8px 12px; + background: var(--surface-2); + font-weight: 750; +} +#manual-oficial-campos { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} +#manual-oficial-campos.hidden { display: none !important; } +#manual-template-nome { min-width: 0; width: 100%; } +.template-vars { + padding: 12px; + border: 1px dashed var(--grid); + border-radius: var(--radius); + background: var(--surface-2); +} +.template-vars > label { color: var(--ink); font-weight: 800; } +.template-vars p { margin: 2px 0 10px; } +.variable-helper { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; + margin-bottom: 10px; +} +.variable-helper select, .variable-picker { min-width: min(100%, 320px); } +.template-vars-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} +.template-vars-grid label:last-child { grid-column: 1 / -1; } +.template-preview { + margin-top: 12px; + padding: 12px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--surface); + color: var(--ink-2); + white-space: pre-wrap; +} +.manual-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-top: 4px; +} +#manual-status:not(:empty) { + display: block; + width: 100%; + padding: 10px 12px; + border-radius: 12px; + background: var(--surface-2); + border: 1px solid var(--border); +} +#manual-status:not(:empty)::first-letter { font-weight: 900; } +#manual-status:not(:empty) { color: var(--ink-2); } +#manual-status.status-ok { color: var(--good-text); background: color-mix(in srgb, var(--good) 10%, var(--surface)); border-color: color-mix(in srgb, var(--good) 28%, var(--border)); } +#manual-status.status-error { color: var(--critical); background: var(--critical-bg); border-color: color-mix(in srgb, var(--critical) 30%, var(--border)); } +#btn-toggle-outreach-log { justify-self: start; margin-top: 2px; } + +.outreach-template { flex-direction: column; align-items: stretch; gap: 8px; } +.outreach-template .row.space-between { align-items: center; } +.outreach-template textarea { width: 100%; margin-top: 2px; } +.outreach-template-oficial { display: flex; flex-direction: column; gap: 8px; } +.outreach-template-vars { margin-top: 2px; } +.outreach-template-vars .template-vars-grid { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); } +.outreach-template-vars .template-vars-grid label:last-child { grid-column: auto; } +.outreach-template-link { color: var(--accent-2); font-size: 12.5px; font-weight: 750; text-decoration: none; } +.outreach-template-link:hover { text-decoration: underline; } +.outreach-template-aviso { color: var(--critical); font-size: 12.5px; margin: 0; } +#outreach-limite { width: 82px; } + +.zona-risco { border-color: color-mix(in srgb, var(--critical) 45%, var(--border)); background: color-mix(in srgb, var(--critical) 5%, var(--surface)); } +.zona-risco summary { color: var(--critical); } +.template-sync-box { + margin-top: 14px; + padding-top: 14px; + border-top: 1px dashed var(--grid); + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(220px, 0.45fr); + gap: 12px; +} +.template-sync-box .row { grid-column: 1 / -1; } +.template-sync-inline { + display: flex; + gap: 10px; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + margin: 0 0 12px; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); +} + +/* login */ +.login-body { + display: flex; + min-height: 100vh; + background: + radial-gradient(circle at 18% 12%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 28rem), + radial-gradient(circle at 82% 4%, color-mix(in srgb, var(--accent-2) 18%, transparent), transparent 26rem), + linear-gradient(135deg, var(--page), var(--page-2)); +} +.login-wrap { margin: auto; width: 100%; max-width: 410px; padding: 22px; } +.login-card { display: flex; flex-direction: column; gap: 14px; padding: 28px; } +.login-brand { justify-content: center; margin-bottom: 6px; } .login-erro { color: var(--critical); min-height: 1em; margin: 0; } +.erro { color: var(--critical); margin: 4px 0 0; font-weight: 750; } +.login-card .field input { width: 100%; } -/* ---------- usuarios ---------- */ -#lista-usuarios { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; } -#lista-usuarios > div { padding: 6px 0; border-bottom: 1px solid var(--grid); } -#form-novo-usuario select { border: 1px solid var(--grid); border-radius: 8px; padding: 7px 10px; background: var(--surface); color: var(--ink); } - +/* chips, avisos e listas */ .chip { - font-size: 12px; font-weight: 600; - padding: 3px 10px; border-radius: 999px; - background: var(--grid); color: var(--ink-2); + display: inline-flex; + align-items: center; + gap: 6px; + border: 1px solid var(--border); + background: var(--surface); + color: var(--ink-2); + border-radius: 999px; + padding: 5px 11px; + font-size: 12px; + font-weight: 800; } -.chip.ok { background: color-mix(in srgb, var(--good) 15%, transparent); color: var(--good-text); } -.chip.warn { background: color-mix(in srgb, var(--warning) 20%, transparent); color: var(--ink); } - +.chip.ok { background: color-mix(in srgb, var(--good) 12%, var(--surface)); color: var(--good-text); border-color: color-mix(in srgb, var(--good) 26%, var(--border)); } +.chip.warn { background: color-mix(in srgb, var(--warning) 14%, var(--surface)); color: var(--ink); border-color: color-mix(in srgb, var(--warning) 32%, var(--border)); } .notice { - margin-top: 10px; padding: 12px; - border: 1px solid var(--warning); - border-radius: 8px; - background: color-mix(in srgb, var(--warning) 8%, transparent); + margin-top: 12px; + padding: 14px; + border: 1px solid color-mix(in srgb, var(--warning) 42%, var(--border)); + border-radius: var(--radius); + background: color-mix(in srgb, var(--warning) 10%, var(--surface)); } .notice p { margin: 0 0 10px; } - -/* pickers de cidade/nicho */ -.picker { margin-top: 8px; } -.picker input[type="search"] { width: 100%; margin-bottom: 6px; } -.sugestoes { display: flex; flex-direction: column; gap: 2px; max-height: 180px; overflow-y: auto; } -.sugestoes button { - text-align: left; font: inherit; border: none; background: none; - color: var(--ink); padding: 6px 8px; border-radius: 6px; cursor: pointer; -} -.sugestoes button:hover { background: var(--grid); } -.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } -.chips .chip { cursor: pointer; } -.chips .chip::after { content: " ✕"; color: var(--muted); } -.nichos-grid { - display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); - gap: 2px 12px; max-height: 300px; overflow-y: auto; margin-top: 8px; -} -.nichos-grid label { display: flex; gap: 6px; align-items: center; padding: 3px 4px; border-radius: 6px; cursor: pointer; } -.nichos-grid label:hover { background: var(--grid); } - -.cnae-avulso { margin-top: 10px; border-top: 1px dashed var(--grid); padding-top: 10px; } -.cnae-avulso summary { cursor: pointer; font-size: 13px; color: var(--ink-2); font-weight: 600; } -.cnae-avulso-hint { margin: 6px 0 8px; font-size: 12.5px; } -.cnae-avulso input[type="search"] { width: 100%; margin-bottom: 6px; } -.cnae-avulso .sugestoes button { display: flex; justify-content: space-between; gap: 8px; } -.cnae-avulso .sugestoes .cod { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 12px; } - -.fontes { display: flex; flex-direction: column; gap: 6px; } -.fonte { - display: flex; gap: 10px; align-items: flex-start; - border: 1px solid var(--grid); border-radius: 8px; padding: 8px 10px; cursor: pointer; -} -.fonte input { margin-top: 3px; } -.fonte .desc { font-size: 12.5px; color: var(--ink-2); } -.fonte.indisponivel { opacity: 0.55; } - -/* ---------- progresso ---------- */ -.progress-bar { height: 8px; border-radius: 999px; background: var(--grid); overflow: hidden; margin: 10px 0 8px; } -.progress-fill { height: 100%; width: 3%; background: var(--accent); border-radius: 999px; transition: width 0.4s; } -.progress-msg { color: var(--ink-2); margin: 4px 0 10px; min-height: 1.4em; } -.stat-row { display: flex; gap: 18px; margin-bottom: 10px; } -.stat-row .mini { display: flex; flex-direction: column; } -.stat-row .mini b { font-size: 20px; } -.stat-row .mini span { font-size: 12px; color: var(--muted); } -.log { - font: 12px/1.5 ui-monospace, Consolas, monospace; - background: var(--page); border: 1px solid var(--grid); border-radius: 8px; - padding: 8px 10px; max-height: 160px; overflow-y: auto; white-space: pre-wrap; -} - .lista { display: flex; flex-direction: column; gap: 8px; } .lista .item { - display: flex; justify-content: space-between; align-items: center; gap: 10px; - border-bottom: 1px solid var(--grid); padding: 8px 2px; flex-wrap: wrap; + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + flex-wrap: wrap; + padding: 11px 12px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--surface); } -.lista .item:last-child { border-bottom: none; } -.lista .item .sub { font-size: 12px; color: var(--muted); } +.lista .item .sub { color: var(--muted); font-size: 12px; } -/* ---------- tratamento: tiles e rankings ---------- */ -.filtros-card { padding: 12px 16px; } -.filtros { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } -.filtros select, .filtros input { min-width: 130px; } -#f-busca { flex: 1; min-width: 200px; } +/* pickers */ +.picker { margin-top: 10px; } +.picker input[type="search"], .cnae-avulso input[type="search"] { width: 100%; margin-bottom: 8px; } +.sugestoes { display: flex; flex-direction: column; gap: 4px; max-height: 200px; overflow-y: auto; } +.sugestoes button { + text-align: left; + border: 0; + border-radius: 10px; + background: transparent; + color: var(--ink); + cursor: pointer; + padding: 8px 10px; +} +.sugestoes button:hover { background: var(--surface-2); } +.chips { display: flex; flex-wrap: wrap; gap: 7px; margin: 8px 0 12px; } +.chips .chip { cursor: pointer; } +.chips .chip::after { content: " x"; color: var(--muted); } +.chips .chip:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--surface)); } +.nichos-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); + gap: 4px 12px; + max-height: 320px; + overflow-y: auto; + margin-top: 10px; +} +.nichos-grid label { display: flex; gap: 7px; align-items: center; padding: 6px 7px; border-radius: 10px; cursor: pointer; } +.nichos-grid label:hover { background: var(--surface-2); } +.cnae-avulso { margin-top: 14px; border-top: 1px dashed var(--grid); padding-top: 12px; } +.cnae-avulso summary { color: var(--ink-2); font-size: 13px; font-weight: 800; } +.cnae-avulso-hint { margin: 7px 0 9px; font-size: 12.5px; } +.cnae-avulso .sugestoes button { display: flex; justify-content: space-between; gap: 8px; } +.cnae-avulso .sugestoes .cod { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 12px; } +.fontes { display: flex; flex-direction: column; gap: 8px; } +.fonte { + display: flex; + gap: 11px; + align-items: flex-start; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 11px 12px; + background: var(--surface); + cursor: pointer; +} +.fonte:hover { border-color: var(--accent); box-shadow: var(--shadow-soft); } +.fonte input { margin-top: 3px; } +.fonte .desc { color: var(--ink-2); font-size: 12.5px; } +.fonte.indisponivel { opacity: 0.58; } -.stat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 16px; } +/* progresso */ +.progress-bar { height: 10px; border-radius: 999px; background: var(--grid); overflow: hidden; margin: 12px 0 9px; } +.progress-fill { height: 100%; width: 3%; background: linear-gradient(90deg, var(--accent), #ffb36d); border-radius: 999px; transition: width 0.4s; } +.progress-msg { color: var(--ink-2); margin: 4px 0 11px; min-height: 1.4em; } +.stat-row { display: flex; gap: 18px; margin-bottom: 12px; flex-wrap: wrap; } +.stat-row .mini { display: flex; flex-direction: column; min-width: 96px; } +.stat-row .mini b { font-size: 23px; letter-spacing: -0.03em; } +.stat-row .mini span { color: var(--muted); font-size: 12px; } +.log { + max-height: 170px; + overflow-y: auto; + white-space: pre-wrap; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #0f172a; + color: #dbeafe; + font: 12px/1.55 ui-monospace, SFMono-Regular, Consolas, monospace; +} + +/* tratamento */ +.filtros-card { padding: 14px; } +.filtros { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 9px; align-items: center; } +.filtros select, .filtros input { width: 100%; min-width: 0; } +#f-busca { grid-column: span 2; } +.stat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 14px; margin-bottom: 18px; } .tile { - background: var(--surface); border: 1px solid var(--border); border-radius: 12px; - padding: 14px 16px; box-shadow: var(--shadow); + position: relative; + overflow: hidden; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 16px; + box-shadow: var(--shadow-soft); } -.tile .valor { font-size: 26px; font-weight: 700; } -.tile .rotulo { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; } -.tile .detalhe { font-size: 12px; color: var(--muted); } +.tile::after { + content: ""; + position: absolute; + inset: auto -20px -28px auto; + width: 96px; + height: 96px; + border-radius: 999px; + background: color-mix(in srgb, var(--accent) 10%, transparent); +} +.tile .valor { font-size: 29px; font-weight: 900; letter-spacing: -0.04em; } +.tile .rotulo { color: var(--ink-2); font-size: 12.5px; font-weight: 750; margin-top: 2px; } +.tile .detalhe { color: var(--muted); font-size: 12px; } .tile.t-good .valor { color: var(--good-text); } -.tile.t-warn .valor { color: var(--ink); } .tile.tile-wide { grid-column: span 2; } -@media (max-width: 640px) { .tile.tile-wide { grid-column: span 1; } } -.meta-input { width: 70px; text-align: right; padding: 3px 6px; border-radius: 6px; border: 1px solid var(--border); background: var(--surface); color: var(--ink); } +.meta-input { width: 78px; text-align: right; padding: 5px 7px; } -/* ---------- funil comercial ---------- */ -.funil { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; } -.funil-etapa { display: grid; grid-template-columns: 190px 1fr 70px; align-items: center; gap: 8px; padding: 1px 4px; } -.funil-etapa.pendente { opacity: 0.4; } -.funil-label { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.funil-barra { height: 14px; border-radius: 4px; overflow: hidden; background: none; } -.funil-fill { height: 100%; background: var(--bar); border-radius: 0 4px 4px 0; min-width: 2px; } -.funil-valor { font-size: 12.5px; color: var(--ink-2); text-align: right; font-variant-numeric: tabular-nums; } - -/* barras de ranking: magnitude = um matiz (azul), rótulo direto, sem legenda */ -.ranking { display: flex; flex-direction: column; gap: 7px; } -.rank-row { display: grid; grid-template-columns: 150px 1fr 70px; gap: 8px; align-items: center; cursor: pointer; border-radius: 6px; padding: 1px 4px; } -.rank-row:hover { background: var(--grid); } -.rank-row.ativo { background: color-mix(in srgb, var(--accent) 16%, transparent); font-weight: 600; } +.ranking, .funil { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; } +.rank-row, .funil-etapa { + display: grid; + grid-template-columns: 150px 1fr 70px; + align-items: center; + gap: 9px; + padding: 4px 6px; + border-radius: 10px; +} +.funil-etapa { grid-template-columns: 190px 1fr 70px; } +.rank-row { cursor: pointer; } +.rank-row:hover, .oportunidade-clicavel:hover { background: var(--surface-2); } +.rank-row.ativo, .oportunidade-clicavel.ativo { background: color-mix(in srgb, var(--accent) 13%, var(--surface)); font-weight: 800; } .rank-row.nao-clicavel { cursor: default; } -.rank-row.nao-clicavel:hover { background: none; } -.oportunidade-clicavel { cursor: pointer; border-radius: 8px; padding: 6px 8px; } -.oportunidade-clicavel:hover { background: var(--grid); } -.oportunidade-clicavel.ativo { background: color-mix(in srgb, var(--accent) 16%, transparent); } -.rank-label { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.rank-track { height: 14px; border-radius: 4px; overflow: hidden; background: none; } -.rank-fill { height: 100%; background: var(--bar); border-radius: 0 4px 4px 0; min-width: 2px; } -.rank-value { font-size: 12.5px; color: var(--ink-2); text-align: right; font-variant-numeric: tabular-nums; } +.rank-row.nao-clicavel:hover { background: transparent; } +.funil-etapa.pendente { opacity: 0.43; } +.rank-label, .funil-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; } +.rank-track, .funil-barra { height: 12px; overflow: hidden; border-radius: 999px; background: var(--grid); } +.rank-fill, .funil-fill { height: 100%; min-width: 3px; border-radius: 999px; background: linear-gradient(90deg, var(--bar), #ffb36d); } +.rank-value, .funil-valor { color: var(--ink-2); text-align: right; font-size: 12.5px; font-variant-numeric: tabular-nums; } +.oportunidade-clicavel { cursor: pointer; border-radius: 12px; padding: 8px 10px; } -/* ---------- tabela ---------- */ -.table-wrap { overflow-x: auto; margin: 0 -4px; } -table { border-collapse: collapse; width: 100%; } -th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--grid); white-space: nowrap; } -th { font-size: 12px; text-transform: uppercase; letter-spacing: 0.4px; color: var(--muted); } +/* tabela */ +.table-wrap { overflow-x: auto; margin: 0 -6px; } +table { width: 100%; border-collapse: separate; border-spacing: 0; } +th, td { text-align: left; white-space: nowrap; padding: 10px 12px; border-bottom: 1px solid var(--grid); } +th { + color: var(--muted); + background: var(--surface); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.06em; +} td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; } +.acao-col { + position: sticky; + left: 0; + z-index: 3; + width: 58px; + min-width: 58px; + text-align: center; + background: var(--surface); + box-shadow: 10px 0 18px -18px rgba(22, 32, 51, 0.8); +} +td.acao-col { padding-left: 8px; padding-right: 8px; } tbody tr { cursor: pointer; } tbody tr:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); } -.badge { font-size: 12px; font-weight: 600; padding: 2px 8px; border-radius: 999px; white-space: nowrap; } -/* Escala sequencial ordinal (excelente -> muito baixo), verde -> ambar -> vermelho, - construida so com os tokens ja validados (--good/--warning/--critical). */ -.badge.excelente { background: color-mix(in srgb, var(--good) 20%, transparent); color: var(--good-text); } +tbody tr:hover .acao-col { background: color-mix(in srgb, var(--accent) 6%, var(--surface)); } +.badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 9px; font-size: 12px; font-weight: 850; white-space: nowrap; } +.badge.excelente { background: color-mix(in srgb, var(--good) 18%, transparent); color: var(--good-text); } .badge.bom { background: color-mix(in srgb, var(--good) 12%, var(--warning) 8%); color: var(--good-text); } -.badge.medio { background: color-mix(in srgb, var(--warning) 22%, transparent); color: var(--ink); } -.badge.baixo { background: color-mix(in srgb, var(--warning) 18%, var(--critical) 12%); color: var(--critical); } -.badge.muito_baixo { background: color-mix(in srgb, var(--critical) 16%, transparent); color: var(--critical); } - -.badge.prioridade-1 { background: color-mix(in srgb, var(--good) 20%, transparent); color: var(--good-text); } -.badge.prioridade-2 { background: color-mix(in srgb, var(--warning) 22%, transparent); color: var(--ink); } +.badge.medio { background: color-mix(in srgb, var(--warning) 20%, transparent); color: var(--ink); } +.badge.baixo { background: color-mix(in srgb, var(--warning) 16%, var(--critical) 10%); color: var(--critical); } +.badge.muito_baixo { background: color-mix(in srgb, var(--critical) 14%, transparent); color: var(--critical); } +.badge.prioridade-1 { background: color-mix(in srgb, var(--good) 18%, transparent); color: var(--good-text); } +.badge.prioridade-2 { background: color-mix(in srgb, var(--warning) 20%, transparent); color: var(--ink); } .badge.prioridade-3 { background: var(--grid); color: var(--ink-2); } .badge.prioridade-4 { background: var(--grid); color: var(--muted); } - .produto-cell { font-size: 12.5px; } .estrelas { color: var(--warning); letter-spacing: 1px; white-space: nowrap; } -.pontos-fortes { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 3px; font-size: 13px; } -.resumo-comercial { border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; margin-top: 12px; } -.resumo-comercial h3 { margin: 0 0 8px; font-size: 14px; } .empresa-cell { max-width: 280px; overflow: hidden; text-overflow: ellipsis; } - -/* ---------- explicacao de score (v2.1) ---------- */ -.score-breakdown-wrap { border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; margin-top: 12px; } -.score-breakdown { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 3px; font-size: 13px; } -.score-breakdown li.ok { color: var(--ink); } +.whatsapp-action { + width: 32px; + height: 32px; + display: inline-grid; + place-items: center; + border: 1px solid color-mix(in srgb, var(--good) 35%, var(--grid)); + border-radius: 999px; + background: color-mix(in srgb, var(--good) 12%, var(--surface)); + color: var(--good-text); + cursor: pointer; + font-size: 17px; + line-height: 1; +} +.whatsapp-action:hover { background: var(--good); color: #fff; } +.pontos-fortes, .score-breakdown { list-style: none; margin: 7px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; font-size: 13px; } +.resumo-comercial, .score-breakdown-wrap { border: 1px solid var(--border); border-radius: var(--radius); padding: 13px 14px; margin-top: 12px; background: var(--surface-2); } +.resumo-comercial h3 { margin: 0 0 8px; font-size: 14px; } .score-breakdown li.nok { color: var(--muted); } -/* ---------- atalhos de exportacao (v2.1) ---------- */ -.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 12px; } -.chip { - border: 1px solid var(--border); background: var(--surface); color: var(--ink); - border-radius: 999px; padding: 4px 12px; font-size: 12.5px; cursor: pointer; -} -.chip:hover { background: var(--grid); border-color: var(--accent); } +/* usuarios */ +#lista-usuarios { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; } +#lista-usuarios > div { padding: 10px 12px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface-2); } -/* ---------- modais e toast ---------- */ +/* modais e toast */ dialog { - border: 1px solid var(--border); border-radius: 14px; - background: var(--surface); color: var(--ink); - max-width: 560px; width: calc(100vw - 40px); + width: calc(100vw - 40px); + max-width: 600px; padding: 0; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--surface); + color: var(--ink); + box-shadow: var(--shadow); } -dialog::backdrop { background: rgba(0, 0, 0, 0.45); } -.modal-body { padding: 20px 22px; } -.modal-body h2 { margin-top: 0; } -#modal-historico { max-width: 620px; } +dialog::backdrop { background: rgba(8, 13, 22, 0.58); backdrop-filter: blur(4px); } +.modal-body { padding: 24px; } +.modal-body h2 { margin-top: 0; letter-spacing: -0.03em; } +#modal-historico { max-width: 680px; } #modal-historico .lista { max-height: 60vh; overflow-y: auto; margin-top: 10px; } -textarea { width: 100%; resize: vertical; } - .toast { - position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); - background: var(--ink); color: var(--page); - padding: 10px 18px; border-radius: 10px; font-weight: 600; - z-index: 100; max-width: 80vw; + position: fixed; + left: 50%; + bottom: 24px; + z-index: 100; + max-width: min(86vw, 680px); + transform: translateX(-50%); + padding: 11px 16px; + border-radius: 999px; + background: var(--ink); + color: var(--page); + box-shadow: var(--shadow); + font-weight: 800; } .hidden { display: none !important; } .muted { color: var(--muted); font-weight: 400; } - -/* detalhe do lead */ -.detalhe-grid { display: grid; grid-template-columns: 140px 1fr; gap: 4px 12px; font-size: 13.5px; } +.detalhe-grid { display: grid; grid-template-columns: 150px 1fr; gap: 6px 14px; font-size: 13.5px; } .detalhe-grid dt { color: var(--muted); } .detalhe-grid dd { margin: 0; word-break: break-word; } + +@media (max-width: 1100px) { + .grid-3 { grid-template-columns: 1fr 1fr; } + .topbar { grid-template-columns: 1fr auto auto auto; } + .tabs { grid-column: 1 / -1; order: 2; width: 100%; justify-content: center; } + #usuario-logado { justify-self: end; } +} + +@media (max-width: 780px) { + main { padding: 16px 12px 50px; } + .topbar { padding: 10px 12px; grid-template-columns: 1fr auto auto; gap: 8px; } + .brand-name { display: none; } + #usuario-logado { display: none; } + .tabs { overflow-x: auto; justify-content: flex-start; } + .tab { white-space: nowrap; } + .grid-3 { grid-template-columns: 1fr; } + .card { border-radius: var(--radius); padding: 16px; } + .settings-header, .settings-principal { grid-template-columns: 1fr; } + .settings-controls { justify-content: flex-start; } + .manual-send-card, #manual-oficial-campos { grid-template-columns: 1fr; } + .settings-section input[type="text"], + .settings-section input[type="password"], + .settings-section textarea, + .settings-section select, + .manual-send-card input, + .manual-send-card select { width: 100%; min-width: 0; } + .manual-actions { align-items: stretch; flex-direction: column; } + .manual-actions .btn { width: 100%; } + #f-busca, .tile.tile-wide { grid-column: span 1; } + .rank-row, .funil-etapa { grid-template-columns: 1fr 90px; } + .rank-track, .funil-barra { grid-column: 1 / -1; order: 3; } + th { top: 107px; } + .modal-body { padding: 18px; } +} diff --git a/public/img/logo-gti.png b/public/img/logo-gti.png new file mode 100644 index 0000000000000000000000000000000000000000..574db002abadd86b930f1d6fc3502cbd9941f749 GIT binary patch literal 12541 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 M$f$m-k}?bVKQWeKL;wH) literal 0 HcmV?d00001 diff --git a/public/index.html b/public/index.html index 20f2c08..79568da 100644 --- a/public/index.html +++ b/public/index.html @@ -5,12 +5,12 @@ GTI Máquina Comercial - +
- GTI + GTI Máquina Comercial