Tutorial – Crie uma Interface Inteligente para Lançar Vendas e Gerar o QR Code do PIX

Cansado de ter que abrir o aplicativo do seu banco toda vez que precisa gerar uma cobrança para um cliente? E o pior: depois ainda ter que anotar tudo manualmente na sua planilha? Neste tutorial, vamos levar o conceito de lançador de dados para o nível profissional. Vamos construir uma Planilha Inteligente que funciona como um verdadeiro terminal de vendas.

Você aprenderá como criar uma interface web moderna onde registra o cliente e o valor, e o sistema gera — na hora — o QR Code do PIX e o código “Copia e Cola” com o valor exato da venda. Além disso, incluímos um placar de faturamento que soma suas vendas do dia automaticamente para te manter focado no resultado.

O que você vai aprender:

  • Engenharia do Payload PIX: Como usar o Google Apps Script para calcular a linha de código do PIX baseada no valor da sua venda.
  • Interface Estilo “Fintech”: O passo a passo para construir um design escuro e moderno (HTML/CSS) que transforma a aparência da sua planilha em um aplicativo de alta tecnologia.
  • Placar de Vendas em Tempo Real: Como criar uma função de monitoramento que busca os dados na planilha e exibe o total vendido no dia diretamente no topo da sua tela.
  • Integração com QR Code API: Como conectar seu sistema a uma API gratuita para transformar os dados de pagamento em uma imagem escaneável por qualquer celular.

Código.gs

/**
 * IMPORTANTE: Este script gera payloads de PIX baseados nos dados da aba 'Configurações'.
 * A conferência da Chave Pix e do Nome do Recebedor é de inteira responsabilidade do usuário.
 * Realize testes com valores mínimos (R$ 0,01) antes de utilizar.
 */

// ==========================================================================
// ARQUIVO: Código.gs
// ==========================================================================

function doGet() {
  return HtmlService.createHtmlOutputFromFile('Index')
    .setTitle("Lançador de Vendas com Pix")
    .addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

/**
 * Calcula o total vendido na data de hoje (Fuso Horário Brasil)
 */
function getTotalVendasHoje() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName("Vendas");
  if (!sheet) return "0,00";
  
  const lastRow = sheet.getLastRow();
  if (lastRow <= 1) return "0,00";
  
  const dados = sheet.getRange(2, 1, lastRow - 1, 4).getValues();
  const timezone = ss.getSpreadsheetTimeZone();
  const hojeString = Utilities.formatDate(new Date(), timezone, "yyyy-MM-dd");
  
  let total = 0;
  for (let i = 0; i < dados.length; i++) {
    const dataLinha = dados[i][0];
    if (dataLinha instanceof Date) {
      const dataString = Utilities.formatDate(dataLinha, timezone, "yyyy-MM-dd");
      if (dataString === hojeString) total += Number(dados[i][3]) || 0;
    }
  }
  return total.toLocaleString('pt-BR', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}

/**
 * Registra a venda e retorna o código Pix
 */
function registrarVendaEGerarPix(dados) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const abaVendas = ss.getSheetByName("Vendas");
  const abaConfig = ss.getSheetByName("Configurações");
  
  const chave = String(abaConfig.getRange("B1").getValue()).trim();
  let nome = String(abaConfig.getRange("B2").getValue()).trim();
  let cidade = String(abaConfig.getRange("B3").getValue()).trim();
  const txtId = String(abaConfig.getRange("B4").getValue()).trim() || "***";

  nome = removeAcentos(nome).substring(0, 25);
  cidade = removeAcentos(cidade);
  if (!cidade) cidade = "Cidade Nao Informada";

  const valorNum = parseFloat(dados.valor.replace(/\./g, '').replace(',', '.'));
  const valorFmt = valorNum.toFixed(2);

  const dataParts = dados.data.split('-');
  const dataObj = new Date(dataParts[0], dataParts[1] - 1, dataParts[2], 12, 0, 0);

  const pixPayload = gerarPayloadPix(chave, nome, cidade, txtId, valorFmt);
  
  abaVendas.appendRow([dataObj, dados.cliente, dados.descricao, valorNum, pixPayload]);
  
  const lastRow = abaVendas.getLastRow();
  abaVendas.getRange(lastRow, 1).setNumberFormat("dd/mm/yyyy");
  abaVendas.getRange(lastRow, 4).setNumberFormat("R$ #,##0.00");

  return {
    success: true,
    pixCode: pixPayload,
    valor: valorFmt.replace('.', ','),
    cliente: dados.cliente,
    novoTotal: getTotalVendasHoje()
  };
}

function removeAcentos(t) { return t.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); }

// --- LÓGICA DO PIX ---
function gerarPayloadPix(k, n, c, i, v) {
  const f = (id, val) => id + val.length.toString().padStart(2, "0") + val;
  const p = [ f("00", "01"), f("26", f("00", "BR.GOV.BCB.PIX") + f("01", k)), f("52", "0000"), f("53", "986"), f("54", v), f("58", "BR"), f("59", n), f("60", c), f("62", f("05", i)) ].join('');
  return p + "6304" + getCRC16(p + "6304");
}

function getCRC16(p) {
  let res = 0xFFFF;
  for (let o = 0; o < p.length; o++) {
    res ^= (p.charCodeAt(o) << 8);
    for (let b = 0; b < 8; b++) {
      if ((res <<= 1) & 0x10000) res ^= 0x1021;
      res &= 0xFFFF;
    }
  }
  return res.toString(16).toUpperCase().padStart(4, "0");
}

Index.html

<!DOCTYPE html>
<html>

<head>
  <base target="_top">
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <style>
    :root {
      --brand: #32bcad;
      --dark: #1e1e1e;
      --light: #f5f5f5;
    }

    body {
      font-family: 'Inter', sans-serif;
      background-color: var(--dark);
      color: var(--light);
      display: flex;
      flex-direction: column;
      align-items: center;
      min-height: 100vh;
      margin: 0;
      padding: 20px;
      box-sizing: border-box;
    }

    .scoreboard {
      background: rgba(50, 188, 173, 0.1);
      border: 1px solid var(--brand);
      color: var(--brand);
      padding: 10px 20px;
      border-radius: 50px;
      margin-bottom: 20px;
      font-weight: 600;
      font-size: 0.9rem;
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .container {
      background-color: #2a2a2a;
      padding: 30px;
      border-radius: 20px;
      width: 100%;
      max-width: 380px;
      box-shadow: 0 15px 35px rgba(0, 0, 0, 0.6);
      text-align: center;
    }

    h2 {
      color: var(--brand);
      margin-top: 0;
      font-size: 1.5rem;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 10px;
    }

    .input-group {
      text-align: left;
      margin-bottom: 12px;
    }

    .input-group label {
      font-size: 0.8rem;
      color: #aaa;
      margin-left: 5px;
    }

    input {
      width: 100%;
      padding: 14px;
      margin-top: 5px;
      border-radius: 10px;
      border: 1px solid #444;
      background: #333;
      color: white;
      font-size: 1rem;
      box-sizing: border-box;
      transition: 0.2s;
    }

    input:focus {
      outline: none;
      border-color: var(--brand);
    }

    .money-input {
      font-size: 1.6rem;
      font-weight: 800;
      color: var(--brand);
      text-align: center;
      letter-spacing: 1px;
    }

    button {
      width: 100%;
      padding: 14px;
      border: none;
      border-radius: 10px;
      font-weight: bold;
      cursor: pointer;
      font-size: 1rem;
      margin-top: 10px;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
      transition: 0.2s;
    }

    .btn-primary {
      background-color: var(--brand);
      color: #000;
    }

    .btn-primary:hover {
      transform: translateY(-2px);
      filter: brightness(1.1);
    }

    .btn-copy {
      background: #444;
      color: white;
    }

    .btn-outline {
      background: transparent;
      border: 1px solid #555;
      color: #aaa;
    }

    #step-pix,
    #loader {
      display: none;
    }

    /* Escondidos por padrão */

    .qr-container {
      background: white;
      padding: 10px;
      border-radius: 15px;
      margin: 20px auto;
      width: 220px;
      height: 220px;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    .qr-container img {
      width: 100%;
      height: 100%;
    }

    .pix-amount {
      font-size: 2.2rem;
      font-weight: 800;
      color: var(--brand);
      margin: 5px 0;
    }

    .client-name {
      color: #fff;
      font-weight: 600;
      font-size: 1.2rem;
      margin-top: 10px;
    }

    .spinner {
      width: 30px;
      height: 30px;
      border: 4px solid rgba(255, 255, 255, 0.1);
      border-left-color: var(--brand);
      border-radius: 50%;
      animation: spin 1s linear infinite;
      margin: 0 auto;
    }

    @keyframes spin {
      100% {
        transform: rotate(360deg);
      }
    }
  </style>
</head>

<body>

  <div class="scoreboard"><span class="material-icons">trending_up</span>Hoje: R$ <span id="totalHoje">...</span></div>

  <div class="container">
    <!-- TELA 1: FORMULÁRIO -->
    <div id="step-form">
      <h2><span class="material-icons">point_of_sale</span> Nova Venda</h2>

      <div class="input-group">
        <label>Data</label>
        <input type="date" id="dataVenda">
      </div>

      <div class="input-group">
        <label>Valor (R$)</label>
        <input type="text" id="valor" class="money-input" inputmode="numeric" value="0,00" oninput="formatarMoeda(this)">
      </div>

      <div class="input-group">
        <label>Cliente</label>
        <input type="text" id="cliente" placeholder="Nome do cliente">
      </div>

      <div class="input-group">
        <label>Descrição</label>
        <input type="text" id="descricao" placeholder="Ex: Produto / Serviço">
      </div>

      <button class="btn-primary" id="btnGerar" onclick="gerarPix()">Gerar Pix <span class="material-icons">arrow_forward</span></button>

      <div id="loader">
        <div class="spinner"></div>
        <p>Registrando...</p>
      </div>
    </div>

    <!-- TELA 2: RESUMO DO PIX (QR CODE) -->
    <div id="step-pix">
      <span class="material-icons" style="color:#00e676; font-size: 50px;">check_circle</span>
      <div class="client-name" id="displayCliente"></div>
      <div class="pix-amount">R$ <span id="displayValor"></span></div>

      <div class="qr-container">
        <img id="qrImage" src="" alt="QR Code">
      </div>

      <textarea id="pixCodeText" style="position:absolute; left:-9999px;"></textarea>

      <button class="btn-primary" onclick="copiarCodigo()"><span class="material-icons">content_copy</span> Copiar Pix (Copia e Cola)</button>
      <button class="btn-outline" onclick="novaVenda()"><span class="material-icons">add</span> Nova Venda</button>
    </div>
  </div>

  <script>
    // Inicia a página configurando data e placar
    document.addEventListener('DOMContentLoaded', () => {
      resetDataField();
      atualizarPlacar();
    });

    function resetDataField() {
      const hoje = new Date();
      const dataFmt = hoje.getFullYear() + '-' + String(hoje.getMonth() + 1).padStart(2, '0') + '-' + String(hoje.getDate()).padStart(2, '0');
      document.getElementById('dataVenda').value = dataFmt;
    }

    function atualizarPlacar() {
      google.script.run.withSuccessHandler(v => { document.getElementById('totalHoje').innerText = v; }).getTotalVendasHoje();
    }

    // Máscara de Moeda (Estilo Caixa Eletrônico)
    function formatarMoeda(e) {
      let v = e.value.replace(/\D/g, "");
      if (v === "" || v === "0") { e.value = "0,00"; return; }
      v = (v / 100).toFixed(2).replace(".", ",");
      v = v.replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1.");
      e.value = v;
    }

    function gerarPix() {
      const d = {
        data: document.getElementById('dataVenda').value,
        valor: document.getElementById('valor').value,
        cliente: document.getElementById('cliente').value,
        descricao: document.getElementById('descricao').value
      };

      if(!d.cliente || d.valor === "0,00") {
        alert("Preencha o Cliente e o Valor!");
        return;
      }

      document.getElementById('btnGerar').style.display = 'none';
      document.getElementById('loader').style.display = 'block';

      google.script.run.withSuccessHandler(mostrarPix).registrarVendaEGerarPix(d);
    }

    function mostrarPix(r) {
      // Preenche os dados do resumo
      document.getElementById('displayValor').innerText = r.valor;
      document.getElementById('displayCliente').innerText = r.cliente;
      document.getElementById('pixCodeText').value = r.pixCode;
      
      // Atualiza o placar de vendas lá em cima
      document.getElementById('totalHoje').innerText = r.novoTotal;
      
      // Gera o QR Code
      document.getElementById('qrImage').src = "https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=" + encodeURIComponent(r.pixCode);
      
      // Troca as telas: Esconde form, mostra Pix
      document.getElementById('step-form').style.display = 'none';
      document.getElementById('loader').style.display = 'none';
      document.getElementById('step-pix').style.display = 'block';
    }

    function copiarCodigo() {
      const c = document.getElementById("pixCodeText");
      c.select(); c.setSelectionRange(0, 99999);
      document.execCommand("copy");
      alert("Pix Copia e Cola copiado!");
    }

    function novaVenda() {
      // Limpa os campos
      document.getElementById('valor').value = '0,00';
      document.getElementById('cliente').value = '';
      document.getElementById('descricao').value = '';
      
      // Reseta a interface
      document.getElementById('btnGerar').style.display = 'flex';
      document.getElementById('step-pix').style.display = 'none';
      document.getElementById('step-form').style.display = 'block';
      
      resetDataField();
    }
  </script>
</body>

</html>

Acompanhe nas redes sociais

Projetos Personalizados

Precisa de uma solução sob medida?

© 2025 Transformando Planilhas. Todos os direitos reservados.