App local (Node + SQLite) para captura, tratamento e exportacao de leads comerciais, com dashboard de gestao, login/usuarios e integracao Z-PRO. Co-Authored-By: Claude <noreply@anthropic.com>
412 lines
12 KiB
JavaScript
412 lines
12 KiB
JavaScript
const TARGET_CITY_ALIASES = new Set([
|
|
'santo antonio de jesus',
|
|
'santo antonio de jesus ba',
|
|
'saj'
|
|
]);
|
|
|
|
const TARGET_SEGMENTS = new Set([
|
|
'mercado',
|
|
'supermercado',
|
|
'farmacia',
|
|
'drogaria',
|
|
'restaurante',
|
|
'delivery',
|
|
'distribuidora',
|
|
'autopecas',
|
|
'material de construcao',
|
|
'clinica',
|
|
'loja',
|
|
'varejo',
|
|
'atacado',
|
|
'oficina',
|
|
'escola',
|
|
'salao de beleza',
|
|
'barbearia',
|
|
'estetica',
|
|
'celular',
|
|
'transportadora',
|
|
'logistica'
|
|
]);
|
|
|
|
const SEGMENT_STOPWORDS = new Set(['de', 'da', 'do', 'e']);
|
|
|
|
function segmentTokens(value = '') {
|
|
return cleanText(value)
|
|
.split(/[^a-z0-9]+/)
|
|
.filter((token) => token.length > 2 && !SEGMENT_STOPWORDS.has(token));
|
|
}
|
|
|
|
const TARGET_SEGMENT_TOKENS = new Set(
|
|
[...TARGET_SEGMENTS].flatMap((term) => segmentTokens(term))
|
|
);
|
|
|
|
function tokenMatches(token, targetToken) {
|
|
return token === targetToken
|
|
|| (token.length > 4 && targetToken.length > 4 && (token.startsWith(targetToken) || targetToken.startsWith(token)));
|
|
}
|
|
|
|
function isTargetSegmentMatch(segmentKey) {
|
|
if (!segmentKey) return false;
|
|
if (TARGET_SEGMENTS.has(segmentKey)) return true;
|
|
return segmentTokens(segmentKey).some((token) =>
|
|
[...TARGET_SEGMENT_TOKENS].some((targetToken) => tokenMatches(token, targetToken))
|
|
);
|
|
}
|
|
|
|
const SERVICE_LABELS = {
|
|
erp: 'ERP / sistema de gestao',
|
|
ti: 'Gestao de TI',
|
|
whatsapp: 'Autoatendimento para WhatsApp',
|
|
indefinido: 'A definir'
|
|
};
|
|
|
|
const URGENCY_LABELS = {
|
|
alta: 'Alta',
|
|
media: 'Media',
|
|
baixa: 'Baixa',
|
|
indefinida: 'Indefinida'
|
|
};
|
|
|
|
export function stripAccents(value = '') {
|
|
return String(value)
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '');
|
|
}
|
|
|
|
export function cleanText(value = '') {
|
|
return stripAccents(value)
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9\s@._+-]/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
function firstDefined(...values) {
|
|
return values.find((value) => value !== undefined && value !== null && value !== '') ?? '';
|
|
}
|
|
|
|
function numberFrom(value) {
|
|
if (value === undefined || value === null || value === '') return 0;
|
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
const match = String(value).match(/\d+/);
|
|
return match ? Number(match[0]) : 0;
|
|
}
|
|
|
|
function toArray(value) {
|
|
if (Array.isArray(value)) return value.filter(Boolean).map(String);
|
|
if (!value) return [];
|
|
return String(value)
|
|
.split(/[,;|]/)
|
|
.map((item) => item.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
export function normalizePhone(value = '') {
|
|
let digits = String(value).replace(/\D/g, '');
|
|
|
|
while (digits.startsWith('0')) digits = digits.slice(1);
|
|
|
|
if (digits.length === 10 || digits.length === 11) {
|
|
digits = `55${digits}`;
|
|
}
|
|
|
|
return digits;
|
|
}
|
|
|
|
export function detectService(value = '') {
|
|
const text = cleanText(value);
|
|
|
|
if (!text) return 'indefinido';
|
|
|
|
if (/\b(1|erp|sistema|gestao empresarial|estoque|financeiro|fiscal|nfce|nfe|pdv)\b/.test(text)) {
|
|
return 'erp';
|
|
}
|
|
|
|
if (/\b(2|ti|suporte|servidor|rede|backup|seguranca|computador|infraestrutura)\b/.test(text)) {
|
|
return 'ti';
|
|
}
|
|
|
|
if (/\b(3|whatsapp|autoatendimento|chatbot|bot|atendimento automatico|mensagem)\b/.test(text)) {
|
|
return 'whatsapp';
|
|
}
|
|
|
|
return 'indefinido';
|
|
}
|
|
|
|
export function normalizeUrgency(value = '') {
|
|
const text = cleanText(value);
|
|
|
|
if (!text) return 'indefinida';
|
|
if (/\b(alta|urgente|imediato|hoje|agora|essa semana|parado|problema serio|1)\b/.test(text)) return 'alta';
|
|
if (/\b(media|medio|proximo mes|em breve|avaliando|2)\b/.test(text)) return 'media';
|
|
if (/\b(baixa|sem pressa|futuro|pesquisa|so pesquisando|3)\b/.test(text)) return 'baixa';
|
|
|
|
return 'indefinida';
|
|
}
|
|
|
|
export function normalizeCurrentSystem(value = '') {
|
|
const text = cleanText(value);
|
|
|
|
if (!text) return 'nao_informado';
|
|
if (/\b(trocar|insatisfeito|migrar|outro sistema|uso outro|4)\b/.test(text)) return 'quer_trocar';
|
|
if (/\b(planilha|excel|manual|papel|3)\b/.test(text)) return 'planilhas';
|
|
if (/\b(sim|tenho|uso sistema|ja uso|1)\b/.test(text)) return 'usa_sistema';
|
|
if (/\b(nao|nenhum|nao tenho|sem sistema|2)\b/.test(text)) return 'sem_sistema';
|
|
|
|
return 'nao_informado';
|
|
}
|
|
|
|
export function normalizeLead(input = {}) {
|
|
const body = input.body && typeof input.body === 'object' ? input.body : input;
|
|
const contact = body.contact ?? body.contato ?? {};
|
|
const company = body.company ?? body.empresa_dados ?? {};
|
|
const message = body.message ?? body.mensagem ?? body.text ?? body.texto ?? body.description ?? '';
|
|
const serviceRaw = firstDefined(
|
|
body.service,
|
|
body.servico,
|
|
body.interest,
|
|
body.interesse,
|
|
body.solution,
|
|
body.solucao,
|
|
message
|
|
);
|
|
|
|
const city = firstDefined(company.city, company.cidade, body.city, body.cidade);
|
|
const state = firstDefined(company.state, company.uf, body.state, body.uf, body.estado);
|
|
const segment = firstDefined(company.segment, company.segmento, body.segment, body.segmento);
|
|
const phone = normalizePhone(firstDefined(contact.phone, contact.whatsapp, body.phone, body.telefone, body.whatsapp, body.from));
|
|
const name = firstDefined(contact.name, contact.nome, body.name, body.nome, body.contact_name, 'Lead sem nome');
|
|
const email = firstDefined(contact.email, body.email);
|
|
const companyName = firstDefined(company.name, company.nome, body.company_name, body.empresa, body.nome_empresa, 'Empresa nao informada');
|
|
const pains = toArray(firstDefined(body.pains, body.dores, body.pain, body.dor, body.maior_dificuldade));
|
|
const source = firstDefined(body.source, body.origem, body.canal, 'indefinida');
|
|
const service = detectService(serviceRaw);
|
|
const urgency = normalizeUrgency(firstDefined(body.urgency, body.urgencia, body.prazo, message));
|
|
const currentSystem = normalizeCurrentSystem(firstDefined(body.current_system, body.sistema_atual, body.usa_sistema, body.sistema, message));
|
|
const employees = numberFrom(firstDefined(company.employees, company.funcionarios, body.employees, body.funcionarios));
|
|
const computers = numberFrom(firstDefined(company.computers, company.computadores, body.computers, body.computadores, body.usuarios));
|
|
const whatsappVolume = numberFrom(firstDefined(body.whatsapp_volume, body.volume_whatsapp, body.mensagens_dia));
|
|
const consent = Boolean(firstDefined(body.consent, body.consentimento, body.opt_in, body.lgpd, false));
|
|
const cityKey = cleanText(city);
|
|
const segmentKey = cleanText(segment);
|
|
|
|
return {
|
|
id: body.id ?? body.lead_id ?? `GTI-${Date.now()}-${Math.random().toString(36).slice(2, 8).toUpperCase()}`,
|
|
createdAt: body.created_at ?? body.createdAt ?? new Date().toISOString(),
|
|
source: String(source || 'indefinida'),
|
|
name: String(name),
|
|
phone,
|
|
email: String(email || ''),
|
|
companyName: String(companyName),
|
|
city: String(city || ''),
|
|
state: String(state || ''),
|
|
segment: String(segment || ''),
|
|
service,
|
|
serviceLabel: SERVICE_LABELS[service],
|
|
urgency,
|
|
urgencyLabel: URGENCY_LABELS[urgency],
|
|
currentSystem,
|
|
pains,
|
|
message: String(message || ''),
|
|
employees,
|
|
computers,
|
|
whatsappVolume,
|
|
consent,
|
|
isLocal: TARGET_CITY_ALIASES.has(cityKey) || (cityKey.includes('santo antonio') && cityKey.includes('jesus')),
|
|
isTargetSegment: isTargetSegmentMatch(segmentKey),
|
|
raw: body
|
|
};
|
|
}
|
|
|
|
export function prioritizeLead(lead) {
|
|
const hasWhatsapp = Boolean(lead.phone);
|
|
|
|
return {
|
|
priority: hasWhatsapp ? 'com_whatsapp' : 'sem_whatsapp',
|
|
hasWhatsapp,
|
|
reasons: hasWhatsapp ? ['Possui WhatsApp/telefone para contato'] : []
|
|
};
|
|
}
|
|
|
|
export function buildNextQuestion(lead) {
|
|
if (lead.service === 'indefinido') {
|
|
return [
|
|
'Para te direcionar melhor, escolha uma opcao:',
|
|
'1 - Sistema ERP / gestao empresarial',
|
|
'2 - Suporte e gestao de TI',
|
|
'3 - Autoatendimento para WhatsApp',
|
|
'4 - Ainda nao sei, preciso de orientacao'
|
|
].join('\n');
|
|
}
|
|
|
|
if (lead.service === 'erp') {
|
|
if (!lead.segment) {
|
|
return [
|
|
'Qual o segmento da sua empresa?',
|
|
'1 - Comercio / loja',
|
|
'2 - Restaurante / delivery',
|
|
'3 - Mercado / supermercado',
|
|
'4 - Distribuidora',
|
|
'5 - Servicos',
|
|
'6 - Outro'
|
|
].join('\n');
|
|
}
|
|
|
|
if (lead.currentSystem === 'nao_informado') {
|
|
return [
|
|
'Hoje voce ja usa algum sistema?',
|
|
'1 - Sim',
|
|
'2 - Nao',
|
|
'3 - Uso planilhas',
|
|
'4 - Uso outro sistema, mas quero trocar'
|
|
].join('\n');
|
|
}
|
|
|
|
if (!lead.pains.length) {
|
|
return [
|
|
'Qual sua maior dificuldade hoje?',
|
|
'1 - Controle financeiro',
|
|
'2 - Estoque',
|
|
'3 - Vendas',
|
|
'4 - Emissao fiscal',
|
|
'5 - Relatorios',
|
|
'6 - Tudo muito manual'
|
|
].join('\n');
|
|
}
|
|
}
|
|
|
|
if (lead.service === 'ti') {
|
|
if (!lead.computers) {
|
|
return [
|
|
'Sua empresa possui quantos computadores ou usuarios?',
|
|
'1 - Ate 5',
|
|
'2 - 6 a 15',
|
|
'3 - 16 a 30',
|
|
'4 - Mais de 30'
|
|
].join('\n');
|
|
}
|
|
|
|
if (!lead.pains.length) {
|
|
return [
|
|
'Qual sua maior necessidade de TI?',
|
|
'1 - Suporte tecnico',
|
|
'2 - Servidores',
|
|
'3 - Rede / internet',
|
|
'4 - Backup',
|
|
'5 - Seguranca',
|
|
'6 - Terceirizar o TI'
|
|
].join('\n');
|
|
}
|
|
}
|
|
|
|
if (lead.service === 'whatsapp') {
|
|
if (!lead.pains.length) {
|
|
return [
|
|
'Voce quer automatizar qual tipo de atendimento?',
|
|
'1 - Vendas',
|
|
'2 - Suporte',
|
|
'3 - Agendamento',
|
|
'4 - Cardapio / pedidos',
|
|
'5 - Triagem de mensagens',
|
|
'6 - Financeiro / segunda via'
|
|
].join('\n');
|
|
}
|
|
|
|
if (!lead.whatsappVolume) {
|
|
return [
|
|
'Em media, quantas mensagens chegam no WhatsApp da empresa por dia?',
|
|
'1 - Ate 20',
|
|
'2 - 21 a 50',
|
|
'3 - 51 a 100',
|
|
'4 - Mais de 100'
|
|
].join('\n');
|
|
}
|
|
}
|
|
|
|
if (lead.urgency === 'indefinida') {
|
|
return [
|
|
'Qual a urgencia para resolver isso?',
|
|
'1 - Alta, preciso resolver agora',
|
|
'2 - Media, quero avaliar para breve',
|
|
'3 - Baixa, estou pesquisando'
|
|
].join('\n');
|
|
}
|
|
|
|
return [
|
|
'Pelo que voce informou, faz sentido um diagnostico rapido com a GTI.',
|
|
'Escolha o melhor horario:',
|
|
'1 - Hoje pela manha',
|
|
'2 - Hoje a tarde',
|
|
'3 - Amanha pela manha',
|
|
'4 - Amanha a tarde'
|
|
].join('\n');
|
|
}
|
|
|
|
export function buildInternalWhatsAppMessage(lead, priorityInfo) {
|
|
const pains = lead.pains.length ? lead.pains.join(', ') : 'Nao informado';
|
|
const phone = lead.phone || 'Nao informado';
|
|
const priority = priorityInfo.priority === 'com_whatsapp' ? 'COM WHATSAPP' : 'SEM WHATSAPP';
|
|
|
|
return [
|
|
`Novo lead captado - ${priority}`,
|
|
'',
|
|
`Empresa: ${lead.companyName}`,
|
|
`Contato: ${lead.name}`,
|
|
`WhatsApp: ${phone}`,
|
|
`Cidade/UF: ${lead.city || 'Nao informada'}${lead.state ? `/${lead.state}` : ''}`,
|
|
`Segmento: ${lead.segment || 'Nao informado'}`,
|
|
`Origem: ${lead.source}`,
|
|
`Dores: ${pains}`,
|
|
'',
|
|
'Acao recomendada: tratar manualmente pela equipe GTI.'
|
|
].join('\n');
|
|
}
|
|
|
|
export function buildCrmRecord(lead, priorityInfo) {
|
|
return {
|
|
lead_id: lead.id,
|
|
data_criacao: lead.createdAt,
|
|
status: 'Captado',
|
|
prioridade: priorityInfo.priority,
|
|
tem_whatsapp: Boolean(lead.phone),
|
|
origem: lead.source,
|
|
servico: lead.service,
|
|
servico_label: lead.serviceLabel,
|
|
empresa: lead.companyName,
|
|
contato: lead.name,
|
|
whatsapp: lead.phone,
|
|
email: lead.email,
|
|
cidade: lead.city,
|
|
uf: lead.state,
|
|
segmento: lead.segment,
|
|
urgencia: lead.urgency,
|
|
sistema_atual: lead.currentSystem,
|
|
dores: lead.pains.join('; '),
|
|
funcionarios: lead.employees,
|
|
computadores: lead.computers,
|
|
volume_whatsapp: lead.whatsappVolume,
|
|
consentimento: lead.consent,
|
|
proxima_acao: 'Tratamento manual',
|
|
observacao: lead.message
|
|
};
|
|
}
|
|
|
|
export function processLead(input = {}, options = {}) {
|
|
const lead = normalizeLead(input);
|
|
const priority = prioritizeLead(lead);
|
|
const commercialWhatsApp = options.commercialWhatsApp || process.env.GTI_COMMERCIAL_WHATSAPP || '{{GTI_COMMERCIAL_WHATSAPP}}';
|
|
|
|
return {
|
|
lead,
|
|
priority,
|
|
crmRecord: buildCrmRecord(lead, priority),
|
|
whatsapp: {
|
|
internal: {
|
|
to: commercialWhatsApp,
|
|
message: buildInternalWhatsAppMessage(lead, priority)
|
|
}
|
|
},
|
|
nextAction: 'tratamento_manual'
|
|
};
|
|
}
|