25/07/2025 - Atualização da opção "Séries / Exemplo D2P T2" no menu principal, com adição da correção de bugs.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard de Produtividade Pessoal</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></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">
<script src="https://apis.google.com/js/api.js"></script>
<style>
/* Estilo base para a página */
body {
font-family: 'Inter', sans-serif;
background-color: #f0f2f5;
}
/* Efeito de destaque suave nos botões */
.btn {
transition: all 0.2s ease-in-out;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
/* Estilo para o estado 'ativo' de uma tarefa */
.task-active {
box-shadow: 0 0 0 3px #3b82f6; /* Borda azul para indicar atividade */
transform: scale(1.02);
}
</style>
</head>
<body class="bg-gray-100 text-gray-800 p-4 md:p-8">
<div class="max-w-7xl mx-auto">
<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">Gerencie seu dia, suas tarefas e sua produtividade.</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 class="mt-6 border-t pt-4">
<button id="btnGoogleCalendar" class="btn w-full bg-gray-700 hover:bg-gray-800 text-white font-bold py-2 px-4 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm10 5H4v8h12V7z" clip-rule="evenodd"></path></svg>
Criar Plano no Google Calendar
</button>
</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 class="bg-white p-6 rounded-xl shadow-md">
<h2 class="text-xl font-semibold mb-4">Registro de Defeito</h2>
<form id="formAddDefect" class="space-y-4">
<div>
<label for="defectType" class="block text-sm font-medium text-gray-700">Tipo de Defeito</label>
<select id="defectType" required class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<option>Erro de Lógica</option>
<option>Erro de Interface (UI)</option>
<option>Erro de Requisito</option>
<option>Problema de Performance</option>
<option>Outro</option>
</select>
</div>
<div>
<label for="defectDescription" class="block text-sm font-medium text-gray-700">Descrição</label>
<textarea id="defectDescription" rows="2" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"></textarea>
</div>
<button type="submit" class="btn w-full bg-purple-500 hover:bg-purple-600 text-white font-bold py-2 px-4 rounded-lg">Registrar Defeito</button>
</form>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-md">
<h2 class="text-xl font-semibold mb-4">Relatórios do Mês</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div>
<h3 class="text-lg font-medium mb-2">Pareto de Defeitos</h3>
<canvas id="paretoChart"></canvas>
</div>
<div>
<h3 class="text-lg font-medium mb-2">Gráfico de Controle (XmR) de Produtividade</h3>
<canvas id="xmrChart"></canvas>
</div>
</div>
</div>
</div>
<script>
// CORREÇÃO: Trocado '.' por ',' antes da palavra 'function'
document.addEventListener('DOMContentLoaded', function() {
// --- ELEMENTOS DO DOM ---
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');
const formAddDefect = document.getElementById('formAddDefect');
const btnGoogleCalendar = document.getElementById('btnGoogleCalendar');
// --- ESTADO DA APLICAÇÃO ---
let state = {
tasks: [],
defects: [],
// Dados simulados de produtividade para o gráfico XmR
productivityHistory: [
{ date: '2024-05-01', value: 8.5 }, { date: '2024-05-02', value: 7.2 },
{ date: '2024-05-03', value: 9.1 }, { date: '2024-05-05', value: 6.8 },
{ date: '2024-05-06', value: 8.0 }, { date: '2024-05-07', value: 7.5 },
{ date: '2024-05-08', value: 10.2}, { date: '2024-05-09', value: 9.5 },
],
workday: {
startTime: null,
endTime: null,
durationTimer: null,
totalSeconds: 0,
totalEffectiveSeconds: 0,
},
currentTask: {
id: null,
timer: null,
isPaused: false,
effectiveSeconds: 0,
},
nextTaskId: 1,
};
// --- GRÁFICOS (CHART.JS) ---
let paretoChart, xmrChart;
// --- 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 ---
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 = ''; // Limpa a lista
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);
});
// Adiciona event listeners aos novos botões de iniciar tarefa
document.querySelectorAll('.btn-start-task').forEach(btn => {
btn.addEventListener('click', () => handleStartTask(parseInt(btn.dataset.taskId)));
});
};
const updateUI = () => {
renderTasks();
// Atualiza controles da jornada
btnStartWorkday.disabled = !!state.workday.startTime;
btnEndWorkday.disabled = !state.workday.startTime || !!state.workday.endTime || !!state.currentTask.id;
// Atualiza controles da tarefa
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`;
// Atualiza métricas
const totalCompletedPoints = state.tasks
.filter(t => t.status === 'completed')
.reduce((sum, t) => sum + t.points, 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 ---
// JORNADA
const 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.isPaused && state.currentTask.id) {
state.workday.totalEffectiveSeconds++;
updateUI(); // Atualiza a produtividade em tempo real
}
}, 1000);
updateUI();
};
const handleEndWorkday = () => {
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();
const finalProductivity = parseFloat(productivityRateEl.textContent);
if (finalProductivity > 0) {
state.productivityHistory.push({
date: new Date().toISOString().split('T')[0],
value: finalProductivity
});
renderXMRChart();
}
alert(`Jornada finalizada! Produtividade do dia: ${finalProductivity} pontos/hora.`);
updateUI();
};
// TAREFAS
const handleAddTask = (e) => {
e.preventDefault();
const taskName = document.getElementById('taskName').value;
const taskCode = document.getElementById('taskCode').value;
const taskPoints = parseInt(document.getElementById('taskPoints').value);
if (taskName && taskPoints) {
const newTask = {
id: state.nextTaskId++,
name: taskName,
code: taskCode,
points: taskPoints,
status: 'pending', // pending, in-progress, completed
effectiveDuration: 0,
};
state.tasks.push(newTask);
formAddTask.reset();
updateUI();
}
};
const handleStartTask = (taskId) => {
if (state.currentTask.id) {
alert('Já existe uma tarefa em andamento. Finalize-a primeiro.');
return;
}
const task = state.tasks.find(t => t.id === taskId);
if (task) {
task.status = 'in-progress';
state.currentTask.id = taskId;
state.currentTask.effectiveSeconds = task.effectiveDuration; // Continua de onde parou
state.currentTask.isPaused = false;
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();
}
};
const handlePauseResumeTask = () => {
state.currentTask.isPaused = !state.currentTask.isPaused;
updateUI();
};
const handleStopTask = () => {
clearInterval(state.currentTask.timer);
const task = state.tasks.find(t => t.id === state.currentTask.id);
if(task) {
task.status = 'completed';
task.effectiveDuration = state.currentTask.effectiveSeconds;
}
// Resetar tarefa atual
state.currentTask = { id: null, timer: null, isPaused: false, effectiveSeconds: 0 };
currentTaskNameEl.textContent = 'Nenhuma tarefa iniciada';
currentTaskTimerEl.textContent = '00:00:00';
updateUI();
};
// DEFEITOS
const handleAddDefect = (e) => {
e.preventDefault();
const defectType = document.getElementById('defectType').value;
const defectDescription = document.getElementById('defectDescription').value;
if (!defectType) {
alert('Por favor, selecione um tipo de defeito.');
return;
}
state.defects.push({
type: defectType,
description: defectDescription,
timestamp: new Date()
});
formAddDefect.reset();
alert('Defeito registrado com sucesso!');
renderParetoChart(); // Atualiza o gráfico
};
// GOOGLE CALENDAR (PLACEHOLDER)
const handleGoogleCalendar = () => {
alert(
"Funcionalidade de integração com o Google Calendar.\n\n" +
"Para implementar:\n" +
"1. Configure um projeto no Google Cloud Console e ative a API do Google Calendar.\n" +
"2. Crie credenciais de ID do Cliente OAuth 2.0.\n" +
"3. Use a biblioteca 'Google API Client Library for JavaScript (gapi)' para autenticar o usuário e criar os eventos.\n\n" +
"As tarefas do dia seriam adicionadas como eventos no calendário do usuário."
);
};
// --- LÓGICA DOS GRÁFICOS ---
const renderParetoChart = () => {
const ctx = document.getElementById('paretoChart').getContext('2d');
if (paretoChart) paretoChart.destroy();
const defectCounts = state.defects.reduce((acc, defect) => {
acc[defect.type] = (acc[defect.type] || 0) + 1;
return acc;
}, {});
const sortedDefects = Object.entries(defectCounts)
.sort(([, a], [, b]) => b - a);
const labels = sortedDefects.map(([type]) => type);
const data = sortedDefects.map(([, count]) => count);
const totalDefects = data.reduce((sum, count) => sum + count, 0);
if (totalDefects === 0) return; // Não renderiza o gráfico se não houver dados
let cumulativePercentage = 0;
const paretoData = data.map(count => {
cumulativePercentage += (count / totalDefects) * 100;
return cumulativePercentage;
});
paretoChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: 'Nº de Defeitos',
data: data,
backgroundColor: 'rgba(59, 130, 246, 0.7)',
yAxisID: 'y',
},
{
label: 'Cumulativo %',
data: paretoData,
type: 'line',
borderColor: 'rgba(239, 68, 68, 0.8)',
backgroundColor: 'rgba(239, 68, 68, 0.2)',
tension: 0.1,
yAxisID: 'y1',
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: { display: true, text: 'Contagem' },
beginAtZero: true
},
y1: {
type: 'linear',
display: true,
position: 'right',
grid: { drawOnChartArea: false },
min: 0,
max: 100,
title: { display: true, text: 'Percentual Acumulado (%)' }
}
}
}
});
};
const renderXMRChart = () => {
const ctx = document.getElementById('xmrChart').getContext('2d');
if (xmrChart) xmrChart.destroy();
const values = state.productivityHistory.map(h => h.value);
if(values.length < 2) return; // Precisa de pelo menos 2 pontos
// 1. Calcular Média
const mean = values.reduce((a, b) => a + b, 0) / values.length;
// 2. Calcular Moving Ranges (MR)
const movingRanges = [];
for (let i = 1; i < values.length; i++) {
movingRanges.push(Math.abs(values[i] - values[i - 1]));
}
// 3. Calcular Média do Moving Range (MR-bar)
const mrBar = movingRanges.reduce((a, b) => a + b, 0) / movingRanges.length;
// 4. Calcular Limites de Controle (D4 e E2 são constantes estatísticas)
const UCL = mean + (2.66 * mrBar);
const LCL = Math.max(0, mean - (2.66 * mrBar)); // Não pode ser negativo
xmrChart = new Chart(ctx, {
type: 'line',
data: {
labels: state.productivityHistory.map(h => new Date(h.date)),
datasets: [
{
label: 'Produtividade (Pontos/Hora)',
data: values,
borderColor: 'rgba(59, 130, 246, 1)',
backgroundColor: 'rgba(59, 130, 246, 0.2)',
tension: 0.1,
},
{
label: 'Média',
data: Array(values.length).fill(mean.toFixed(2)),
borderColor: 'rgba(22, 163, 74, 1)',
borderDash: [5, 5],
borderWidth: 2,
pointRadius: 0,
},
{
label: 'Limite Superior (UCL)',
data: Array(values.length).fill(UCL.toFixed(2)),
borderColor: 'rgba(239, 68, 68, 1)',
borderWidth: 2,
pointRadius: 0,
},
{
label: 'Limite Inferior (LCL)',
data: Array(values.length).fill(LCL.toFixed(2)),
borderColor: 'rgba(239, 68, 68, 1)',
borderWidth: 2,
pointRadius: 0,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'time',
time: { unit: 'day', tooltipFormat: 'dd/MM/yyyy' },
title: { display: true, text: 'Data' }
},
y: {
title: { display: true, text: 'Pontos / Hora' },
beginAtZero: true
}
}
}
});
};
// --- INICIALIZAÇÃO ---
const init = () => {
// CORREÇÃO: Adicionando todos os event listeners
formAddTask.addEventListener('submit', handleAddTask);
formAddDefect.addEventListener('submit', handleAddDefect);
btnStartWorkday.addEventListener('click', handleStartWorkday);
btnEndWorkday.addEventListener('click', handleEndWorkday);
btnPauseResumeTask.addEventListener('click', handlePauseResumeTask);
btnStopTask.addEventListener('click', handleStopTask);
btnGoogleCalendar.addEventListener('click', handleGoogleCalendar);
// Renderização inicial
updateUI();
renderParetoChart();
renderXMRChart();
};
// Inicia a aplicação
init();
});
</script>
</body>
</html>