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:
- Google Sheets como Banco de Dados: Uma planilha guardará todas as suas tarefas, defeitos e histórico de produtividade de forma permanente.
- 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.
- 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 delocalStorageou redirecionamentos.
Passo 1: Preparar a Planilha Google (Seu Banco de Dados)
-
Crie uma nova Planilha Google:
.sheets.new -
Renomeie o arquivo para algo como "Banco de Dados - D2P".
-
Crie três abas (páginas) na parte inferior com os seguintes nomes exatos:
TarefasDefeitosHistoricoProdutividade
-
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)
- Com a planilha aberta, vá no menu Extensões > Apps Script.
- Isso abrirá o editor de scripts em uma nova aba. Apague todo o código que estiver no arquivo
Code.gs. - Crie o arquivo do Dashboard:
- No menu à esquerda do editor, clique no
+ao lado de "Arquivos". - Selecione
HTML. - Dê o nome
Dashboardpara o arquivo e pressione Enter. Apague o conteúdo padrão dele.
- No menu à esquerda do editor, clique no
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.
// 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.
<!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.
- No editor do Apps Script, clique no botão azul Implantar > Nova implantação.
- Clique no ícone de engrenagem (⚙️) ao lado de "Selecione o tipo" e escolha App da Web.
- 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) ouApenas eu(se for estritamente pessoal).
- Descrição:
- Clique em Implantar.
- 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).
- 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.

Nenhum comentário:
Postar um comentário