From 5c32665b557e02ddf35db6eca690fda5ba4cccb3 Mon Sep 17 00:00:00 2001 From: Gabriel Menezes Date: Sat, 10 Oct 2026 23:00:35 -0400 Subject: [PATCH] fix(dashboard): unify styles, theme tokens, and integrate live timer with real tasks --- new-frontend/src/pages/DashboardPage.tsx | 771 ++++++++++++++--------- 1 file changed, 487 insertions(+), 284 deletions(-) diff --git a/new-frontend/src/pages/DashboardPage.tsx b/new-frontend/src/pages/DashboardPage.tsx index 2c22277..83ea565 100644 --- a/new-frontend/src/pages/DashboardPage.tsx +++ b/new-frontend/src/pages/DashboardPage.tsx @@ -1,374 +1,577 @@ -import { useState, useEffect } from 'react' -import { Link } from 'react-router-dom' -import { - Card, - CardHeader, - CardTitle, - CardDescription, - CardContent, - CardFooter, - Button, - Chip, - ChipLabel, - Avatar, - AvatarFallback, -} from '@heroui/react' +import React, { useState, useEffect, useCallback, useMemo } from 'react' +import { Link, useNavigate } from 'react-router-dom' import { Clock, CheckCircle2, FolderKanban, Play, Square, - Users, TrendingUp, Plus, Sparkles, ArrowRight, Flame, + RefreshCw, + Folder, + Tag, + Activity, + AlertCircle, } from 'lucide-react' import { useAuth } from '../contexts/AuthContext' +import { useTimer } from '../contexts/TimerContext' +import { taskService } from '../services/taskService' +import { projectService } from '../services/projectService' +import type { Task, Project } from '../types/task' +import { calculateTaskTotalSeconds, formatSecondsToHuman, formatSecondsToTimer } from '../lib/timeUtils' +import { CreateTaskModal } from '../components/tasks/CreateTaskModal' -export const DashboardPage = () => { +export const DashboardPage: React.FC = () => { const { user, activeOrg } = useAuth() + const { activeTask, activeSeconds, startTimer, stopTimer, syncWithTasks } = useTimer() + const navigate = useNavigate() - // Simulação de cronômetro local interativo - const [isRunning, setIsRunning] = useState(false) - const [timerSeconds, setTimerSeconds] = useState(1450) + const [tasks, setTasks] = useState([]) + const [projects, setProjects] = useState([]) + const [loading, setLoading] = useState(true) + const [refreshing, setRefreshing] = useState(false) + const [error, setError] = useState(null) + + // Modal + const [isModalOpen, setIsModalOpen] = useState(false) + + const activeOrgId = activeOrg?.id + + const loadData = useCallback(async () => { + if (!activeOrgId) return + try { + setRefreshing(true) + const [tasksRes, projsRes] = await Promise.allSettled([ + taskService.listTasks({ organizationId: activeOrgId }), + projectService.listProjects(activeOrgId), + ]) + + const loadedTasks = tasksRes.status === 'fulfilled' ? tasksRes.value || [] : [] + setTasks(loadedTasks) + syncWithTasks(loadedTasks) + + if (projsRes.status === 'fulfilled') { + setProjects(projsRes.value || []) + } + } catch (err: unknown) { + console.error(err) + setError('Falha ao atualizar dados do dashboard.') + } finally { + setLoading(false) + setRefreshing(false) + } + }, [activeOrgId, syncWithTasks]) useEffect(() => { - let interval: ReturnType - if (isRunning) { - interval = setInterval(() => { - setTimerSeconds((prev) => prev + 1) - }, 1000) - } - return () => clearInterval(interval) - }, [isRunning]) + loadData() + }, [loadData]) - const formatTimer = (total: number) => { - const h = Math.floor(total / 3600) - const m = Math.floor((total % 3600) / 60) - const s = total % 60 - return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}` + // KPIs + const stats = useMemo(() => { + const inProgress = tasks.filter((t) => t.status === 'IN_PROGRESS').length + const completed = tasks.filter((t) => t.status === 'COMPLETED').length + const notStarted = tasks.filter((t) => t.status === 'NOT_STARTED').length + const totalSeconds = tasks.reduce((acc, t) => acc + calculateTaskTotalSeconds(t), 0) + + return { + inProgress, + completed, + notStarted, + totalSeconds, + projectsCount: projects.length, + } + }, [tasks, projects]) + + // Recent 5 tasks + const recentTasks = useMemo(() => { + return [...tasks].slice(0, 5) + }, [tasks]) + + // Toggle timer on a task + const handleToggleTimer = async (task: Task) => { + try { + if (activeTask?.id === task.id) { + const stopped = await stopTimer(task) + if (stopped) { + setTasks((prev) => prev.map((t) => (t.id === stopped.id ? stopped : t))) + } + } else { + const started = await startTimer(task) + setTasks((prev) => prev.map((t) => (t.id === started.id ? started : t))) + } + } catch (err: unknown) { + console.error(err) + setError('Erro ao acionar cronômetro.') + } } - const kpis = [ - { - label: 'Tarefas em Andamento', - value: '4', - sub: '2 com prazo hoje', - icon: , - bg: 'bg-brand-soft', - }, - { - label: 'Finalizadas nesta Semana', - value: '18', - sub: '+25% vs semana anterior', - icon: , - bg: 'bg-emerald-50', - }, - { - label: 'Horas Rastreadas (Você)', - value: '26.4h', - sub: 'Média de 6.6h / dia', - icon: , - bg: 'bg-accent-soft', - }, - { - label: 'Projetos Conectados', - value: '5', - sub: 'No workspace ativo', - icon: , - bg: 'bg-brand-soft', - }, - ] + const handleModalSubmit = async (taskData: { + title: string + description: string + status: 'NOT_STARTED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELED' + projectId?: number + categoryId?: number + assigneeUserId?: number + }) => { + if (!activeOrgId) return + const created = await taskService.createTask({ + title: taskData.title, + description: taskData.description, + status: taskData.status, + projectId: taskData.projectId, + categoryId: taskData.categoryId, + assigneeUserId: taskData.assigneeUserId, + organizationId: activeOrgId, + }) + setTasks((prev) => [created, ...prev]) + } - const recentTasks = [ - { - id: 1, - title: 'Implementação do AppShell e Sidebar retrátil', - project: 'Frontend v3', - category: 'Feature', - status: 'EM ANDAMENTO', - time: '02h 15m', - }, - { - id: 2, - title: 'Ajuste de Headers CORS e preflight no Quarkus', - project: 'Core Backend', - category: 'Bugfix', - status: 'CONCLUÍDO', - time: '01h 05m', - }, - { - id: 3, - title: 'Configuração de balde MinIO e expiração de URLs', - project: 'Infra & Storage', - category: 'DevOps', - status: 'A FAZER', - time: '00h 00m', - }, - ] + // Priority task to focus on if none running + const suggestedTask = useMemo(() => { + return tasks.find((t) => t.status === 'IN_PROGRESS') || tasks.find((t) => t.status === 'NOT_STARTED') || tasks[0] + }, [tasks]) return ( -
- {/* Top Welcome Banner */} -
+
+ {/* Header */} +
-
-

+
+

Olá, {user?.name?.split(' ')[0] || 'Desenvolvedor'}! 👋

- - {activeOrg?.name || 'Workspace'} - + + {activeOrg?.name || 'Workspace'} +
-

- Aqui está o resumo da sua produtividade e das atividades da equipe hoje. +

+ Visão geral da sua produtividade, metas e atividades recentes no workspace.

-
- - - - - - +
+ + + + +
- {/* KPI Stats Grid */} -
- {kpis.map((kpi, idx) => ( - +
+ + {error} +
+
+ Fechar + +
+ )} - {/* Main Grid: Active Task Tracker + Recent Tasks */} -
- {/* Active Session & Pomodoro Card (Left 5 cols) */} -
- - -
-
+ {/* KPI Cards */} + {loading ? ( +
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+ ) : ( +
+ {/* In Progress */} +
+
+ Tarefas em Andamento +
+ +
+
+
+

+ {stats.inProgress} +

+

+ {stats.notStarted} no backlog para iniciar +

+
+
+ + {/* Completed */} +
+
+ Entregas Concluídas +
+ +
+
+
+

+ {stats.completed} +

+

+ de {tasks.length} tarefas cadastradas +

+
+
+ + {/* Time Tracked */} +
+
+ Tempo Total Dedicado +
+ +
+
+
+

+ {formatSecondsToTimer(stats.totalSeconds)} +

+

+ {formatSecondsToHuman(stats.totalSeconds)} acumuladas +

+
+
+ + {/* Projects */} +
+
+ Projetos Ativos +
+ +
+
+
+

+ {stats.projectsCount} +

+

+ Iniciativas no workspace +

+
+
+
+ )} + + {/* Main Content: Focus Session (Left) + Recent Tasks (Right) */} +
+ {/* Focus Session Card (Left 5 cols) */} +
+
+
+
+
- - Sessão em Foco - - - Time tracking em tempo real - +

Sessão em Foco

+

Cronômetro em tempo real

- - {isRunning ? "ATIVO" : "PAUSADO"} - - - -
+ + {activeTask ? 'ATIVO' : 'EM ESPERA'} + +
+ + {/* Active or Suggested Task Info */} + {activeTask ? ( +
- - Frontend v3 - - - Squad Core - + {activeTask.project && ( + + {activeTask.project.name} + + )} + {activeTask.category && ( + + {activeTask.category.name} + + )}
-

- Refatoração do Layout Principal & Navegação com HeroUI +

+ {activeTask.title}

- - {/* Big Timer display */} -
-

- Tempo na Tarefa -

-

- {formatTimer(timerSeconds)} -

-
- -
- +
+

+ {suggestedTask.title} +

- + ) : ( +
+ Nenhuma tarefa pendente. Crie uma nova tarefa para iniciar o foco! +
+ )} - - Sessão vinculada ao usuário - - {user?.email} + {/* Big Timer Display */} +
+ + {activeTask ? 'Tempo Decorrido na Tarefa' : 'Pronto para Cronometrar'} - - +

+ {activeTask ? formatSecondsToTimer(activeSeconds) : '00:00:00'} +

+
+ + {/* Controls */} + {activeTask ? ( + + ) : suggestedTask ? ( + + ) : ( + + )} + +
+ Usuário ativo: + + {user?.name || user?.email} + +
+
{/* Quick Shortcuts */} - -

- Acesso Rápido -

+
+ + Atalhos de Gestão +
- Ver Projetos + Projetos - - Métricas da Org + + Analytics
- +
{/* Recent Tasks List (Right 7 cols) */}
-

+

- Tarefas Recentes da Sprint + Tarefas Recentes

- Ver todas + Ver todas
-
- {recentTasks.map((task) => ( - + {[1, 2, 3].map((i) => ( +
+ ))} +
+ ) : recentTasks.length === 0 ? ( +
+ +

+ Nenhuma tarefa encontrada neste workspace. +

+ +
+ ) : ( +
+ {recentTasks.map((task) => { + const isTracking = activeTask?.id === task.id + const taskSec = isTracking ? activeSeconds : calculateTaskTotalSeconds(task) -
- - {task.time} - - - {task.status} - -
- - ))} -
+
+
+ {task.project && ( + + + {task.project.name} + + )} + {task.category && ( + + + {task.category.name} + + )} +
- {/* Team Active Members Banner */} - -
-
- -
-
-

- Squad Ativo no Workspace -

-

- 3 colaboradores com sessões registradas hoje -

-
-
+

navigate('/tasks')} + className="text-xs font-bold text-foreground hover:text-brand cursor-pointer truncate" + > + {task.title} +

+
-
- - - GB - - - - - AL - - - - - MS - - +
+
+ + {formatSecondsToTimer(taskSec)} +
+ + +
+
+ ) + })}
- + )}
+ + {/* Task Creation Modal */} + {activeOrgId && ( + setIsModalOpen(false)} + onSubmit={handleModalSubmit} + organizationId={activeOrgId} + initialStatus="NOT_STARTED" + /> + )}
) }