Tutorial – Crie um Menu Profissional para suas Planilhas Google
Cansado de se perder no meio de dezenas de abas lá embaixo na sua planilha? Navegar por abas padrão é um processo lento, poluído e que deixa o seu trabalho com cara de amador. Neste tutorial completo, vamos construir um sistema que transforma essa confusão visual em uma Interface de Software Profissional.
Neste projeto, desenvolveremos um Gestor de Estoque equipado com um menu superior fixo e ícones modernos. Você aprenderá como usar o Google Apps Script para criar uma navegação dinâmica, onde o conteúdo da tela muda instantaneamente ao clique de um botão, sem precisar recarregar a página.
Interface Web Moderna: Como criar uma barra de navegação azul estilo Google usando HTML e CSS.
Navegação Dinâmica: O segredo da função JavaScript para alternar entre as abas da planilha com um clique.
Automação com Apps Script: Como usar a função getSheetData para ler qualquer aba de forma genérica e inteligente.
// ==========================================================================
// ARQUIVO: Código.gs
// ==========================================================================
const PROJECT_TITLE = "Gestor de Estoque";
function doGet(e) {
const tpl = HtmlService.createTemplateFromFile('Index');
return tpl.evaluate()
.setTitle(PROJECT_TITLE)
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
/**
* Função genérica para buscar dados de qualquer aba.
* Usa getDisplayValues() para trazer os dados já formatados (R$, Datas) como texto.
*/
function getSheetData(sheetName) {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(sheetName);
if (!sheet) return { error: `Aba "${sheetName}" não encontrada.` };
const lastRow = sheet.getLastRow();
const lastCol = sheet.getLastColumn();
if (lastRow<1||lastCol<1) return { headers:[], data:[] }; // Aba vazia
// Pega cabeçalhos e dados
const headers = sheet.getRange(1, 1, 1, lastCol).getValues()[0];
// Se tiver dados além do cabeçalho
let data =[];
if (lastRow>1) {
data = sheet.getRange(2, 1, lastRow - 1, lastCol).getDisplayValues();
}
return { headers: headers, data: data, sheetName: sheetName };
} catch (e) {
return { error: e.message };
}
}
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<!-- Fonte Google (Roboto) -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Ícones Material Design -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
:root {
--primary-color: #1a73e8; /* Azul Google */
--header-height: 60px;
--bg-color: #f8f9fa;
--card-bg: #ffffff;
--text-color: #202124;
--border-color: #dadce0;
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
padding-top: var(--header-height); /* Espaço para o menu fixo */
}
/* --- MENU SUPERIOR FIXO --- */
.navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: var(--header-height);
background-color: var(--primary-color);
color: white;
display: flex;
align-items: center;
padding: 0 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
z-index: 1000;
}
.nav-title {
font-size: 1.2rem;
font-weight: 500;
margin-right: 40px;
display: flex;
align-items: center;
gap: 10px;
}
.nav-links {
display: flex;
gap: 5px;
overflow-x: auto; /* Permite rolar no mobile */
}
.nav-btn {
background: none;
border: none;
color: rgba(255,255,255,0.7);
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
transition: all 0.2s;
white-space: nowrap;
}
.nav-btn:hover { color: white; background-color: rgba(255,255,255,0.1); }
.nav-btn.active { color: var(--primary-color); background-color: white; }
/* --- ÁREA DE CONTEÚDO --- */
.container {
max-width: 1200px;
margin: 20px auto;
padding: 0 15px;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
h2 { margin: 0; font-weight: 400; color: #5f6368; }
/* --- TABELA ESTILIZADA --- */
.table-responsive {
overflow-x: auto;
background: var(--card-bg);
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
border: 1px solid var(--border-color);
}
table {
width: 100%;
border-collapse: collapse;
min-width: 600px; /* Garante largura mínima */
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background-color: #f1f3f4;
font-weight: 600;
color: #5f6368;
position: sticky;
top: 0;
}
tr:hover { background-color: #f8f9fa; }
tr:last-child td { border-bottom: none; }
/* --- LOADER --- */
#loader {
display: none;
justify-content: center;
padding: 40px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid var(--primary-color);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<!-- Menu Superior -->
<nav class="navbar">
<div class="nav-title">
<span class="material-icons">inventory_2</span> Estoque
</div>
<div class="nav-links">
<button class="nav-btn active" onclick="loadTab('Produtos', this)">Produtos</button>
<button class="nav-btn" onclick="loadTab('Estoque', this)">Estoque</button>
<button class="nav-btn" onclick="loadTab('Vendas', this)">Vendas</button>
<button class="nav-btn" onclick="loadTab('Histórico', this)">Histórico</button>
</div>
</nav>
<!-- Área Principal -->
<div class="container">
<div class="content-header">
<h2 id="pageTitle">Produtos</h2>
<button onclick="refreshData()" style="border:none; background:none; cursor:pointer; color: #1a73e8;">
<span class="material-icons">refresh</span>
</button>
</div>
<!-- Loading -->
<div id="loader"><div class="spinner"></div></div>
<!-- Tabela Dinâmica -->
<div id="tableContainer" class="table-responsive">
<!-- Tabela inserida aqui via JS -->
</div>
</div>
<script>
let currentTab = 'Produtos';
// Ao carregar a página
document.addEventListener('DOMContentLoaded', () => {
loadTab('Produtos', document.querySelector('.nav-btn.active'));
});
function loadTab(sheetName, btnElement) {
currentTab = sheetName;
// Atualiza visual do menu
if (btnElement) {
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
btnElement.classList.add('active');
}
// Atualiza título e mostra loader
document.getElementById('pageTitle').textContent = sheetName;
document.getElementById('tableContainer').innerHTML = '';
document.getElementById('loader').style.display = 'flex';
// Chama o Backend
google.script.run
.withSuccessHandler(renderTable)
.withFailureHandler(showError)
.getSheetData(sheetName);
}
function refreshData() {
// Simula um clique no botão ativo atual para recarregar
const activeBtn = document.querySelector('.nav-btn.active');
loadTab(currentTab, activeBtn);
}
function renderTable(response) {
document.getElementById('loader').style.display = 'none';
const container = document.getElementById('tableContainer');
if (response.error) {
container.innerHTML = `<p style="padding:20px; color:red;">${response.error}</p>`;
return;
}
if (!response.data || response.data.length === 0) {
container.innerHTML = `<p style="padding:20px; text-align:center; color:#888;">Nenhum registro encontrado em ${response.sheetName}.</p>`;
return;
}
// Monta a Tabela HTML
let html = '<table><thead><tr>';
// Cabeçalhos
response.headers.forEach(h => {
html += `<th>${h}</th>`;
});
html += '</tr></thead><tbody>';
// Dados
response.data.forEach(row => {
html += '<tr>';
row.forEach(cell => {
html += `<td>${cell}</td>`;
});
html += '</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
}
function showError(err) {
document.getElementById('loader').style.display = 'none';
document.getElementById('tableContainer').innerHTML = `<p style="color:red; padding:20px;">Erro de conexão: ${err.message}</p>`;
}
</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.