Seguidores

quinta-feira, 26 de junho de 2025

Dashboard de Produtividade Pessoal T2E01: App d2p sem login e usando planilha Google e Apps Script

INTRODUÇÃO

Este é o primeiro post da temporada 2 da série Dashboard de Produtividade Pessoal, na qual vamos abrir mão do login e usar apenas planilha Google com Apps Script.



Prompt para o Gemini 2.5 Pro (Prévia)

Olá Gemini! Considerando que não estou sendo capaz de configurar o login no Google, quero mudar a arquitetura do d2p. Agora não quero mais o login. Quero usar apenas planilha Google junto com o Dashboard a partir do Apps Script. Você pode fazer isso?

Resposta do Gemini 2.5 Pro (Prévia)

Olá! Excelente ideia. Mudar para uma abordagem com Google Sheets e Apps Script é uma ótima decisão. Na verdade, para o que você quer fazer, essa é uma solução muito mais robusta, integrada e, ironicamente, mais simples de configurar do que o login manual.

Você elimina toda a complexidade da configuração do Google Cloud Console e ganha uma base de dados persistente (a planilha) de graça.

Vamos construir essa nova versão passo a passo.

A Nova Arquitetura: Dashboard com Google Sheets

O conceito será o seguinte:

  1. Google Sheets como Banco de Dados: Uma planilha guardará todas as suas tarefas, defeitos e histórico de produtividade de forma permanente.
  2. Google Apps Script como "Back-end": Um script associado à sua planilha irá ler e escrever dados nela. Ele também será o responsável por servir a página do seu dashboard.
  3. HTML/CSS/JavaScript como "Front-end": O seu dashboard (d2p.html) será servido pelo Apps Script e se comunicará com a planilha através dele, sem precisar de localStorage ou redirecionamentos.

Passo 1: Preparar a Planilha Google (Seu Banco de Dados)

  1. Crie uma nova Planilha Google: sheets.new.

  2. Renomeie o arquivo para algo como "Banco de Dados - D2P".

  3. Crie três abas (páginas) na parte inferior com os seguintes nomes exatos:

    • Tarefas
    • Defeitos
    • HistoricoProdutividade
  4. Configure os Cabeçalhos (a primeira linha) de cada aba:

    • Na aba Tarefas:

      ID | Nome | Codigo | Pontos | Status | DuracaoEfetivaSegundos | DataConclusao

    • Na aba Defeitos:

      ID | Tipo | Descricao | Timestamp

    • Na aba HistoricoProdutividade:

      Data | Valor | PontosTotais | HorasTrabalhadas

Sua planilha está pronta para receber os dados!


Passo 2: Configurar o Google Apps Script (Seu Back-end)

  1. Com a planilha aberta, vá no menu Extensões > Apps Script.
  2. Isso abrirá o editor de scripts em uma nova aba. Apague todo o código que estiver no arquivo Code.gs.
  3. Crie o arquivo do Dashboard:
    • No menu à esquerda do editor, clique no + ao lado de "Arquivos".
    • Selecione HTML.
    • Dê o nome Dashboard para o arquivo e pressione Enter. Apague o conteúdo padrão dele.

Agora você terá dois arquivos: Code.gs (para nosso back-end) e Dashboard.html (para nosso front-end).


Passo 3: Adicionar o Código ao Apps Script

Copie e cole os códigos abaixo nos arquivos correspondentes.

A. Código para Code.gs (O Back-end)

Este script lida com toda a lógica de salvar e buscar dados na planilha.

JavaScript
// ID da sua planilha. Você também pode deixar em branco se o script estiver contido na planilha.
const SPREADSHEET_ID = SpreadsheetApp.getActiveSpreadsheet().getId();
const TAREFAS_SHEET = "Tarefas";
const DEFEITOS_SHEET = "Defeitos";
const HISTORICO_SHEET = "HistoricoProdutividade";

/**
 * Função principal que é executada quando o usuário acessa a URL do web app.
 * Ela serve o arquivo HTML do dashboard.
 */
function doGet(e) {
  return HtmlService.createTemplateFromFile('Dashboard')
      .evaluate()
      .setTitle('Dashboard de Produtividade Pessoal')
      .addMetaTag('viewport', 'width=device-width, initial-scale=1.0');
}

/**
 * Inclui o conteúdo de outro arquivo (CSS ou JS) no template HTML.
 * Usado para manter o código organizado.
 */
function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

// --- FUNÇÕES DE INTERAÇÃO COM A PLANILHA ---
// Estas funções serão chamadas pelo JavaScript do seu dashboard.

/**
 * Busca todos os dados iniciais das planilhas para popular o dashboard.
 */
function getInitialData() {
  const sheetTarefas = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(TAREFAS_SHEET);
  // Pega todos os dados exceto o cabeçalho
  const tarefas = sheetTarefas.getLastRow() > 1 ? sheetTarefas.getRange(2, 1, sheetTarefas.getLastRow() - 1, 7).getValues() : [];
  
  // Mapeia os arrays para objetos para facilitar o uso no front-end
  const tarefasData = tarefas.map(t => ({
    id: t[0],
    name: t[1],
    code: t[2],
    points: t[3],
    status: t[4],
    effectiveDuration: t[5],
    completedAt: t[6]
  }));

  // (Você pode adicionar a busca de defeitos e histórico aqui da mesma forma)

  return { tasks: tarefasData };
}

/**
 * Adiciona uma nova tarefa à planilha 'Tarefas'.
 */
function addTask(taskObject) {
  try {
    const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(TAREFAS_SHEET);
    const newId = "T-" + new Date().getTime(); // Gera um ID único simples
    taskObject.id = newId;

    sheet.appendRow([
      taskObject.id,
      taskObject.name,
      taskObject.code,
      taskObject.points,
      taskObject.status,
      taskObject.effectiveDuration,
      "" // Data de Conclusão vazia inicialmente
    ]);
    return taskObject; // Retorna a tarefa com o novo ID
  } catch (e) {
    Logger.log(e);
    return null;
  }
}

/**
 * Atualiza o status e a duração de uma tarefa existente.
 */
function updateTaskStatus(taskId, status, duration) {
    try {
        const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(TAREFAS_SHEET);
        const data = sheet.getDataRange().getValues();
        
        for (let i = 1; i < data.length; i++) { // Começa em 1 para pular o cabeçalho
            if (data[i][0] == taskId) { // Coluna 0 é o ID
                sheet.getRange(i + 1, 5).setValue(status); // Coluna 5 é o Status
                sheet.getRange(i + 1, 6).setValue(duration); // Coluna 6 é a Duração
                if (status === 'completed') {
                    sheet.getRange(i + 1, 7).setValue(new Date()); // Coluna 7 é a Data de Conclusão
                }
                return { success: true, taskId: taskId };
            }
        }
        return { success: false, message: "Tarefa não encontrada." };
    } catch (e) {
        Logger.log(e);
        return { success: false, message: e.toString() };
    }
}

B. Código para Dashboard.html (O Front-end)

Este é o seu dashboard adaptado. A principal mudança está no JavaScript: em vez de manipular um state local, ele agora usa google.script.run para chamar as funções do Code.gs e interagir com a planilha.

HTML
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
    
    <style>
      /* Seus estilos CSS personalizados aqui */
      body { font-family: 'Inter', sans-serif; background-color: #f0f2f5; }
      .btn { transition: all 0.2s ease-in-out; }
      .btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
      .task-active { box-shadow: 0 0 0 3px #3b82f6; transform: scale(1.02); }
      .loader {
        border: 8px solid #f3f3f3; 
        border-top: 8px solid #3498db;
        border-radius: 50%;
        width: 60px;
        height: 60px;
        animation: spin 2s linear infinite;
        position: absolute;
        top: 50%;
        left: 50%;
        margin-top: -30px;
        margin-left: -30px;
      }
      @keyframes spin {
        0% { transform: rotate(0deg); }
        100% { transform: rotate(360deg); }
      }
    </style>
  </head>
  <body class="bg-gray-100 text-gray-800 p-4 md:p-8">

    <div id="loader" class="loader"></div>
    
    <div id="dashboard-content" class="max-w-7xl mx-auto hidden">
        <header class="mb-8">
            <h1 class="text-3xl md:text-4xl font-bold text-gray-900">Dashboard de Produtividade</h1>
            <p class="text-gray-600 mt-1">Dados salvos diretamente na sua Planilha Google.</p>
        </header>

        <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
            <div class="bg-white p-6 rounded-xl shadow-md">
                <h2 class="text-xl font-semibold mb-4">Jornada do Dia</h2>
                <div class="flex items-center justify-between mb-4">
                    <button id="btnStartWorkday" class="btn bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-lg w-full mr-2">Iniciar Jornada</button>
                    <button id="btnEndWorkday" class="btn bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg w-full ml-2" disabled>Finalizar Jornada</button>
                </div>
                <div class="text-sm text-gray-500">
                    <p>Início: <span id="workdayStartTime" class="font-medium">--:--:--</span></p>
                    <p>Duração Total: <span id="workdayDuration" class="font-medium">0h 0m 0s</span></p>
                </div>
            </div>
            <div class="bg-white p-6 rounded-xl shadow-md text-center">
                 <h2 class="text-xl font-semibold mb-2">Produtividade</h2>
                 <p class="text-4xl font-bold text-blue-600"><span id="productivityRate">0.00</span></p>
                 <p class="text-gray-500">Pontos / Hora</p>
            </div>
             <div class="bg-white p-6 rounded-xl shadow-md text-center">
                 <h2 class="text-xl font-semibold mb-2">Pontos Concluídos</h2>
                 <p class="text-4xl font-bold text-green-600"><span id="totalPointsCompleted">0</span></p>
                 <p class="text-gray-500">Total de pontos de tarefas finalizadas</p>
            </div>
        </div>
        
        <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
            <div class="lg:col-span-2 bg-white p-6 rounded-xl shadow-md">
                <h2 class="text-xl font-semibold mb-4">Plano do Dia</h2>
                <form id="formAddTask" class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6 items-end">
                    <div class="md:col-span-2">
                        <label for="taskName" class="block text-sm font-medium text-gray-700">Nome da Tarefa</label>
                        <input type="text" id="taskName" required class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
                    </div>
                    <div>
                        <label for="taskCode" class="block text-sm font-medium text-gray-700">Código</label>
                        <input type="text" id="taskCode" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
                    </div>
                    <div>
                        <label for="taskPoints" class="block text-sm font-medium text-gray-700">Pontos</label>
                        <input type="number" id="taskPoints" required min="1" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
                    </div>
                    <button type="submit" class="btn md:col-start-4 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-lg">Adicionar Tarefa</button>
                </form>
                <div id="taskList" class="space-y-3">
                    <p class="text-gray-500 text-center py-4">Nenhuma tarefa adicionada ainda.</p>
                </div>
            </div>
            <div class="space-y-8">
                <div class="bg-white p-6 rounded-xl shadow-md">
                    <h2 class="text-xl font-semibold mb-4">Tarefa em Execução</h2>
                    <div id="currentTaskDisplay" class="bg-gray-50 p-4 rounded-lg mb-4 text-center">
                        <h3 id="currentTaskName" class="text-lg font-medium text-gray-800">Nenhuma tarefa iniciada</h3>
                        <p id="currentTaskTimer" class="text-5xl font-bold tracking-tight my-2">00:00:00</p>
                        <p class="text-sm text-gray-500">Duração Efetiva</p>
                    </div>
                    <div class="grid grid-cols-2 gap-4">
                        <button id="btnPauseResumeTask" class="btn bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded-lg" disabled>Pausar</button>
                        <button id="btnStopTask" class="finalizar-btn bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg" disabled>Finalizar</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <script>
    // --- CÓDIGO JAVASCRIPT DO CLIENTE ---
    document.addEventListener('DOMContentLoaded', function() {
        // --- ESTADO DA APLICAÇÃO (AGORA SINCRONIZADO COM A PLANILHA) ---
        let state = {
            tasks: [],
            workday: { startTime: null, endTime: null, durationTimer: null, totalSeconds: 0, totalEffectiveSeconds: 0 },
            currentTask: { id: null, timer: null, isPaused: false, effectiveSeconds: 0 },
        };

        // --- ELEMENTOS DO DOM ---
        const loader = document.getElementById('loader');
        const dashboardContent = document.getElementById('dashboard-content');
        const formAddTask = document.getElementById('formAddTask');
        const taskListEl = document.getElementById('taskList');
        const btnStartWorkday = document.getElementById('btnStartWorkday');
        // (Adicione outros elementos do DOM aqui)
        const currentTaskNameEl = document.getElementById('currentTaskName');
        const currentTaskTimerEl = document.getElementById('currentTaskTimer');
        const btnPauseResumeTask = document.getElementById('btnPauseResumeTask');
        const btnStopTask = document.getElementById('btnStopTask');
        const totalPointsCompletedEl = document.getElementById('totalPointsCompleted');
        const productivityRateEl = document.getElementById('productivityRate');
        const workdayStartTimeEl = document.getElementById('workdayStartTime');
        const workdayDurationEl = document.getElementById('workdayDuration');

        // --- FUNÇÕES DE RENDERIZAÇÃO E UI (a maioria permanece a mesma) ---
        const formatTime = (totalSeconds) => {
            if (isNaN(totalSeconds) || totalSeconds < 0) return "00:00:00";
            const hours = Math.floor(totalSeconds / 3600);
            const minutes = Math.floor((totalSeconds % 3600) / 60);
            const seconds = Math.floor(totalSeconds % 60);
            return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
        };

        const formatTimeHours = (totalSeconds) => {
            if (isNaN(totalSeconds) || totalSeconds <= 0) return 0;
            return totalSeconds / 3600;
        };
        
        const renderTasks = () => {
          if (state.tasks.length === 0) {
              taskListEl.innerHTML = '<p class="text-gray-500 text-center py-4">Nenhuma tarefa adicionada ainda.</p>';
              return;
          }
          taskListEl.innerHTML = '';
          state.tasks.forEach(task => {
              const taskEl = document.createElement('div');
              taskEl.className = `p-4 border rounded-lg flex items-center justify-between transition-all duration-300 ${task.status === 'completed' ? 'bg-green-50 border-green-200' : 'bg-white'} ${state.currentTask.id === task.id ? 'task-active' : ''}`;
              taskEl.id = `task-${task.id}`;
              let statusIndicator = '';
              if (task.status === 'completed') {
                  statusIndicator = '<span class="text-xs font-medium bg-green-100 text-green-800 py-1 px-2 rounded-full">Concluída</span>';
              } else if (task.status === 'in-progress') {
                  statusIndicator = '<span class="text-xs font-medium bg-blue-100 text-blue-800 py-1 px-2 rounded-full">Em Progresso</span>';
              }
              taskEl.innerHTML = `
                  <div>
                      <p class="font-semibold">${task.name} <span class="text-sm font-normal text-gray-500">(${task.code || 'N/C'})</span></p>
                      <p class="text-sm text-gray-600">Pontos: ${task.points} | Duração Efetiva: ${formatTime(task.effectiveDuration)}</p>
                  </div>
                  <div class="flex items-center space-x-2">
                      ${statusIndicator}
                      <button data-task-id="${task.id}" class="btn-start-task bg-blue-500 hover:bg-blue-600 text-white p-2 rounded-full disabled:bg-gray-300 disabled:cursor-not-allowed" ${task.status !== 'pending' || !state.workday.startTime || state.currentTask.id ? 'disabled' : ''} title="Iniciar Tarefa">
                          <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
                      </button>
                  </div>
              `;
              taskListEl.appendChild(taskEl);
          });
          document.querySelectorAll('.btn-start-task').forEach(btn => {
              btn.addEventListener('click', () => handleStartTask(btn.dataset.taskId));
          });
        };

        const updateUI = () => {
            renderTasks();
            const totalCompletedPoints = state.tasks.filter(t => t.status === 'completed').reduce((sum, t) => sum + parseInt(t.points, 10), 0);
            totalPointsCompletedEl.textContent = totalCompletedPoints;
            const hoursWorked = formatTimeHours(state.workday.totalEffectiveSeconds);
            const productivity = hoursWorked > 0 ? (totalCompletedPoints / hoursWorked).toFixed(2) : '0.00';
            productivityRateEl.textContent = productivity;
            // (Adicione outras atualizações de UI aqui)
        };

        // --- LÓGICA DE NEGÓCIO (AGORA COM COMUNICAÇÃO COM O APPS SCRIPT) ---
        
        function handleAddTask(e) {
            e.preventDefault();
            const form = e.target;
            const newTask = {
                name: form.taskName.value,
                code: form.taskCode.value,
                points: parseInt(form.taskPoints.value, 10),
                status: 'pending',
                effectiveDuration: 0
            };
            
            form.querySelector('button[type="submit"]').disabled = true; // Desabilita o botão para evitar cliques duplos

            // Chama a função do back-end (Code.gs) para salvar a tarefa
            google.script.run
                .withSuccessHandler(taskAdicionadaComSucesso)
                .withFailureHandler(falhaAoSalvar)
                .addTask(newTask);
        }

        function taskAdicionadaComSucesso(taskComId) {
            formAddTask.reset();
            formAddTask.querySelector('button[type="submit"]').disabled = false;
            state.tasks.push(taskComId); // Adiciona a nova tarefa (com ID) ao estado local
            updateUI();
        }

        function falhaAoSalvar(error) {
            alert('Erro ao salvar a tarefa: ' + error.message);
            formAddTask.querySelector('button[type="submit"]').disabled = false;
        }

        function handleStartTask(taskId) {
            // A lógica para iniciar uma tarefa ainda é principalmente do lado do cliente
            if (state.currentTask.id) {
                alert('Já existe uma tarefa em andamento.');
                return;
            }
            const task = state.tasks.find(t => t.id === taskId);
            if (task) {
                task.status = 'in-progress';
                state.currentTask = { id: taskId, timer: null, isPaused: false, effectiveSeconds: task.effectiveDuration || 0 };
                
                currentTaskNameEl.textContent = task.name;
                currentTaskTimerEl.textContent = formatTime(state.currentTask.effectiveSeconds);
                
                state.currentTask.timer = setInterval(() => {
                    if (!state.currentTask.isPaused) {
                        state.currentTask.effectiveSeconds++;
                        currentTaskTimerEl.textContent = formatTime(state.currentTask.effectiveSeconds);
                    }
                }, 1000);
                updateUI();
            }
        }

        function handleStopTask() {
            clearInterval(state.currentTask.timer);
            const taskId = state.currentTask.id;
            const task = state.tasks.find(t => t.id === taskId);
            
            if (task) {
                task.status = 'completed';
                task.effectiveDuration = state.currentTask.effectiveSeconds;

                // Agora, salva o status finalizado na planilha
                google.script.run
                    .withSuccessHandler(() => console.log(`Tarefa ${taskId} atualizada na planilha.`))
                    .withFailureHandler((err) => alert(`Erro ao atualizar tarefa: ${err.message}`))
                    .updateTaskStatus(taskId, 'completed', task.effectiveDuration);
            }

            state.currentTask = { id: null, timer: null, isPaused: false, effectiveSeconds: 0 };
            currentTaskNameEl.textContent = 'Nenhuma tarefa iniciada';
            currentTaskTimerEl.textContent = '00:00:00';
            updateUI();
        }


        // --- INICIALIZAÇÃO DA APLICAÇÃO ---
        function init() {
            // Adiciona os listeners de eventos
            formAddTask.addEventListener('submit', handleAddTask);
            btnStopTask.addEventListener('click', handleStopTask);
            // (Adicione outros listeners aqui)

            // Busca os dados da planilha ao carregar a página
            google.script.run
                .withSuccessHandler(dadosCarregadosComSucesso)
                .withFailureHandler(falhaAoCarregar)
                .getInitialData();
        }

        function dadosCarregadosComSucesso(data) {
            console.log("Dados recebidos da planilha:", data);
            state.tasks = data.tasks || [];
            // Mostra o conteúdo e esconde o loader
            loader.style.display = 'none';
            dashboardContent.classList.remove('hidden');
            updateUI();
        }

        function falhaAoCarregar(error) {
            loader.style.display = 'none';
            alert('Não foi possível carregar os dados da planilha: ' + error.message);
        }

        // Inicia tudo
        init();
    });
    </script>
  </body>
</html>

Passo 4: Publicar o Web App

Esta é a etapa final e mais importante. É como você "liga" seu aplicativo.

  1. No editor do Apps Script, clique no botão azul Implantar > Nova implantação.
  2. Clique no ícone de engrenagem (⚙️) ao lado de "Selecione o tipo" e escolha App da Web.
  3. Preencha as informações:
    • Descrição: Dashboard de Produtividade Pessoal
    • Executar como: Eu (seu-email@gmail.com)
    • Quem pode acessar: Qualquer pessoa com uma Conta do Google (Recomendado para que você possa acessar de qualquer lugar) ou Apenas eu (se for estritamente pessoal).
  4. Clique em Implantar.
  5. AUTORIZAR PERMISSÕES: O Google pedirá permissão para que o script acesse sua Planilha Google em seu nome. Clique em "Autorizar acesso" e siga as etapas (pode ser necessário clicar em "Avançado" > "Acessar... (não seguro)", mas não se preocupe, é seguro pois é o seu próprio script).
  6. COPIE A URL: Após a implantação, ele fornecerá uma URL do app da Web. Copie essa URL.

Essa URL é o novo link para o seu dashboard! Acesse-a e seu aplicativo estará funcionando, conectado diretamente à sua planilha.

CONCLUSÃO

Os testes que estavam planejados para o App d2p da Temporada 1 desta série serão realizados.

No próximo post, serão apresentados os resultados do Caso de Teste 1 Tarefas sem pausa.

Próximo post: 


Nenhum comentário:

Postar um comentário

Entendendo a Metáfora "Home Office é Moto Boy"

UCEM L-pI.184 Exercícios Parte I Lição 184 - Caso de uso da IA para estudar

  Lição anterior Escolher outra lição Próxima lição Lição 184   O Nome de Deus é a minha herança. Texto e audio da  Lição 184  no site ofici...