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:
/**
* 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");
}
<!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> Precisa de uma solução sob medida?
Nós usamos cookies para analisar o tráfego do site e melhorar sua experiência. Ao clicar em 'Aceitar', você concorda com o uso de ferramentas de estatística, conforme detalhado em nossa Política de Privacidade.