Tutorial – Crie um Gerador de Propostas em PDF com Google Planilhas
Cansado do trabalho manual de copiar e colar dados da sua planilha para criar propostas, relatórios ou recibos? Neste tutorial completo, vamos construir um sistema que transforma sua Planilha Google em um poderoso gerador de documentos PDF.
Você aprenderá como criar um painel de controle que, com um único clique, seleciona os dados de uma linha, os insere em um layout profissional e gera um arquivo PDF perfeito, nomeado e salvo automaticamente no seu Google Drive.
// ==========================================================================
// CONFIGURAÇÕES GLOBAIS
// ==========================================================================
// Nome da aba onde os dados das propostas estão.
const NOME_DA_ABA = "Propostas";
// ==========================================================================
// FUNÇÕES PRINCIPAIS DO APP WEB
// ==========================================================================
/**
* Função principal que serve a página HTML do nosso aplicativo.
*/
function doGet(e) {
return HtmlService.createTemplateFromFile('Index')
.evaluate()
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
.setTitle('Gerador de Propostas');
}
/**
* Inclui o conteúdo de um arquivo no template HTML. Usado para CSS.
*/
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
// ==========================================================================
// FUNÇÕES EXPOSTAS PARA O FRONT-END (JAVASCRIPT)
// ==========================================================================
/**
* Lê todas as propostas da planilha e as retorna para a interface web.
* @returns {Array<Object>} Um array de objetos, onde cada objeto é uma proposta.
*/
function getPropostas() {
try {
const sheet = getSheet();
const range = sheet.getDataRange();
const values = range.getValues();
if (values.length <= 1) {
return []; // Retorna vazio se só houver o cabeçalho
}
const headers = values.shift(); // Remove a primeira linha (cabeçalhos)
// Mapeia os dados para objetos, tratando o campo de Data
const propostas = values.map(row => {
let proposta = {};
headers.forEach((header, index) => {
const cellValue = row[index];
// Se a coluna for 'Data' e o valor for um objeto Date, converte para string
if (header === 'Data' && cellValue instanceof Date) {
proposta[header] = cellValue.toISOString().split('T')[0]; // Formato AAAA-MM-DD
} else {
proposta[header] = cellValue;
}
});
proposta['ID'] = String(proposta['ID']); // Garante que o ID seja uma string
return proposta;
});
return propostas;
} catch (e) {
Logger.log(`Erro em getPropostas: ${e.message}`);
// Lança um erro que será capturado pelo withFailureHandler no front-end
throw new Error(`Falha ao ler propostas: ${e.message}`);
}
}
/**
* Gera um PDF para uma proposta específica com base no seu ID.
* @param {string} idDaProposta O ID da proposta a ser usada.
* @returns {Object} Um objeto com a URL do PDF gerado ou uma mensagem de erro.
*/
function gerarPdfProposta(idDaProposta) {
try {
const sheet = getSheet();
const propostaData = findRowById(sheet, idDaProposta);
if (!propostaData) {
throw new Error(`Proposta com ID "${idDaProposta}" não encontrada.`);
}
let dataFormatada = 'Data não informada';
if (propostaData.Data && typeof propostaData.Data.toLocaleDateString === 'function') { // Garante que é um objeto de data
const meses = ["janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"];
// O Apps Script já lê a data da planilha como um objeto Date. Não precisamos converter de string.
const dataObj = propostaData.Data;
const dia = dataObj.getDate();
const mes = meses[dataObj.getMonth()]; // getMonth() retorna 0-11
const ano = dataObj.getFullYear();
dataFormatada = `${dia} de ${mes} de ${ano}`;
}
// 1. Monta o conteúdo HTML do PDF
const htmlContent = `
<html>
<head>
<style>
body { font-family: 'Helvetica', 'Arial', sans-serif; font-size: 12pt; }
h1 { color: #444; border-bottom: 2px solid #27ae60; padding-bottom: 10px; }
.header { display: -webkit-flex; justify-content: space-between; margin-top: 20px; }
.section { margin-top: 20px; }
.label { font-weight: bold; color: #555; }
.value { margin-left: 10px; }
.valor { font-size: 1.5em; font-weight: bold; color: #27ae60; margin-top: 25px; }
</style>
</head>
<body>
<h1>Proposta de Projeto</h1>
<div class="header">
<div>
<span class="label">Cliente:</span>
<span class="value">${propostaData.Cliente}</span>
</div>
<div>
<span class="label">Data:</span>
<span class="value">${dataFormatada}</span>
</div>
</div>
<div class="section">
<span class="label">Projeto:</span>
<span class="value">${propostaData.Projeto}</span>
</div>
<div class="section">
<span class="label">Descrição:</span>
<p>${propostaData.Descrição}</p>
</div>
<div class="valor">
<span class="label">Valor:</span>
<span>R$ ${parseFloat(propostaData.Valor).toFixed(2)}</span>
</div>
</body>
</html>
`;
// 2. Cria o arquivo PDF
const blob = Utilities.newBlob(htmlContent, MimeType.HTML).getAs(MimeType.PDF);
const nomeDoArquivo = `Proposta - ${propostaData.Cliente} - ${propostaData.Projeto}.pdf`;
// 3. Salva o PDF na mesma pasta da planilha (ou na raiz, como fallback)
const planilhaArquivo = DriveApp.getFileById(SpreadsheetApp.getActiveSpreadsheet().getId());
const parents = planilhaArquivo.getParents();
let pdfFile;
if (parents.hasNext()) {
const folder = parents.next();
pdfFile = folder.createFile(blob).setName(nomeDoArquivo);
} else {
pdfFile = DriveApp.createFile(blob).setName(nomeDoArquivo); // Fallback para a pasta raiz
}
Logger.log(`PDF gerado com sucesso: ${pdfFile.getName()}, URL: ${pdfFile.getUrl()}`);
// 4. Retorna a URL para o front-end
return { success: true, url: pdfFile.getUrl() };
} catch (e) {
Logger.log(`Erro em gerarPdfProposta: ${e.message}`);
return { success: false, message: e.message };
}
}
// ==========================================================================
// FUNÇÕES AUXILIARES INTERNAS
// ==========================================================================
/**
* Função auxiliar para obter a aba da planilha.
* Lança um erro se a aba não forem encontradas.
*/
function getSheet() {
// Esta linha pega a planilha ATIVA, onde o script está rodando, independentemente do nome.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(NOME_DA_ABA);
if (!sheet) {
// A mensagem de erro agora é mais inteligente, pois usa o nome real do arquivo.
throw new Error(`Aba "${NOME_DA_ABA}" não encontrada na planilha "${ss.getName()}".`);
}
return sheet;
}
/**
* Encontra uma linha na planilha pelo ID e retorna seus dados como um objeto.
* @param {GoogleAppsScript.Spreadsheet.Sheet} sheet A aba onde procurar.
* @param {string} id O ID a ser encontrado.
* @returns {Object|null} Um objeto com os dados da linha ou null se não encontrar.
*/
function findRowById(sheet, id) {
const range = sheet.getDataRange();
const values = range.getValues();
const headers = values.shift();
const idIndex = headers.indexOf('ID');
if (idIndex === -1) {
throw new Error('Coluna "ID" não encontrada na planilha.');
}
for (const row of values) {
if (String(row[idIndex]) === String(id)) {
let rowData = {};
headers.forEach((header, index) => {
rowData[header] = row[index];
});
return rowData;
}
}
return null; // Retorna null se não encontrar
}
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background-color: #ffffff;
padding: 25px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.05);
}
h1 {
color: #343a40;
text-align: center;
border-bottom: 1px solid #dee2e6;
padding-bottom: 15px;
margin-top: 0;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
th {
background-color: #f8f9fa;
font-weight: 600;
color: #495057;
}
tr:hover {
background-color: #f1f3f5;
}
.btn {
padding: 8px 12px;
font-size: 14px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #007bff;
color: white;
transition: background-color 0.2s;
}
.btn:hover {
background-color: #0056b3;
}
.btn:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
#loader, #feedback {
text-align: center;
padding: 20px;
font-size: 1.1em;
color: #6c757d;
}
.feedback.error {
color: #dc3545;
}
.feedback.success {
color: #28a745;
}
/* Estilos para responsividade em telas menores */
@media (max-width: 768px) {
body {
padding: 10px;
}
.container {
padding: 15px;
}
table, thead, tbody, th, td, tr {
display: block;
}
thead tr {
position: absolute;
top: -9999px;
left: -9999px;
}
tr {
border: 1px solid #ccc;
border-radius: 5px;
margin-bottom: 15px;
}
td {
border: none;
border-bottom: 1px solid #eee;
position: relative;
padding-left: 50%;
}
td:before {
position: absolute;
left: 6px;
width: 45%;
padding-right: 10px;
white-space: nowrap;
font-weight: bold;
content: attr(data-label);
}
td:last-child {
border-bottom: 0;
}
}
</style>
</head>
<body>
<div class="container">
<h1>Gerador de Propostas</h1>
<div id="table-container">
<div id="loader">Carregando propostas...</div>
<table id="propostasTable" style="display:none;">
<thead>
<tr>
<th>Data</th>
<th>Cliente</th>
<th>Projeto</th>
<th style="text-align:right;">Valor</th>
<th>Ação</th>
</tr>
</thead>
<tbody id="propostasTbody">
<!-- As linhas da tabela serão inseridas aqui pelo JavaScript -->
</tbody>
</table>
</div>
<div id="feedback"></div>
</div>
<script>
const loader = document.getElementById('loader');
const feedback = document.getElementById('feedback');
const propostasTable = document.getElementById('propostasTable');
const propostasTbody = document.getElementById('propostasTbody');
// Função para mostrar feedback ao usuário
function showFeedback(message, type = 'info') {
feedback.textContent = message;
feedback.className = `feedback ${type}`;
}
// Função para gerar uma linha da tabela
function createTableRow(proposta) {
const tr = document.createElement('tr');
// Formata a data para exibição no formato DD/MM/AAAA
const dataFormatada = proposta.Data
? new Date(proposta.Data).toLocaleDateString('pt-BR', {timeZone: 'UTC'})
: 'N/A';
tr.innerHTML = `
<td data-label="Data">${dataFormatada}</td>
<td data-label="Cliente">${proposta.Cliente}</td>
<td data-label="Projeto">${proposta.Projeto}</td>
<td data-label="Valor" style="text-align:right;">${(proposta.Valor || 0).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}</td>
<td data-label="Ação">
<button class="btn" data-id="${proposta.ID}">Gerar PDF</button>
</td>
`;
return tr;
}
// Função para carregar e exibir as propostas
function carregarPropostas() {
loader.style.display = 'block';
propostasTable.style.display = 'none';
propostasTbody.innerHTML = '';
showFeedback('');
google.script.run
.withSuccessHandler(function(propostas) {
loader.style.display = 'none';
if (propostas && propostas.length > 0) {
propostas.forEach(p => propostasTbody.appendChild(createTableRow(p)));
propostasTable.style.display = 'table';
} else {
showFeedback('Nenhuma proposta encontrada na planilha.');
}
})
.withFailureHandler(function(error) {
loader.style.display = 'none';
showFeedback(error.message, 'error');
})
.getPropostas();
}
// Listener de clique para os botões "Gerar PDF"
propostasTbody.addEventListener('click', function(e) {
const button = e.target.closest('button[data-id]');
if (!button) return;
const propostaId = button.dataset.id;
button.textContent = 'Gerando...';
button.disabled = true;
showFeedback('');
google.script.run
.withSuccessHandler(function(response) {
if (response.success) {
showFeedback('PDF gerado com sucesso! Abrindo em nova aba...', 'success');
window.open(response.url, '_blank');
} else {
showFeedback(response.message, 'error');
}
button.textContent = 'Gerar PDF';
button.disabled = false;
})
.withFailureHandler(function(error) {
showFeedback(error.message, 'error');
button.textContent = 'Gerar PDF';
button.disabled = false;
})
.gerarPdfProposta(propostaId);
});
// Carrega as propostas quando a página é aberta
window.addEventListener('load', carregarPropostas);
</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.