Tutorial Apps Script

Como Criar e Personalizar um Menu Lateral de Software no Google Planilhas

Se você acompanhou o nosso tutorial clássico de “Menu”, sabe como esconder as abas originais do Google Planilhas eleva o profissionalismo da planilha. Mas hoje, nós vamos levar essa experiência para o próximo nível.

Neste tutorial completo, vamos construir e personalizar um Menu Lateral de Software. Você vai aprender a estruturar um menu dinâmico controlado pelo back-end (Apps Script) e integrado a uma interface web elegante de única página (SPA). Além disso, mostramos como posicionar os botões de ação e cadastrar dados diretamente na planilha sem tocar na grade de células.

O que você vai aprender:

  • Automação de Banco de Dados (setupSheet): Esqueça a configuração manual de abas. O código cria e formata tudo o que você precisa ao ser iniciado.
  • Menus Dinâmicos no Apps Script: Aprenda a alterar textos, ícones e ações diretamente no arquivo Código.gs de forma centralizada.
  • Estilização com CSS Flexbox: O segredo de design para alinhar botões e títulos na mesma linha de forma organizada.
  • Navegação SPA Fluida: Alternância instantânea de telas sem a necessidade de recarregar a página do navegador.

// ==========================================================================
// CONSTANTES GLOBAIS E CONFIGURAÇÃO DE ABAS
// ==========================================================================
const HTML_INDEX = 'Index';
const PROJECT_TITLE = 'Sistema de Gestão';

const SHEET_PRODUTOS = 'Produtos';
const SHEET_ESTOQUE = 'Estoque';
const SHEET_VENDAS = 'Vendas';
const SHEET_HISTORICO = 'Histórico';

// Mapeamento idêntico às colunas físicas da sua planilha
const HEADERS_PRODUTOS = ["ID", "Nome_do_Produto", "Categoria", "Preco_Unit", "Descricao"];
const HEADERS_ESTOQUE = ["ID", "Produto", "Qtd_Atual", "Qtd_Minima", "Status"];
const HEADERS_VENDAS = ["ID", "Data", "Cliente", "Produto", "Qtd", "Total", "Vendedor"];
const HEADERS_HISTORICO = ["Data_Hora", "Usuario", "Acao", "Detalhes"];

function doGet(e) {
  try {
    setupSheet(SHEET_PRODUTOS, HEADERS_PRODUTOS);
    setupSheet(SHEET_ESTOQUE, HEADERS_ESTOQUE);
    setupSheet(SHEET_VENDAS, HEADERS_VENDAS);
    setupSheet(SHEET_HISTORICO, HEADERS_HISTORICO);
   
    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)
              .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
  } catch (err) {
    Logger.log(`ERRO no doGet: ${err.message}`);
    return HtmlService.createHtmlOutput(`<h1>Erro ao Carregar o Sistema</h1><p>${err.message}</p>`);
  }
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

// ==========================================================================
// 🍔 MENU LATERAL (ALTERAR ITENS E ÍCONES AQUI)
// ==========================================================================
function getMenuItems() {
  return [
    { id: 'produtos', text: 'Produtos', icon: 'shopping_bag' },
    { id: 'estoque', text: 'Estoque', icon: 'inventory_2' },
    { id: 'vendas', text: 'Vendas', icon: 'point_of_sale' },
    { id: 'historico', text: 'Histórico', icon: 'history' }
  ];
}

// ==========================================================================
// CARREGAMENTO INICIAL DE DADOS
// ==========================================================================
function getInitialAppData() {
  try {
    return {
      menuItems: getMenuItems(),
      produtos: getProdutos(),
      estoque: getEstoque(),
      vendas: getVendas(),
      historico: getHistorico()
    };
  } catch (e) {
    return { error: `Erro geral ao carregar dados: ${e.message}` };
  }
}

// ==========================================================================
// LEITURA DE DADOS (READ)
// ==========================================================================
function getProdutos() {
  return readSheetData(SHEET_PRODUTOS, HEADERS_PRODUTOS);
}

function getEstoque() {
  return readSheetData(SHEET_ESTOQUE, HEADERS_ESTOQUE);
}

function getVendas() {
  return readSheetData(SHEET_VENDAS, HEADERS_VENDAS);
}

function getHistorico() {
  return readSheetData(SHEET_HISTORICO, HEADERS_HISTORICO).sort((a,b) => new Date(b.Data_Hora) - new Date(a.Data_Hora));
}

// ==========================================================================
// GRAVAÇÃO DE DADOS (CREATE)
// ==========================================================================
function addProduto(produtoData) {
  try {
    const sheet = setupSheet(SHEET_PRODUTOS, HEADERS_PRODUTOS);
    const newProdId = "PRD_" + new Date().getTime();
    const preco = parseFloat(produtoData.Preco_Unit) || 0;

    // 1. Grava na planilha de Produtos
    const newRow = [
      newProdId,
      produtoData.Nome_do_Produto,
      produtoData.Categoria,
      preco,
      produtoData.Descricao
    ];
    sheet.appendRow(newRow);

    // Formata preço de forma robusta
    const colPreco = HEADERS_PRODUTOS.indexOf("Preco_Unit") + 1;
    sheet.getRange(sheet.getLastRow(), colPreco).setNumberFormat("R$ #,##0.00");

    // 2. Grava o Estoque Inicial na planilha de Estoque de forma direta
    const sheetEstoque = setupSheet(SHEET_ESTOQUE, HEADERS_ESTOQUE);
    const newEstId = "EST_" + new Date().getTime();
    const estRow = [newEstId, produtoData.Nome_do_Produto, 10, 5, "Ok"]; 
    sheetEstoque.appendRow(estRow);

    // 3. Registra no histórico
    logActivity("Cadastro", `Produto criado: ${produtoData.Nome_do_Produto} (ID: ${newProdId})`);
    return { success: true };
  } catch (e) {
    return { error: e.message };
  }
}

// ==========================================================================
// COMPONENTES DE SUPORTE E LEITURA SEGURA
// ==========================================================================
function logActivity(acao, detalhes) {
  try {
    const sheet = setupSheet(SHEET_HISTORICO, HEADERS_HISTORICO);
    const usuario = Session.getActiveUser().getEmail() || "tutorial@sistema.com";
    const dataHora = new Date();
    sheet.appendRow([dataHora, usuario, acao, detalhes]);
  } catch(e) {
    Logger.log("Falha ao registrar histórico: " + e.message);
  }
}

function readSheetData(sheetName, headers) {
  const sheet = setupSheet(sheetName, headers);
  const lastRow = sheet.getLastRow();
  if (lastRow <= 1) return [];

  return sheet.getRange(2, 1, lastRow - 1, headers.length).getValues().map(row => {
    let obj = {};
    headers.forEach((header, index) => {
      const val = row[index];
      // Converte objetos Date para string ISO para evitar erro na serialização nativa
      if (val instanceof Date) {
        obj[header] = val.toISOString();
      } else {
        obj[header] = val;
      }
    });
    return obj;
  });
}

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);
    
    const maxRows = sheet.getMaxRows();
    if (maxRows > 1) {
      if (headers.indexOf("Data") > -1) {
        sheet.getRange(2, headers.indexOf("Data") + 1, maxRows - 1).setNumberFormat("yyyy-mm-dd");
      }
      if (headers.indexOf("Data_Hora") > -1) {
        sheet.getRange(2, headers.indexOf("Data_Hora") + 1, maxRows - 1).setNumberFormat("yyyy-mm-dd hh:mm:ss");
      }
    }
    
    // Inserção automática de dados fictícios para fins de teste inicial do layout
    if (sheetName === SHEET_PRODUTOS) {
      sheet.appendRow(["PRD_DEMO1", "Chocolate em Barra 100g", "Doces", 8.50, "Chocolate amargo 70%"]);
      sheet.getRange(2, headers.indexOf("Preco_Unit") + 1).setNumberFormat("R$ #,##0.00");
    } else if (sheetName === SHEET_ESTOQUE) {
      sheet.appendRow(["EST_DEMO1", "Chocolate em Barra 100g", 12, 5, "Ok"]);
    } else if (sheetName === SHEET_VENDAS) {
      sheet.appendRow(["VEN_DEMO1", new Date(), "Carlos Souza", "Chocolate em Barra 100g", 2, 17.00, "Vendedor Demo"]);
      sheet.getRange(2, headers.indexOf("Total") + 1).setNumberFormat("R$ #,##0.00");
    } else if (sheetName === SHEET_HISTORICO) {
      sheet.appendRow([new Date(), "sistema@tutorial.com", "Sistema", "Banco de dados inicializado com sucesso."]);
    }
  }
  return sheet;
}
<!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/icon?family=Material+Icons" rel="stylesheet">

  <style>
    /* ==========================================================================
       🎨 CORES DO LAYOUT - ALTERE AS CORES DIRETAMENTE NESTA SEÇÃO
       ========================================================================== */
    :root {
      --primary-color: #2c3e50;         /* Cor Principal (Topo e Títulos) */
      --primary-dark: #1a252f;          /* Tom escuro (Menu Lateral) */
      --secondary-color: #3498db;       /* Cor de destaque (Botão de Cadastro) */
      --light-gray: #f8f9fa;            /* Cor de fundo das listagens */
      --medium-gray: #bdc3c7;           /* Cor dos divisores e bordas */
      --dark-gray: #2c3e50;             /* Cor das fontes */
      --body-bg: #eceff1;               /* Cor de fundo geral da página */
      --card-bg: #ffffff;               /* Cor do painel das tabelas e modais */
      --sidebar-width: 240px;
    }

    /* --- RESET E ESTILOS GERAIS --- */
    *, *::before, *::after { box-sizing: border-box; }
    body, html { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: var(--body-bg); color: var(--dark-gray); height: 100%; overflow: hidden; }

    /* --- CONTAINER PRINCIPAL --- */
    .app-container { display: flex; height: 100vh; padding-top: 60px; }
    
    /* Header Superior */
    .app-header { display: flex; align-items: center; position: fixed; top: 0; left: 0; width: 100%; height: 60px; padding: 0 20px; background-color: var(--primary-color); color: white; box-shadow: 0 2px 5px rgba(0,0,0,0.15); z-index: 1000; }
    .app-header h1 { margin: 0; font-size: 1.25rem; font-weight: 500; }

    /* Menu Lateral Fixo */
    .sidebar { position: fixed; top: 60px; left: 0; width: var(--sidebar-width); height: calc(100vh - 60px); background-color: var(--primary-dark); color: white; z-index: 1050; }
    .sidebar-menu { list-style: none; padding: 20px 0; margin: 0; }
    .sidebar-menu li a { display: flex; align-items: center; padding: 15px 20px; color: #ecf0f1; text-decoration: none; transition: background-color 0.2s ease; border-left: 4px solid transparent; }
    .sidebar-menu li a:hover { background-color: rgba(255,255,255,0.05); }
    .sidebar-menu li a.active { background-color: rgba(255,255,255,0.1); border-left-color: var(--secondary-color); font-weight: bold; }
    .sidebar-menu li a .material-icons { margin-right: 15px; }

    /* Área Útil de Conteúdo */
    .main-content { flex-grow: 1; margin-left: var(--sidebar-width); width: calc(100% - var(--sidebar-width)); height: 100%; overflow: hidden; }

    /* Seções (Telas) */
    .content-section { display: none; flex-direction: column; height: 100%; padding: 25px; overflow-y: auto; }
    .content-section.active { display: flex; }

    /* ==========================================================================
       👑 CABEÇALHO UNIFICADO - COLOCA O TÍTULO E O BOTÃO NA MESMA LINHA
       ========================================================================== */
    .section-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding-bottom: 15px;
      border-bottom: 2px solid var(--medium-gray);
      margin-bottom: 25px;
    }
    .section-header h2 {
      display: flex;
      align-items: center;
      gap: 10px;
      margin: 0;
      font-size: 1.6rem;
      color: var(--primary-color);
    }

    /* Status Badges */
    .badge { padding: 4px 10px; border-radius: 12px; font-size: 0.85rem; font-weight: bold; text-align: center; display: inline-block; }
    .badge-ok { background-color: rgba(46, 204, 113, 0.2); color: #27ae60; }
    .badge-baixo { background-color: rgba(241, 196, 15, 0.2); color: #d68910; }
    .badge-vazio { background-color: rgba(231, 76, 60, 0.2); color: #c0392b; }

    /* --- ESTILO DAS TABELAS --- */
    .data-table-wrapper { width: 100%; overflow-x: auto; background: var(--card-bg); border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
    .data-table { width: 100%; border-collapse: collapse; min-width: 600px; }
    .data-table th, .data-table td { padding: 14px 18px; text-align: left; border-bottom: 1px solid var(--light-gray); }
    .data-table th { background-color: #f2f4f4; font-weight: 600; color: var(--primary-color); }
    .data-table tbody tr:hover { background-color: #fdfefe; }

    /* --- BOTÕES --- */
    .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 18px; border: 1px solid transparent; border-radius: 6px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; }
    .btn-primary { background-color: var(--secondary-color); color: white; }
    .btn-primary:hover { background-color: #2980b9; }
    .btn-secondary { background-color: #95a5a6; color: white; }
    .btn-secondary:hover { background-color: #7f8c8d; }

    /* --- FORMULÁRIOS E MODAIS --- */
    .modal-backdrop { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1060; }
    .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1070; align-items: center; justify-content: center; padding: 15px; }
    .modal.open { display: flex; }
    .modal-content { background: var(--card-bg); width: 100%; max-width: 480px; border-radius: 8px; overflow: hidden; box-shadow: 0 5px 15px rgba(0,0,0,0.3); animation: slideDown 0.3s ease-out; }
    .modal-header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid var(--medium-gray); background-color: var(--light-gray); }
    .modal-header h3 { margin: 0; font-size: 1.2rem; color: var(--primary-color); }
    .close-modal-btn { background: none; border: none; font-size: 1.6rem; cursor: pointer; color: #7f8c8d; }
    .modal-body { padding: 20px; }
    .modal-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 15px 20px; border-top: 1px solid var(--light-gray); }
    
    .form-group { margin-bottom: 15px; }
    .form-group label { display: block; margin-bottom: 6px; font-weight: 500; }
    .form-group input, .form-group textarea { width: 100%; padding: 10px; border: 1px solid var(--medium-gray); border-radius: 6px; font-size: 0.95rem; }

    /* Notificação Toast */
    .toast { position: fixed; top: 20px; left: 50%; transform: translate(-50%, -150%); z-index: 2000; display: flex; align-items: center; gap: 10px; padding: 12px 24px; border-radius: 30px; box-shadow: 0 4px 10px rgba(0,0,0,0.15); color: white; opacity: 0; transition: all 0.3s ease; }
    .toast.show { opacity: 1; transform: translate(-50%, 0); }
    .toast.success { background-color: #2ecc71; }
    .toast.error { background-color: var(--danger-color); }

    /* Spinner Loader */
    #app-loader { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(44, 62, 80, 0.8); z-index: 9999; display: flex; flex-direction: column; justify-content: center; align-items: center; color: white; }
    .spinner { width: 45px; height: 45px; border: 4px solid rgba(255,255,255,0.25); border-left-color: var(--secondary-color); border-radius: 50%; animation: spin 1s linear infinite; }
    
    @keyframes spin { to { transform: rotate(360deg); } }
    @keyframes slideDown { from { transform: translateY(-30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

    @media (max-width: 768px) {
      .sidebar { display: none; }
      .main-content { margin-left: 0; width: 100%; }
    }
  </style>
</head>
<body>

  <!-- Spinner Loader -->
  <div id="app-loader">
    <div class="spinner"></div>
    <p style="margin-top: 15px; font-weight: 500;">Sincronizando planilhas...</p>
  </div>

  <!-- Toast -->
  <div id="app-toast" class="toast">
    <span class="material-icons" id="toast-icon">check_circle</span>
    <span id="toast-message">Notificação</span>
  </div>

  <!-- Container Base -->
  <div class="app-container">
    <header class="app-header">
      <h1 id="appTitle"><?= projectTitle ?></h1>
    </header>

    <!-- Sidebar Fixo -->
    <nav class="sidebar" id="sidebar">
      <ul class="sidebar-menu" id="sidebarMenu">
        <!-- Renderizado dinamicamente -->
      </ul>
    </nav>

    <!-- Área Principal de Exibição (SPA) -->
    <main class="main-content" id="mainContent">

      <!-- Aba: Produtos (Único botão de interação) -->
      <section id="content-produtos" class="content-section">
        <div class="section-header">
          <h2><span class="material-icons">shopping_bag</span> Produtos Cadastrados</h2>
          <!-- Botão na mesma linha do cabeçalho -->
          <button id="btnNovoProduto" class="btn btn-primary">
            <span class="material-icons">add</span> Novo Produto
          </button>
        </div>

        <div class="data-table-wrapper">
          <table class="data-table">
            <thead>
              <tr>
                <th>Nome do Produto</th>
                <th>Categoria</th>
                <th>Preço Unitário</th>
                <th>Descrição</th>
              </tr>
            </thead>
            <tbody id="tbodyProdutos">
              <!-- Renderizado dinamicamente -->
            </tbody>
          </table>
        </div>
      </section>

      <!-- Aba: Estoque (Somente Leitura) -->
      <section id="content-estoque" class="content-section">
        <div class="section-header">
          <h2><span class="material-icons">inventory_2</span> Controle de Estoque</h2>
        </div>
        <div class="data-table-wrapper">
          <table class="data-table">
            <thead>
              <tr>
                <th>Produto</th>
                <th>Qtd Atual</th>
                <th>Qtd Mínima</th>
                <th>Status</th>
              </tr>
            </thead>
            <tbody id="tbodyEstoque">
              <!-- Renderizado dinamicamente -->
            </tbody>
          </table>
        </div>
      </section>

      <!-- Aba: Vendas (Somente Leitura) -->
      <section id="content-vendas" class="content-section">
        <div class="section-header">
          <h2><span class="material-icons">point_of_sale</span> Registro de Vendas</h2>
        </div>
        <div class="data-table-wrapper">
          <table class="data-table">
            <thead>
              <tr>
                <th>Data</th>
                <th>Cliente</th>
                <th>Produto</th>
                <th>Qtd</th>
                <th>Total</th>
                <th>Vendedor</th>
              </tr>
            </thead>
            <tbody id="tbodyVendas">
              <!-- Renderizado dinamicamente -->
            </tbody>
          </table>
        </div>
      </section>

      <!-- Aba: Histórico (Somente Leitura) -->
      <section id="content-historico" class="content-section">
        <div class="section-header">
          <h2><span class="material-icons">history</span> Histórico de Atividades</h2>
        </div>
        <div class="data-table-wrapper">
          <table class="data-table">
            <thead>
              <tr>
                <th>Data/Hora</th>
                <th>Usuário</th>
                <th>Ação</th>
                <th>Detalhes</th>
              </tr>
            </thead>
            <tbody id="tbodyHistorico">
              <!-- Renderizado dinamicamente -->
            </tbody>
          </table>
        </div>
      </section>

    </main>
  </div>

  <div id="modalBackdrop" class="modal-backdrop"></div>

  <!-- Modal Cadastro de Produto -->
  <div id="produtoModal" class="modal">
    <div class="modal-content">
      <div class="modal-header">
        <h3>Novo Produto</h3>
        <button class="close-modal-btn" onclick="closeModal('produtoModal')">×</button>
      </div>
      <div class="modal-body">
        <form id="produtoForm">
          <div class="form-group">
            <label for="Nome_do_Produto">Nome do Produto</label>
            <input type="text" id="Nome_do_Produto" name="Nome_do_Produto" required>
          </div>
          <div class="form-group">
            <label for="Categoria">Categoria</label>
            <input type="text" id="Categoria" name="Categoria" required placeholder="Ex: Bebidas, Mercearia">
          </div>
          <div class="form-group">
            <label for="Preco_Unit">Preço Unitário (R$)</label>
            <input type="text" id="Preco_Unit" name="Preco_Unit" required placeholder="R$ 0,00">
          </div>
          <div class="form-group">
            <label for="Descricao">Descrição</label>
            <textarea id="Descricao" name="Descricao" rows="3"></textarea>
          </div>
        </form>
      </div>
      <div class="modal-footer">
        <button class="btn btn-secondary" onclick="closeModal('produtoModal')">Cancelar</button>
        <button type="submit" class="btn btn-primary" form="produtoForm">Salvar Produto</button>
      </div>
    </div>
  </div>

  <!-- ==========================================================================
       JS - LOGICA DO FRONT-END (SPA)
       ========================================================================== -->
  <script>
    let allProdutos = [];
    let allEstoque = [];
    let allVendas = [];
    let allHistorico = [];

    document.addEventListener('DOMContentLoaded', function() {
      // Formatação do campo de preço em tempo real
      document.getElementById('Preco_Unit').addEventListener('input', e => formatCurrency(e.target));

      // Lógica de envio do formulário de produto
      document.getElementById('produtoForm').addEventListener('submit', handleProdutoSubmit);

      // Gatilho do botão novo produto
      document.getElementById('btnNovoProduto').addEventListener('click', () => {
        document.getElementById('produtoForm').reset();
        openModal('produtoModal');
      });

      // Carregamento inicial do app
      initializeApp();
    });

    // ==========================================================================
    // SINCRONIZAÇÃO ASSÍNCRONA COM O GOOGLE SHEETS
    // ==========================================================================
    function initializeApp() {
      showLoader(true);
      google.script.run
        .withSuccessHandler(data => {
          showLoader(false);
          if (!data) {
            showToast("Nenhum dado retornado do servidor.", 'error');
            return;
          }
          if (data.error) {
            showToast(data.error, 'error');
            return;
          }

          // Armazena as respostas nas variáveis globais do front-end
          allProdutos = data.produtos || [];
          allEstoque = data.estoque || [];
          allVendas = data.vendas || [];
          allHistorico = data.historico || [];

          buildMenu(data.menuItems);
          
          // Mantém a última aba acessada ativa ao recarregar
          const lastSection = localStorage.getItem('activeSection') || 'produtos';
          navigateToSection(lastSection);
        })
        .withFailureHandler(err => {
          showLoader(false);
          showToast(`Erro na comunicação: ${err.message}`, 'error');
        })
        .getInitialAppData();
    }

    // ==========================================================================
    // RENDERIZADORES DE MENUS E NAVEGAÇÃO SPA
    // ==========================================================================
    function buildMenu(menuItems) {
      const menuContainer = document.getElementById('sidebarMenu');
      menuContainer.innerHTML = '';
      menuItems.forEach(item => {
        const li = document.createElement('li');
        li.innerHTML = `
          <a href="#" data-section="${item.id}" onclick="event.preventDefault(); navigateToSection('${item.id}');">
            <span class="material-icons">${item.icon}</span>
            <span>${item.text}</span>
          </a>
        `;
        menuContainer.appendChild(li);
      });
    }

    function navigateToSection(sectionId) {
      document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
      document.querySelectorAll('.sidebar-menu a').forEach(a => a.classList.remove('active'));

      const activeSection = document.getElementById(`content-${sectionId}`);
      const activeLink = document.querySelector(`a[data-section="${sectionId}"]`);

      if (activeSection) activeSection.classList.add('active');
      if (activeLink) activeLink.classList.add('active');

      localStorage.setItem('activeSection', sectionId);

      // Seleciona a tabela correspondente para redesenhar na tela
      if (sectionId === 'produtos') renderProdutos();
      if (sectionId === 'estoque') renderEstoque();
      if (sectionId === 'vendas') renderVendas();
      if (sectionId === 'historico') renderHistorico();
    }

    // ==========================================================================
    // RENDERIZADORES DE ELEMENTOS HTML (VISUAL)
    // ==========================================================================
    function renderProdutos() {
      const tbody = document.getElementById('tbodyProdutos');
      tbody.innerHTML = '';
      
      if (allProdutos.length === 0) {
        tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color:#7f8c8d;">Nenhum produto cadastrado.</td></tr>';
        return;
      }
      
      allProdutos.forEach(p => {
        const tr = document.createElement('tr');
        tr.innerHTML = `
          <td><strong>${p.Nome_do_Produto}</strong></td>
          <td>${p.Categoria}</td>
          <td>${formatBRL(p.Preco_Unit)}</td>
          <td>${p.Descricao || '---'}</td>
        `;
        tbody.appendChild(tr);
      });
    }

    function renderEstoque() {
      const tbody = document.getElementById('tbodyEstoque');
      tbody.innerHTML = '';

      if (allEstoque.length === 0) {
        tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color:#7f8c8d;">Nenhum registro de estoque encontrado.</td></tr>';
        return;
      }

      allEstoque.forEach(e => {
        let badgeClass = 'badge-ok';
        if (e.Status === 'Sem Estoque') badgeClass = 'badge-vazio';
        else if (e.Status === 'Estoque Baixo') badgeClass = 'badge-baixo';

        const tr = document.createElement('tr');
        tr.innerHTML = `
          <td><strong>${e.Produto}</strong></td>
          <td>${e.Qtd_Atual}</td>
          <td>${e.Qtd_Minima}</td>
          <td><span class="badge ${badgeClass}">${e.Status}</span></td>
        `;
        tbody.appendChild(tr);
      });
    }

    function renderVendas() {
      const tbody = document.getElementById('tbodyVendas');
      tbody.innerHTML = '';

      if (allVendas.length === 0) {
        tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color:#7f8c8d;">Nenhuma venda registrada.</td></tr>';
        return;
      }

      // Alinhamento direto com as colunas físicas da planilha
      allVendas.forEach(v => {
        const tr = document.createElement('tr');
        tr.innerHTML = `
          <td>${formatDate(v.Data)}</td>
          <td>${v.Cliente}</td>
          <td><strong>${v.Produto}</strong></td>
          <td>${v.Qtd}</td>
          <td><strong>${formatBRL(v.Total)}</strong></td>
          <td>${v.Vendedor}</td>
        `;
        tbody.appendChild(tr);
      });
    }

    function renderHistorico() {
      const tbody = document.getElementById('tbodyHistorico');
      tbody.innerHTML = '';

      if (allHistorico.length === 0) {
        tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color:#7f8c8d;">Sem histórico registrado.</td></tr>';
        return;
      }

      allHistorico.forEach(h => {
        const tr = document.createElement('tr');
        tr.innerHTML = `
          <td style="font-size:0.85rem; color:#7f8c8d;">${formatDateTime(h.Data_Hora)}</td>
          <td>${h.Usuario}</td>
          <td><strong style="color:var(--primary-color);">${h.Acao}</strong></td>
          <td style="font-size:0.9rem;">${h.Detalhes}</td>
        `;
        tbody.appendChild(tr);
      });
    }

    // ==========================================================================
    // ENVIO DE FORMULÁRIO DE PRODUTO
    // ==========================================================================
    function handleProdutoSubmit(e) {
      e.preventDefault();
      
      const data = {
        Nome_do_Produto: document.getElementById('Nome_do_Produto').value,
        Categoria: document.getElementById('Categoria').value,
        Preco_Unit: parseFloat(document.getElementById('Preco_Unit').value.replace(/[^\d,]/g, '').replace(',', '.')) || 0,
        Descricao: document.getElementById('Descricao').value
      };

      showLoader(true);
      google.script.run
        .withSuccessHandler(res => {
          showLoader(false);
          if (!res) {
            showToast("Erro de sincronização com o servidor.", 'error');
            return;
          }
          if (res.error) {
            showToast(res.error, 'error');
          } else {
            closeModal('produtoModal');
            showToast('Produto cadastrado com sucesso!', 'success');
            initializeApp(); // Recarrega os arrays do front
          }
        })
        .withFailureHandler(err => {
          showLoader(false);
          showToast(err.message, 'error');
        })
        .addProduto(data);
    }

    // ==========================================================================
    // INTERFACES E UTILITÁRIOS (UI)
    // ==========================================================================
    function openModal(id) {
      document.getElementById(id).classList.add('open');
      document.getElementById('modalBackdrop').style.display = 'block';
    }

    // Fecha os modais abertos
    function closeModal(id) {
      document.getElementById(id).classList.remove('open');
      document.getElementById('modalBackdrop').style.display = 'none';
    }

    function showLoader(show) {
      document.getElementById('app-loader').style.display = show ? 'flex' : 'none';
    }

    function showToast(message, type = 'success') {
      const toast = document.getElementById('app-toast');
      const icon = document.getElementById('toast-icon');
      const msg = document.getElementById('toast-message');

      toast.className = `toast ${type}`;
      icon.textContent = type === 'success' ? 'check_circle' : 'error';
      msg.textContent = message;

      toast.classList.add('show');
      setTimeout(() => toast.classList.remove('show'), 3000);
    }

    function formatBRL(val) {
      return (parseFloat(val) || 0).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
    }

    function formatDate(isoString) {
      if (!isoString) return '---';
      const d = new Date(isoString);
      return d.toLocaleDateString('pt-BR', { timeZone: 'UTC' });
    }

    function formatDateTime(isoString) {
      if (!isoString) return '---';
      const d = new Date(isoString);
      return d.toLocaleDateString('pt-BR') + ' ' + d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
    }

    function formatCurrency(input) {
      let value = input.value.replace(/\D/g, '');
      if (!value) {
        input.value = '';
        return;
      }
      let numberValue = parseFloat(value) / 100;
      input.value = numberValue.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
    }
  </script>
</body>
</html>

Soluções prontas para você

Escolha um dos nossos templates profissionais de alta performance e comece a otimizar sua gestão hoje mesmo, sem mensalidades.

Vamos Construir sua Ferramenta Sob Medida?

// PROJETOS PERSONALIZADOS
🔍

Entendendo o Desafio

Nosso ponto de partida é o seu objetivo final. Analisamos a fundo a sua necessidade de negócio para mapear a estrutura e as funcionalidades da ferramenta ideal, garantindo uma solução que nasce alinhada à sua estratégia.

⚙️

Criação da Interface

Transformamos a complexidade da sua planilha em um painel de controle simples de usar. O resultado é uma tela limpa, com botões e visões claras, permitindo que você gerencie seus dados sem que precise de conhecimento técnico.

Entrega da Ferramenta

Você recebe o sistema completo e funcional, implementado diretamente na sua conta Google. Entregamos uma ferramenta pronta para o uso, desbloqueando imediatamente o potencial de uma gestão visual e centralizada.

×
Solução Pronta

Gosta de praticidade?

Sabia que nós desenvolvemos sistemas prontos baseados nesses tutoriais? Dê uma olhada nos nossos templates e pule a etapa da programação!

Conhecer os Templates