Tutorial – Crie um Cadastro de Clientes com

E-mail Automático

Neste tutorial passo a passo, você aprenderá a construir um sistema completo de cadastro de clientes usando Google Planilhas e Apps Script. O grande diferencial deste projeto é a automação: a cada novo cliente cadastrado através da sua página web, um e-mail de boas-vindas será enviado automaticamente.

É a ferramenta perfeita para economizar seu tempo e causar uma primeira impressão profissional.

Pré-requisitos da Planilha:

Antes de começar, prepare sua Planilha Google com uma aba chamada Clientes. Esta aba deve conter os seguintes cabeçalhos na primeira linha: ID_Cliente, DataCadastro, NomeCompleto, Telefone, Email, StatusLead, ObservacoesCliente

Código.gs

// ==========================================================================
// ARQUIVO: Code.gs 
// ==========================================================================

// --- CONSTANTES GLOBAIS ---
const HTML_INDEX = 'Index';
const PROJECT_TITLE = 'Cadastro de Clientes';
const SHEET_CLIENTES = 'Clientes';

// Cabeçalhos que serão criados na planilha
const HEADERS_CLIENTES = ["ID_Cliente", "DataCadastro", "NomeCompleto", "Telefone", "Email", "StatusLead", "ObservacoesCliente"];

// --- MODELO DO E-MAIL DE BOAS-VINDAS ---
const WELCOME_EMAIL_SUBJECT = "Seja Bem-Vindo(a)! Cadastro Confirmado.";
const WELCOME_EMAIL_BODY = `
  <div style="font-family: Arial, sans-serif; font-size: 16px; line-height: 1.6;">
    <p>Olá, <strong>{{NOME}}</strong>,</p>
    <p>É com grande alegria que confirmamos o seu cadastro em nosso sistema!</p>
    <p>Agradecemos por se juntar a nós. Em breve, um de nossos especialistas entrará em contato.</p>
    <p>Atenciosamente,<br><strong>Nossa Equipe</strong></p>
  </div>
`;

// ==========================================================================
// FUNÇÃO PRINCIPAL E CONFIGURAÇÃO DA PLANILHA
// ==========================================================================

function doGet(e) {
  setupSheet(SHEET_CLIENTES, HEADERS_CLIENTES);
  
  let tpl = HtmlService.createTemplateFromFile(HTML_INDEX);
  tpl.projectTitle = PROJECT_TITLE;

  return tpl.evaluate()
            .addMetaTag('viewport', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no')
            .setTitle(PROJECT_TITLE);
}

/**
 * Garante que a planilha exista com os cabeçalhos corretos.
 * @param {string} sheetName O nome da aba.
 * @param {Array<string>} headers A lista de cabeçalhos.
 * @returns {Object} O objeto da planilha (aba).
 */
function setupSheet(sheetName, headers) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName(sheetName);
  if (!sheet) {
    sheet = ss.insertSheet(sheetName);
    sheet.appendRow(headers);
    sheet.setFrozenRows(1);
  }
  return sheet; // Retorna o objeto da planilha para ser usado por outras funções.
}


// ==========================================================================
// FUNÇÕES DO SERVIDOR EXPOSTAS AO FRONT-END
// ==========================================================================

/**
 * Adiciona um novo cliente à planilha.
 * @param {object} clienteData Objeto com os dados do cliente do formulário.
 * @returns {object} Um objeto de sucesso contendo os dados do cliente adicionado, ou um objeto de erro.
 */
function addCliente(clienteData) {
  try {
    const sheet = setupSheet(SHEET_CLIENTES, HEADERS_CLIENTES);
    
    const newId = "CLIENTE_" + new Date().getTime();
    const now = new Date();
    
    const newRowArray = HEADERS_CLIENTES.map(header => {
      if (header === "ID_Cliente") return newId;
      if (header === "DataCadastro") return now;
      return clienteData[header] || "";
    });
    
    sheet.appendRow(newRowArray);

    // Converte a linha de volta para um objeto para retornar ao front-end
    const newClientObject = {};
    HEADERS_CLIENTES.forEach((header, index) => {
      const value = newRowArray[index];
      // CORREÇÃO CRÍTICA: Converte objetos de Data para texto antes de enviar
      if (value instanceof Date) {
        newClientObject[header] = value.toISOString();
      } else {
        newClientObject[header] = value;
      }
    });
    
    // Retorna sucesso e os dados do cliente para o front-end usar no e-mail
    return { success: true, client: newClientObject };

  } catch (e) {
    Logger.log(`ERRO em addCliente: ${e.message}`);
    return { error: e.message };
  }
}

/**
 * Envia um e-mail de boas-vindas para o cliente.
 * @param {object} clientData Objeto com os dados do cliente (NomeCompleto e Email).
 * @returns {object} Um objeto de sucesso ou erro.
 */
function sendWelcomeEmail(clientData) {
  if (!clientData || !clientData.Email || !clientData.NomeCompleto) {
    return { error: "Dados do cliente incompletos para enviar o e-mail." };
  }
  
  try {
    const emailBody = WELCOME_EMAIL_BODY.replace('{{NOME}}', clientData.NomeCompleto);
    
    MailApp.sendEmail({
      to: clientData.Email,
      subject: WELCOME_EMAIL_SUBJECT,
      htmlBody: emailBody
    });
    
    return { success: true };

  } catch (e) {
    Logger.log(`ERRO em sendWelcomeEmail: ${e.message}`);
    return { error: `Falha ao enviar e-mail: ${e.message}` };
  }
}

Index.html

<!DOCTYPE html>
<html>

<head>
  <base target="_top">
  <meta charset="UTF-8">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

  <style>
    /* --- ESTILOS VISUAIS --- */
    :root {
      --primary-color: #3498db;
      --primary-dark: #2980b9;
      --secondary-color: #95a5a6;
      --success-color: #2ecc71;
      --danger-color: #e74c3c;
      --warning-color: #f1c40f;
      --info-color: #5bc0de;
      --light-gray: #ecf0f1;
      --medium-gray: #bdc3c7;
      --dark-gray: #2c3e50;
      --text-color: #34495e;
      --text-color-light: #ecf0f1;
      --body-bg: #eef1f2;
      --card-bg: #ffffff;
      --shadow: 0 2px 5px rgba(44, 62, 80, 0.1);
      --border-radius: 0.4rem;
    }

    *,
    *::before,
    *::after {
      box-sizing: border-box;
    }

    body,
    html {
      margin: 0;
      padding: 0;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
      font-size: 16px;
      line-height: 1.5;
      background-color: var(--body-bg);
      color: var(--text-color);
      height: 100%;
    }

    .app-container {
      display: flex;
      justify-content: center;
      align-items: flex-start;
      padding: 30px 15px;
      min-height: 100vh;
    }

    .form-card {
      width: 100%;
      max-width: 500px;
      background: var(--card-bg);
      border-radius: var(--border-radius);
      box-shadow: var(--shadow);
      padding: 1.5rem;
      position: relative;
      overflow: hidden;
    }

    .form-card h2 {
      margin: 0 0 1.5rem 0;
      text-align: center;
      color: var(--dark-gray);
      font-size: 1.5rem;
      font-weight: 500;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
    }

    .btn {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 0.5rem 1rem;
      border: 1px solid transparent;
      border-radius: var(--border-radius);
      font-size: 0.95rem;
      font-weight: 400;
      cursor: pointer;
      text-decoration: none;
      transition: all 0.2s ease;
    }

    .btn-primary {
      background-color: var(--primary-color);
      color: white;
      border-color: var(--primary-color);
    }

    .btn-primary:hover {
      background-color: var(--primary-dark);
      border-color: var(--primary-dark);
    }

    .btn-secondary {
      background-color: var(--secondary-color);
      color: white;
      border-color: var(--secondary-color);
    }

    .btn-secondary:hover {
      background-color: #7f8c8d;
    }

    .btn-block {
      width: 100%;
      padding-top: 0.7rem;
      padding-bottom: 0.7rem;
      font-size: 1rem;
    }

    .modal-backdrop {
      display: none;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.5);
      z-index: 1040;
    }

    .modal {
      display: none;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 1050;
      justify-content: center;
      align-items: center;
      padding: 15px;
    }

    .modal.open {
      display: flex;
    }

    .modal-content {
      position: relative;
      display: flex;
      flex-direction: column;
      width: 100%;
      background-color: var(--card-bg);
      border-radius: var(--border-radius);
      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }

    .modal-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 1rem;
      border-bottom: 1px solid var(--medium-gray);
    }

    .modal-header h3 {
      margin: 0;
      font-size: 1.25rem;
    }

    .modal-body {
      padding: 1.5rem;
    }

    .modal-footer {
      display: flex;
      justify-content: flex-end;
      gap: 0.5rem;
      padding: 1rem;
      border-top: 1px solid var(--medium-gray);
    }

    .form-group {
      margin-bottom: 1rem;
    }

    .form-group label {
      display: block;
      margin-bottom: 0.5rem;
      font-weight: 500;
    }

    .form-group input,
    .form-group select,
    .form-group textarea {
      width: 100%;
      padding: 0.6rem 0.75rem;
      border: 1px solid var(--medium-gray);
      border-radius: var(--border-radius);
    }

    .form-group input:focus,
    .form-group select:focus,
    .form-group textarea:focus {
      outline: none;
      border-color: var(--primary-color);
      box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
    }

    .form-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 1rem;
    }

    .loading-spinner-modal {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      display: none;
      align-items: center;
      justify-content: center;
      background-color: rgba(255, 255, 255, 0.8);
      z-index: 10;
    }

    .loading-spinner-modal::after {
      content: '';
      display: block;
      width: 40px;
      height: 40px;
      border: 4px solid var(--medium-gray);
      border-top-color: var(--primary-color);
      border-radius: 50%;
      animation: spin 0.8s linear infinite;
    }

    @keyframes spin {
      to {
        transform: rotate(360deg);
      }
    }

    #app-loader {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: var(--body-bg);
      z-index: 9999;
      display: flex;
      justify-content: center;
      align-items: center;
      transition: opacity 0.6s ease, visibility 0.6s ease;
    }

    #app-loader.hidden {
      opacity: 0;
      visibility: hidden;
    }

    .spinner {
      width: 44px;
      height: 44px;
      border: 4px solid rgba(44, 62, 80, 0.15);
      border-left-color: var(--dark-gray);
      border-radius: 50%;
      display: inline-block;
      animation: spin 1.2s linear infinite;
    }

    .notification-toast {
      position: fixed;
      top: 20px;
      left: 50%;
      transform: translate(-50%, -150%);
      z-index: 9999;
      display: flex;
      align-items: center;
      padding: 12px 20px;
      min-width: 300px;
      max-width: 90%;
      border-radius: var(--border-radius);
      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
      color: white;
      opacity: 0;
      visibility: hidden;
      transition: opacity 0.4s ease, transform 0.4s ease, visibility 0.4s;
    }

    .notification-toast.show {
      opacity: 1;
      visibility: visible;
      transform: translate(-50%, 0);
    }

    .notification-toast.success {
      background-color: var(--success-color);
    }

    .notification-toast.error {
      background-color: var(--danger-color);
    }

    .notification-icon {
      margin-right: 15px;
      font-size: 1.5rem;
    }

    .notification-message {
      margin: 0;
      font-weight: 500;
    }
  </style>
</head>

<body>
  <!-- Componentes de UI -->
  <div id="notification-toast" class="notification-toast">
    <span class="material-icons notification-icon"></span>
    <p class="notification-message"></p>
  </div>
  <div id="app-loader">
    <div class="spinner"></div>
  </div>
  <div id="modalBackdrop" class="modal-backdrop"></div>

  <!-- Container Principal com o Formulário -->
  <div class="app-container">
    <div class="form-card">
      <h2><span class="material-icons">person_add</span> Cadastro de Cliente</h2>
      <form id="clienteForm">
        <div class="loading-spinner-modal"></div>
        <div class="form-group">
          <label for="NomeCompleto">Nome Completo</label>
          <input type="text" id="NomeCompleto" name="NomeCompleto" required>
        </div>
        <div class="form-grid">
          <div class="form-group">
            <label for="Telefone">Telefone</label>
            <input type="tel" id="Telefone" name="Telefone">
          </div>
          <div class="form-group">
            <label for="Email">E-mail</label>
            <input type="email" id="Email" name="Email">
          </div>
        </div>
        <div class="form-group">
          <label for="StatusLead">Status do Lead</label>
          <select id="StatusLead" name="StatusLead">
                <option>Novo</option><option>Contatado</option><option>Qualificado</option><option>Inativo</option>
              </select>
        </div>
        <div class="form-group">
          <label for="ObservacoesCliente">Observações</label>
          <textarea id="ObservacoesCliente" name="ObservacoesCliente" rows="3"></textarea>
        </div>
        <button type="submit" class="btn btn-primary btn-block" style="margin-top: 1.5rem;">Cadastrar Cliente</button>
      </form>
    </div>
  </div>

  <!-- Modal de Confirmação para envio de e-mail -->
  <div id="confirmModal" class="modal">
    <div class="modal-content" style="max-width: 450px;">
      <div class="modal-header">
        <h3 id="confirmModalTitle"></h3>
      </div>
      <div class="modal-body">
        <p id="confirmModalMessage" style="font-size: 1.1rem; text-align: center;"></p>
      </div>
      <div class="modal-footer">
        <button type="button" id="confirmModalCancelBtn" class="btn btn-secondary">Agora Não</button>
        <button type="button" id="confirmModalOkBtn" class="btn btn-primary"></button>
      </div>
    </div>
  </div>

  <script>
    document.addEventListener('DOMContentLoaded', function() {
  // ==========================================================================
  // REFERÊNCIAS GLOBAIS E ESTADO
  // ==========================================================================
  const appLoader = document.getElementById('app-loader');
  const clienteForm = document.getElementById('clienteForm');
  const modalBackdrop = document.getElementById('modalBackdrop');
  let notificationTimer;

  // ==========================================================================
  // LÓGICA DE MODAIS E LOADERS
  // ==========================================================================
  function openModal(modalId) { document.getElementById(modalId).classList.add('open'); modalBackdrop.style.display = 'block'; }
  function closeModal(modalId) { document.getElementById(modalId).classList.remove('open'); modalBackdrop.style.display = 'none'; }
  function setFormLoading(isLoading) { document.querySelector('#clienteForm .loading-spinner-modal').style.display = isLoading ? 'flex' : 'none'; }
  
  // ==========================================================================
  // FUNÇÕES UTILITÁRIAS DE FORMATAÇÃO
  // ==========================================================================
  function toTitleCase(str) { return str.toLowerCase().replace(/(?:^|\s|-)\S/g, x => x.toUpperCase()); }
  function formatPhone(input) { let v = input.value.replace(/\D/g,'').substring(0,11); v=v.replace(/^(\d{2})(\d)/g,"($1) $2"); v=v.replace(/(\d{5})(\d)/,"$1-$2"); input.value=v; }

  // ==========================================================================
  // LÓGICA PRINCIPAL DO FORMULÁRIO DE CLIENTE
  // ==========================================================================
  clienteForm.addEventListener('submit', function(e) {
    e.preventDefault();
    const submitBtn = this.querySelector('button[type="submit"]');
    const originalBtnText = submitBtn.textContent;

    submitBtn.disabled = true;
    submitBtn.textContent = 'Salvando...';
    setFormLoading(true);
    
    const clienteData = Object.fromEntries(new FormData(this).entries());

    google.script.run
      .withSuccessHandler(response => {
        setFormLoading(false);
        submitBtn.disabled = false;
        submitBtn.textContent = originalBtnText;

        if (response && response.error) {
          showNotification(`Erro: ${response.error}`, 'error');
          return;
        }

        if (response && response.success) {
          clienteForm.reset();
          
          const modal = document.getElementById('confirmModal');
          modal.querySelector('#confirmModalTitle').textContent = 'Cadastro Realizado!';
          modal.querySelector('#confirmModalMessage').textContent = 'Deseja enviar um e-mail de Boas-Vindas para o cliente?';
          const okBtn = modal.querySelector('#confirmModalOkBtn');
          const cancelBtn = modal.querySelector('#confirmModalCancelBtn');
          
          okBtn.textContent = 'Sim, Enviar E-mail';

          okBtn.onclick = () => {
            const okBtnOriginalText = okBtn.textContent;
            okBtn.disabled = true;
            okBtn.textContent = 'Enviando...';
            
            google.script.run
              .withSuccessHandler(emailResponse => {
                closeModal('confirmModal');
                showNotification(emailResponse.success ? 'E-mail de boas-vindas enviado!' : emailResponse.error, emailResponse.success ? 'success' : 'error');
                okBtn.disabled = false;
                okBtn.textContent = okBtnOriginalText;
              })
              .withFailureHandler(err => {
                 closeModal('confirmModal');
                 showNotification(`Falha crítica: ${err.message}`, 'error');
                 okBtn.disabled = false;
                 okBtn.textContent = okBtnOriginalText;
              })
              .sendWelcomeEmail(response.client);
          };

          cancelBtn.onclick = () => {
            closeModal('confirmModal');
            showNotification('Cliente cadastrado com sucesso!', 'success');
          };
          
          openModal('confirmModal');
        }
      })
      .withFailureHandler(e => {
          setFormLoading(false);
          showNotification(`Erro de comunicação: ${e.message}`, 'error');
          submitBtn.disabled = false;
          submitBtn.textContent = originalBtnText;
      })
      .addCliente(clienteData);
  });

  // ==========================================================================
  // SISTEMA DE NOTIFICAÇÕES (TOAST)
  // ==========================================================================
  function showNotification(message, type = 'success', duration = 4000) {
    const toast = document.getElementById('notification-toast');
    if (!toast) return;
    clearTimeout(notificationTimer);
    toast.className = 'notification-toast';
    
    toast.querySelector('.notification-icon').textContent = type === 'success' ? 'check_circle' : 'error';
    toast.querySelector('.notification-message').textContent = message;
    
    toast.classList.add(type);
    toast.classList.add('show');
    
    notificationTimer = setTimeout(() => { toast.classList.remove('show'); }, duration);
  }

  // ==========================================================================
  // INICIALIZAÇÃO DA APLICAÇÃO
  // ==========================================================================
  function initializeApp() {
    setTimeout(() => appLoader?.classList.add('hidden'), 200);
    document.getElementById('NomeCompleto').addEventListener('input', (e) => { e.target.value = toTitleCase(e.target.value); });
    document.getElementById('Telefone').addEventListener('input', (e) => formatPhone(e.target));
    document.getElementById('NomeCompleto').focus();
    modalBackdrop.addEventListener('click', () => closeModal('confirmModal'));
  }

  initializeApp();
});
  </script>
</body>

</html>

Acompanhe nas redes sociais

Projetos Personalizados

Precisa de uma solução sob medida?

© 2025 Transformando Planilhas. Todos os direitos reservados.