Seguidores

sábado, 28 de junho de 2025

Dashboard de Produtividade Pessoal T2E03: BugCT1-8 Botões do Dashboard não funcionam

INTRODUÇÃO

Post anterior: Dashboard de Produtividade Pessoal T2E02: Caso de Teste 1 Tarefa sem pausa

O objetivo deste post é apresentar o relato de um bug encontrado durante a execução do "Caso de Teste 1: Tarefa sem pausa", na temporada 2 da série Dashboard de Produtividade Pessoal.



Promp para Gemini 2.5 Pro (Prévia)

1 Objetivo
Corrija bugs no App d2p, que ocorreram na execução do Caso de Teste 1, após a mudança da arquitetura.

2 Contexto
A arquitetura do App d2p mudou para:
  • Frontend Dashboard de Produtividade Pessoal
  • Backend Google Sheet
  • Apps Scritp para integração do Dashboard com Google Sheet
3 Dados, Fatos e Bugs
Dados do Caso de Teste 1:

  • CódigoTarefa: S00.1.1.01; NomeTarefa: Tarefa 1 Teste 1; TamanhoTarefa: 5
  • CódigoTarefa: S00.1.1.02; NomeTarefa: Tarefa 2 Teste 1; TamanhoTarefa: 1
  • CódigoTarefa: S00.1.1.03; NomeTarefa: Tarefa 3 Teste 1; TamanhoTarefa: 3

Fatos e Bugs na execução do Caso de Teste 1:
  • Ao clicar no botão Iniciar Jornada, não foi registrada a hora e nem iniciada a contagem do tempo.
  • Ao clicar no botão "Play" para iniciar a tarefa registrada, o botão não estava habilitado.
  • Ao clicar no botão para finalizar a tarefa, o botão "Finalizar" estava habilitado, mas não funcionou.

Resposta do Gemini 2.5 Pro (Prévia)

Olá! Perfeito. Agradeço pelo relatório detalhado dos bugs. Casos de teste são essenciais para encontrar problemas, e sua descrição foi muito clara.

Analisei os três bugs e a boa notícia é que todos eles têm a mesma causa raiz, o que torna a correção bem direta.

Diagnóstico da Causa Raiz

A causa principal dos três problemas está na função de inicialização do nosso script (init()) dentro do arquivo dashboard.js. Após a mudança de arquitetura e a movimentação do código, esquecemos de "conectar" os eventos de clique de alguns botões principais (como "Iniciar Jornada") às suas respectivas funções JavaScript.

Isso gerou um efeito dominó:

  1. Bug 1 (Jornada não inicia): O botão "Iniciar Jornada" não tinha seu addEventListener registrado. Por isso, ao clicar, nada acontecia.
  2. Bug 2 (Botão "Play" desabilitado): O botão para iniciar uma tarefa só é habilitado após o início da jornada (state.workday.startTime precisa ter um valor). Como a jornada nunca iniciava, os botões de tarefa permaneciam desabilitados.
  3. Bug 3 (Finalizar não funciona): O fluxo para testar o botão "Finalizar" (btnStopTask) estava quebrado porque não era possível iniciar uma tarefa corretamente. Além disso, o listener do botão de pausa/retomar também estava faltando, o que prejudicaria a interação.

Solução

A solução é garantir que todos os botões da interface tenham seus addEventListener corretamente registrados na função init() do dashboard.js.

Abaixo, forneço a versão corrigida e completa do arquivo dashboard.js. Ele contém a lógica de inicialização correta, que resolve os três bugs de uma só vez.


Código Corrigido para dashboard.js

Copie todo o código abaixo e substitua completamente o conteúdo do seu arquivo dashboard.js atual. As correções estão destacadas com comentários.

JavaScript
document.addEventListener('DOMContentLoaded', function() {
    // ---- VERIFICAÇÃO DE LOGIN E AUTENTICAÇÃO ----
    // Esta parte permanece a mesma, garantindo que o usuário esteja logado.
    const nomeUsuario = localStorage.getItem('usuarioNome');
    const emailUsuario = localStorage.getItem('usuarioEmail');

    if (!nomeUsuario || !emailUsuario) {
        alert("Acesso negado. Por favor, faça o login.");
        window.location.href = 'index.html';
        return;
    }

    document.getElementById('welcome-message').textContent = `Logado como: ${nomeUsuario}`;

    document.getElementById('logoutButton').addEventListener('click', () => {
        if (typeof google !== 'undefined' && google.accounts && google.accounts.id) {
            google.accounts.id.disableAutoSelect();
        }
        localStorage.clear();
        window.location.href = 'index.html';
    });

    // ---- LÓGICA DO DASHBOARD ----
    
    // --- 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');
    const btnEndWorkday = document.getElementById('btnEndWorkday');
    const workdayStartTimeEl = document.getElementById('workdayStartTime');
    const workdayDurationEl = document.getElementById('workdayDuration');
    const productivityRateEl = document.getElementById('productivityRate');
    const totalPointsCompletedEl = document.getElementById('totalPointsCompleted');
    const currentTaskNameEl = document.getElementById('currentTaskName');
    const currentTaskTimerEl = document.getElementById('currentTaskTimer');
    const btnPauseResumeTask = document.getElementById('btnPauseResumeTask');
    const btnStopTask = document.getElementById('btnStopTask');

    // --- ESTADO DA APLICAÇÃO ---
    let state = {
        tasks: [],
        workday: { startTime: null, endTime: null, durationTimer: null, totalSeconds: 0, totalEffectiveSeconds: 0 },
        currentTask: { id: null, timer: null, isPaused: false, effectiveSeconds: 0 },
    };

    // --- FUNÇÕES DE UTILIDADE ---
    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;
    };

    // --- RENDERIZAÇÃO E UI ---
    const renderTasks = () => {
        if (!taskListEl) return;
        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();
        btnStartWorkday.disabled = !!state.workday.startTime;
        btnEndWorkday.disabled = !state.workday.startTime || !!state.workday.endTime || !!state.currentTask.id;
        
        const hasActiveTask = !!state.currentTask.id;
        btnPauseResumeTask.disabled = !hasActiveTask;
        btnStopTask.disabled = !hasActiveTask;
        btnPauseResumeTask.textContent = state.currentTask.isPaused ? 'Retomar' : 'Pausar';
        btnPauseResumeTask.className = `btn font-bold py-2 px-4 rounded-lg ${state.currentTask.isPaused ? 'bg-green-500 hover:bg-green-600' : 'bg-yellow-500 hover:bg-yellow-600'} text-white`;

        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;
    };

    // --- LÓGICA DE NEGÓCIO ---
    function handleStartWorkday() {
        state.workday.startTime = new Date();
        workdayStartTimeEl.textContent = state.workday.startTime.toLocaleTimeString();
        state.workday.durationTimer = setInterval(() => {
            state.workday.totalSeconds = Math.floor((new Date() - state.workday.startTime) / 1000);
            workdayDurationEl.textContent = formatTime(state.workday.totalSeconds);
            if (state.currentTask.id && !state.currentTask.isPaused) {
                state.workday.totalEffectiveSeconds++;
                updateUI();
            }
        }, 1000);
        updateUI();
    }
    
    function handleEndWorkday() {
        // Lógica para finalizar a jornada
        if(state.currentTask.id) {
            alert('Por favor, finalize a tarefa atual antes de encerrar a jornada.');
            return;
        }
        clearInterval(state.workday.durationTimer);
        state.workday.endTime = new Date();
        updateUI();
        // Aqui você chamaria google.script.run para salvar os dados da jornada, se necessário
    }

    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;
        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);
        updateUI();
    }

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

    function handleStartTask(taskId) {
        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: parseInt(task.effectiveDuration, 10) || 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 handlePauseResumeTask() {
        state.currentTask.isPaused = !state.currentTask.isPaused;
        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;
            google.script.run
                .withSuccessHandler(() => console.log(`Tarefa ${taskId} atualizada para 'completed' 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() {
        // CORREÇÃO: Adicionando os listeners que estavam faltando
        formAddTask.addEventListener('submit', handleAddTask);
        btnStartWorkday.addEventListener('click', handleStartWorkday);
        btnEndWorkday.addEventListener('click', handleEndWorkday);
        btnPauseResumeTask.addEventListener('click', handlePauseResumeTask);
        btnStopTask.addEventListener('click', handleStopTask);

        // Busca os dados iniciais da planilha
        google.script.run
            .withSuccessHandler(dadosCarregadosComSucesso)
            .withFailureHandler(falhaAoCarregar)
            .getInitialData();
    }

    function dadosCarregadosComSucesso(data) {
        console.log("Dados recebidos da planilha:", data);
        state.tasks = data.tasks || [];
        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);
    }

    init();
});

Próximos Passos

  1. Substitua o código: Vá ao editor do Apps Script e substitua todo o conteúdo do seu dashboard.js pelo código corrigido acima.
  2. Salve o Projeto: Clique no ícone de disquete ("Salvar projeto").
  3. Teste Novamente: Acesse a URL da sua implantação do App da Web e execute o Caso de Teste 1 novamente.

Agora, os botões de jornada e de tarefas devem funcionar conforme o esperado. Por favor, realize os testes e me informe o resultado!

Pergunta ao Gemini

Olá, Gemini! O que fazer para salvar a mudança no Apps Script?

Resposta do Gemini

Olá! Ótima pergunta. Essa é uma etapa crucial e um ponto que confunde muitos iniciantes no Apps Script.

Salvar as alterações no Apps Script envolve duas fases: 1) Salvar o código no editor e 2) Publicar as alterações para que elas apareçam no seu aplicativo web.

Aqui está o passo a passo detalhado:

Fase 1: Salvar o Código no Editor

Esta é a parte mais simples. Você tem duas maneiras principais de fazer isso:

  1. Usando o Ícone de Salvar:

    • No topo do editor do Apps Script, procure pelo ícone de um disquete 💾.
    • Clique neste ícone (Salvar projeto) para salvar todas as alterações que você fez nos seus arquivos (Code.gs, Dashboard.html, etc.).
  2. Usando o Atalho de Teclado:

    • Como na maioria dos editores, você pode usar o atalho de teclado:
      • Ctrl + S (no Windows ou Linux)
      • Cmd + S (no Mac)

Dica: Você saberá que um arquivo tem alterações não salvas se aparecer um pequeno asterisco * ao lado do nome do arquivo na aba. Depois de salvar, o asterisco desaparece.


Fase 2: Publicar (Implantar) as Alterações no Aplicativo Web

Esta é a parte mais importante e que muitas vezes é esquecida. Apenas salvar o código não atualiza a URL do seu aplicativo web. Você precisa criar uma nova versão da sua implantação.

  1. Abra o Gerenciador de Implantações:

    • No topo da tela, clique no botão azul Implantar.
    • No menu que aparecer, selecione Gerenciar implantações.
  2. Edite sua Implantação Ativa:

    • Você verá sua implantação do "App da Web" na lista. Ela estará marcada como "Ativa".
    • À direita dela, clique no ícone de lápis (✏️) para editar.
  3. Crie uma Nova Versão:

    • Na caixa de diálogo que se abre, o mais importante é o seletor Versão.
    • Clique nele e escolha Nova versão.
  4. Implante a Nova Versão:

    • Clique no botão azul Implantar na parte inferior da caixa.

Pronto! Suas alterações agora estão ativas na URL do seu aplicativo web. Você pode recarregar a página do seu dashboard para ver os bugs corrigidos.

Resumo Rápido

Salvar (💾) + Implantar (⚙️ Nova versão) = Mudanças no ar!

Sempre que fizer uma alteração no código que queira ver refletida no seu site, você precisa seguir esses dois passos.


CONCLUSÃO

Após instalar a correção, o Caso de Teste 1 será executado outra vez.

Resultado do Caso de Teste 1, após corrigir o BugCT1-8: falhou.

No próximo post será apresentado o resultado do teste da correção do BugCT1-8.

Nenhum comentário:

Postar um comentário

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

Exemplos D2P T2E2: Exemplo de uso do app D2P - Lição 1

INTRODUÇÃO Post anterior:  Exemplos D2P T2E1: Exemplo de uso do app D2P - Introdução Este é o episódio 2 da temporada 1 da série  Exemplos D...