Gestão de Clientes
Carregando...
🔴 Manutenções vencidas — clientes que não retornaram
🟡 Manutenções nos próximos 7 dias
🏆 Ranking VIP — maiores LTVs
📊 Sazonalidade mensal
🎯 Origem dos clientes
📦 Produtos mais vendidos
🎂 Aniversariantes do mês
😴 Clientes sumidos (+90 dias)
Cliente
Telefone
Cidade / Bairro
Origem
Status
Ações
Cliente
NPS
Classificação
Ação
🔗 Google Sheets
Planilha com 3 abas:
clientes: id|nome|telefone|email|cidade|bairro|aniversario|origem|interesse|tags|status|nps|depoimento|ultimoContato|cadastroEm|obs
compras: id|clienteId|produto|data|valor|formaPagamento|vendedor|proximaManutencao|frequencia|obs
produtos: id|nome|categoria|descricao|preco|custo|margem|frequencia|status|tags|cadastroEm
🔐 Senha
Padrão: admin123
A senha é salva no Google Sheets (aba config) e sincroniza em todos os dispositivos automaticamente. Se o Sheets não estiver configurado, fica salva só neste navegador.
Exigir senha para entrar
Se desativado, o sistema abre direto, sem pedir senha.
🧹 Limpar Cache
Se o sistema travar ou não carregar, use este botão para limpar o cache do navegador e reiniciar.
💬 Mensagens do WhatsApp
Personalize as mensagens enviadas pelo sistema. Use {nome} para inserir o nome do cliente automaticamente.
⚙️ Código do Apps Script (backend)
Na planilha do Google Sheets: Extensões → Apps Script, apague tudo e cole o código abaixo. Rode a função setup uma vez (▶ Executar) para criar as abas. Depois Implantar → Nova implantação → App da Web (executar como Eu, acesso Qualquer pessoa) e cole a URL que termina em /exec no campo lá em cima.
/*************************************************************
 * CAIXA ESQUECIDO / CLIENTEBASE — Backend (Google Apps Script)
 * Edifício Web — Engenharia de Receita
 *
 * Compatível 100% com clientebase_v5.html
 *
 * COMO INSTALAR (faça uma vez):
 *  1. Abra a planilha do cliente no Google Sheets.
 *  2. Menu  Extensões → Apps Script.
 *  3. Apague tudo que estiver lá e cole ESTE arquivo inteiro.
 *  4. Clique em Salvar (ícone de disquete).
 *  5. No menu de funções (em cima), selecione "setup" e clique em ▶ Executar.
 *     -> Isso cria as 4 abas (clientes, compras, produtos, config) já com
 *        os cabeçalhos certos. Autorize o acesso quando pedir.
 *  6. Clique em Implantar → Nova implantação.
 *       Tipo: App da Web
 *       Executar como: Eu (você)
 *       Quem pode acessar: Qualquer pessoa
 *     Copie a URL que termina em /exec.
 *  7. No sistema, vá em Configurações → cole a URL em "URL do Apps Script" → Salvar.
 *
 * IMPORTANTE: a cada vez que você ALTERAR este código, precisa
 * "Implantar → Gerenciar implantações → Editar (lápis) → Nova versão"
 * para a mudança valer na URL.
 *************************************************************/

// ── Estrutura das abas (a ORDEM das colunas tem que bater com o sistema) ──
var SCHEMA = {
  clientes: ['id','nome','telefone','email','cidade','bairro','aniversario','origem','interesse','tags','status','nps','depoimento','ultimoContato','cadastroEm','obs'],
  compras:  ['id','clienteId','produto','data','valor','formaPagamento','vendedor','proximaManutencao','frequencia','obs'],
  produtos: ['id','nome','categoria','descricao','preco','custo','margem','frequencia','status','tags','cadastroEm'],
  config:   ['chave','valor']
};

/**
 * Rode UMA vez (botão ▶ Executar) para criar todas as abas com cabeçalho.
 * Pode rodar quantas vezes quiser — não apaga dados, só garante a estrutura.
 */
function setup() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  Object.keys(SCHEMA).forEach(function(name){ ensureSheet_(ss, name); });
  // Remove a aba "Página1"/"Sheet1" vazia, se sobrou da planilha em branco
  var lixo = ss.getSheetByName('Página1') || ss.getSheetByName('Sheet1');
  if (lixo && ss.getSheets().length > 1 && lixo.getLastRow() === 0) {
    ss.deleteSheet(lixo);
  }
  return 'Setup concluído ✅ — abas criadas/conferidas.';
}

/**
 * Garante que a aba existe e que a primeira linha tem o cabeçalho correto.
 * Esse é o ponto que faltava no script antigo (criava aba sem cabeçalho).
 */
function ensureSheet_(ss, name) {
  var headers = SCHEMA[name];
  var sheet = ss.getSheetByName(name);
  if (!sheet) {
    sheet = ss.insertSheet(name);
  }
  // Se a aba está vazia OU a primeira coluna não bate, (re)escreve o cabeçalho
  if (sheet.getLastRow() === 0 || String(sheet.getRange(1,1).getValue()) !== headers[0]) {
    sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
    sheet.setFrozenRows(1);
  }
  return sheet;
}

// ── Saídas padronizadas ──
function jsonOut_(obj) {
  return ContentService
    .createTextOutput(JSON.stringify(obj))
    .setMimeType(ContentService.MimeType.JSON);
}

/**
 * Ponto de entrada único. O sistema chama:
 *  - LEITURA:  ...?sheet=clientes        -> devolve array de objetos
 *  - ESCRITA:  ...?payload=base64({action,sheet,id,row})
 *              action = 'salvar' | 'editar' | 'deletar'
 */
function doGet(e) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();

  // ---------- ESCRITA ----------
  if (e && e.parameter && e.parameter.payload) {
    try {
      var data = JSON.parse(
        Utilities.newBlob(
          Utilities.base64Decode(e.parameter.payload, Utilities.Charset.UTF_8)
        ).getDataAsString('UTF-8')
      );

      var name = data.sheet || 'clientes';
      var sheet = SCHEMA[name] ? ensureSheet_(ss, name) : (ss.getSheetByName(name) || ss.insertSheet(name));

      if (data.action === 'salvar') {
        // Upsert por id (coluna 0): se a linha já existe, sobrescreve; senão, adiciona.
        // Isso evita linhas duplicadas caso a fila offline reenvie um "salvar"
        // cuja resposta se perdeu depois de já ter sido gravado.
        var rowsS = sheet.getDataRange().getValues();
        var novoId = String(data.row[0]);
        var achouS = -1;
        for (var s = 1; s < rowsS.length; s++) {
          if (String(rowsS[s][0]) === novoId) { achouS = s; break; }
        }
        if (achouS >= 0) {
          sheet.getRange(achouS + 1, 1, 1, data.row.length).setValues([data.row]);
        } else {
          sheet.appendRow(data.row);
        }
      }

      else if (data.action === 'editar') {
        var rowsE = sheet.getDataRange().getValues();
        for (var i = 1; i < rowsE.length; i++) {
          if (String(rowsE[i][0]) === String(data.id)) {
            sheet.getRange(i + 1, 1, 1, data.row.length).setValues([data.row]);
            break;
          }
        }
      }

      else if (data.action === 'deletar') {
        var rowsD = sheet.getDataRange().getValues();
        for (var j = 1; j < rowsD.length; j++) {
          if (String(rowsD[j][0]) === String(data.id)) {
            sheet.deleteRow(j + 1);
            break;
          }
        }
      }

      return jsonOut_({ ok: true });
    } catch (err) {
      return jsonOut_({ ok: false, error: err.message });
    }
  }

  // ---------- LEITURA ----------
  try {
    var which = (e && e.parameter && e.parameter.sheet) || 'clientes';
    var rsheet = SCHEMA[which] ? ensureSheet_(ss, which) : ss.getSheetByName(which);
    if (!rsheet) return jsonOut_([]);

    var rows = rsheet.getDataRange().getValues();
    if (rows.length < 2) return jsonOut_([]); // só cabeçalho ou vazio

    var headers = rows[0];
    var out = rows.slice(1)
      .filter(function(r){ return String(r[0]).trim() !== ''; }) // ignora linhas sem id
      .map(function(r){
        var o = {};
        headers.forEach(function(h, i){ o[h] = r[i]; });
        return o;
      });

    return jsonOut_(out);
  } catch (err2) {
    return jsonOut_([]);
  }
}

// Alguns navegadores fazem POST; aceitamos e tratamos igual ao GET.
function doPost(e) {
  return doGet(e);
}