diff --git a/new-frontend/src/App.tsx b/new-frontend/src/App.tsx index d45be76..542319c 100644 --- a/new-frontend/src/App.tsx +++ b/new-frontend/src/App.tsx @@ -12,6 +12,8 @@ import { ProjectsPage } from './pages/ProjectsPage' import { CategoriesPage } from './pages/CategoriesPage' import { MembersPage } from './pages/MembersPage' import { OrganizationSettingsPage } from './pages/OrganizationSettingsPage' +import { AnalyticsPage } from './pages/AnalyticsPage' +import { ProfilePage } from './pages/ProfilePage' export function App() { return ( @@ -38,10 +40,10 @@ export function App() { } /> } /> } /> - } /> + } /> } /> } /> - } /> + } /> {/* Fallback */} diff --git a/new-frontend/src/pages/AnalyticsPage.tsx b/new-frontend/src/pages/AnalyticsPage.tsx new file mode 100644 index 0000000..ff888c6 --- /dev/null +++ b/new-frontend/src/pages/AnalyticsPage.tsx @@ -0,0 +1,535 @@ +import React, { useState, useEffect, useCallback, useMemo } from 'react' +import { + Clock, + CheckCircle2, + TrendingUp, + Folder, + Tag, + Users, + RefreshCw, + Sparkles, + Activity, + AlertCircle, +} from 'lucide-react' +import { useAuth } from '../contexts/AuthContext' +import { taskService } from '../services/taskService' +import { projectService } from '../services/projectService' +import { categoryService } from '../services/categoryService' +import { organizationService, type OrganizationMember } from '../services/organizationService' +import type { Task, Project, Category } from '../types/task' +import { calculateTaskTotalSeconds, formatSecondsToHuman, formatSecondsToTimer } from '../lib/timeUtils' + +export const AnalyticsPage: React.FC = () => { + const { activeOrg } = useAuth() + + const [tasks, setTasks] = useState([]) + const [projects, setProjects] = useState([]) + const [categories, setCategories] = useState([]) + const [members, setMembers] = useState([]) + + const [loading, setLoading] = useState(true) + const [refreshing, setRefreshing] = useState(false) + const [error, setError] = useState(null) + + const activeOrgId = activeOrg?.id + + const loadData = useCallback(async () => { + if (!activeOrgId) return + try { + setRefreshing(true) + const [tasksRes, projsRes, catsRes, memsRes] = await Promise.allSettled([ + taskService.listTasks({ organizationId: activeOrgId }), + projectService.listProjects(activeOrgId), + categoryService.listCategories(activeOrgId), + organizationService.listMembers(activeOrgId), + ]) + + if (tasksRes.status === 'fulfilled') setTasks(tasksRes.value || []) + if (projsRes.status === 'fulfilled') setProjects(projsRes.value || []) + if (catsRes.status === 'fulfilled') setCategories(catsRes.value || []) + if (memsRes.status === 'fulfilled') setMembers(memsRes.value || []) + } catch (err: unknown) { + console.error(err) + setError('Falha ao calcular métricas do workspace.') + } finally { + setLoading(false) + setRefreshing(false) + } + }, [activeOrgId]) + + useEffect(() => { + loadData() + }, [loadData]) + + // Aggregate high-level KPIs + const kpis = useMemo(() => { + const totalTasks = tasks.length + const completedTasks = tasks.filter((t) => t.status === 'COMPLETED').length + const inProgressTasks = tasks.filter((t) => t.status === 'IN_PROGRESS').length + const notStartedTasks = tasks.filter((t) => t.status === 'NOT_STARTED').length + const canceledTasks = tasks.filter((t) => t.status === 'CANCELED').length + + const totalSecondsTracked = tasks.reduce( + (acc, t) => acc + calculateTaskTotalSeconds(t), + 0 + ) + + const completionRate = + totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0 + + const avgSecondsPerCompletedTask = + completedTasks > 0 + ? Math.round( + tasks + .filter((t) => t.status === 'COMPLETED') + .reduce((acc, t) => acc + calculateTaskTotalSeconds(t), 0) / completedTasks + ) + : 0 + + return { + totalTasks, + completedTasks, + inProgressTasks, + notStartedTasks, + canceledTasks, + totalSecondsTracked, + completionRate, + avgSecondsPerCompletedTask, + } + }, [tasks]) + + // Project breakdown + const projectBreakdown = useMemo(() => { + const map = new Map() + + projects.forEach((p) => { + map.set(p.id, { project: p, seconds: 0, taskCount: 0 }) + }) + + let unassignedSeconds = 0 + let unassignedTasks = 0 + + tasks.forEach((t) => { + const sec = calculateTaskTotalSeconds(t) + if (t.project?.id && map.has(t.project.id)) { + const item = map.get(t.project.id)! + item.seconds += sec + item.taskCount += 1 + } else { + unassignedSeconds += sec + unassignedTasks += 1 + } + }) + + const list = Array.from(map.values()) + if (unassignedTasks > 0) { + list.push({ + project: { id: 0, name: 'Sem Projeto', color: '#64748b' }, + seconds: unassignedSeconds, + taskCount: unassignedTasks, + }) + } + + return list.sort((a, b) => b.seconds - a.seconds) + }, [projects, tasks]) + + // Category breakdown + const categoryBreakdown = useMemo(() => { + const map = new Map() + + categories.forEach((c) => { + map.set(c.id, { category: c, seconds: 0, taskCount: 0 }) + }) + + let unassignedSeconds = 0 + let unassignedTasks = 0 + + tasks.forEach((t) => { + const sec = calculateTaskTotalSeconds(t) + if (t.category?.id && map.has(t.category.id)) { + const item = map.get(t.category.id)! + item.seconds += sec + item.taskCount += 1 + } else { + unassignedSeconds += sec + unassignedTasks += 1 + } + }) + + const list = Array.from(map.values()) + if (unassignedTasks > 0) { + list.push({ + category: { id: 0, name: 'Sem Categoria', color: '#64748b' }, + seconds: unassignedSeconds, + taskCount: unassignedTasks, + }) + } + + return list.sort((a, b) => b.seconds - a.seconds) + }, [categories, tasks]) + + // Member breakdown + const memberBreakdown = useMemo(() => { + const map = new Map< + string, + { name: string; email: string; seconds: number; completedCount: number; totalCount: number } + >() + + members.forEach((m) => { + map.set(m.userEmail, { + name: m.userName, + email: m.userEmail, + seconds: 0, + completedCount: 0, + totalCount: 0, + }) + }) + + tasks.forEach((t) => { + const sec = calculateTaskTotalSeconds(t) + const email = t.user?.email || 'unassigned@bee.internal' + if (!map.has(email)) { + map.set(email, { + name: t.user?.name || 'Não atribuído', + email, + seconds: 0, + completedCount: 0, + totalCount: 0, + }) + } + const item = map.get(email)! + item.seconds += sec + item.totalCount += 1 + if (t.status === 'COMPLETED') item.completedCount += 1 + }) + + return Array.from(map.values()).sort((a, b) => b.seconds - a.seconds) + }, [members, tasks]) + + return ( +
+ {/* Header */} +
+
+
+

+ Analytics & Produtividade +

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

+ Auditoria de tempo investido, distribuição por iniciativas e acompanhamento da vazão da equipe. +

+
+ + +
+ + {error && ( +
+
+ + {error} +
+ +
+ )} + + {loading ? ( +
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+ ) : ( + <> + {/* Top 4 KPI Cards */} +
+ {/* Total Time */} +
+
+ Tempo Total Dedicado +
+ +
+
+
+

+ {formatSecondsToTimer(kpis.totalSecondsTracked)} +

+

+ Equivalente a {formatSecondsToHuman(kpis.totalSecondsTracked)} +

+
+
+ + {/* Completion Rate */} +
+
+ Taxa de Conclusão +
+ +
+
+
+

+ {kpis.completionRate}% +

+

+ {kpis.completedTasks} de {kpis.totalTasks} demandas finalizadas +

+
+
+ + {/* In Progress vs Backlog */} +
+
+ Em Execução Agora +
+ +
+
+
+

+ {kpis.inProgressTasks} tarefas +

+

+ {kpis.notStartedTasks} no backlog aguardando início +

+
+
+ + {/* Avg Time per Task */} +
+
+ Média por Entrega +
+ +
+
+
+

+ {formatSecondsToHuman(kpis.avgSecondsPerCompletedTask)} +

+

+ Tempo médio gasto em cada entrega concluída +

+
+
+
+ + {/* Main Grid: Projects & Categories Breakdown */} +
+ {/* Project Time Distribution */} +
+
+
+
+ +
+

+ Tempo por Projeto +

+
+ + {projectBreakdown.length} projetos + +
+ + {projectBreakdown.length === 0 ? ( +

Nenhum dado de projeto disponível.

+ ) : ( +
+ {projectBreakdown.map((item) => { + const pct = + kpis.totalSecondsTracked > 0 + ? Math.round((item.seconds / kpis.totalSecondsTracked) * 100) + : 0 + return ( +
+
+
+ + + {item.project.name} + + + ({item.taskCount} {item.taskCount === 1 ? 'tarefa' : 'tarefas'}) + +
+
+ + {formatSecondsToHuman(item.seconds)} + + + {pct}% + +
+
+
+
+
+
+ ) + })} +
+ )} +
+ + {/* Category Time Distribution */} +
+
+
+
+ +
+

+ Tempo por Categoria / Tag +

+
+ + {categoryBreakdown.length} categorias + +
+ + {categoryBreakdown.length === 0 ? ( +

Nenhum dado de categoria disponível.

+ ) : ( +
+ {categoryBreakdown.map((item) => { + const pct = + kpis.totalSecondsTracked > 0 + ? Math.round((item.seconds / kpis.totalSecondsTracked) * 100) + : 0 + return ( +
+
+
+ + + {item.category.name} + + + ({item.taskCount} {item.taskCount === 1 ? 'tarefa' : 'tarefas'}) + +
+
+ + {formatSecondsToHuman(item.seconds)} + + + {pct}% + +
+
+
+
+
+
+ ) + })} +
+ )} +
+
+ + {/* Team Productivity Table */} +
+
+
+
+ +
+

+ Dedicação por Membro da Equipe +

+
+ + {memberBreakdown.length} colaboradores + +
+ +
+ + + + + + + + + + + {memberBreakdown.map((member, idx) => ( + + + + + + + ))} + +
MembroTarefas AtribuídasEntregas ConcluídasTempo Total Registrado
+
+
+ {member.name.charAt(0)} +
+
+

{member.name}

+

{member.email}

+
+
+
+ {member.totalCount} + + + + {member.completedCount} + + + {formatSecondsToTimer(member.seconds)} +
+
+
+ + )} +
+ ) +} diff --git a/new-frontend/src/pages/ProfilePage.tsx b/new-frontend/src/pages/ProfilePage.tsx new file mode 100644 index 0000000..9d8d8d9 --- /dev/null +++ b/new-frontend/src/pages/ProfilePage.tsx @@ -0,0 +1,196 @@ +import React, { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { + Mail, + LogOut, + ExternalLink, + Loader2, +} from 'lucide-react' +import { useAuth } from '../contexts/AuthContext' +import { taskService } from '../services/taskService' +import type { Task } from '../types/task' +import { calculateTaskTotalSeconds, formatSecondsToHuman, formatSecondsToTimer } from '../lib/timeUtils' + +export const ProfilePage: React.FC = () => { + const { user, activeOrg, setActiveOrg, logout } = useAuth() + const navigate = useNavigate() + + const [myTasks, setMyTasks] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + if (!activeOrg?.id) return + const loadMyTasks = async () => { + try { + const res = await taskService.listMyTasks({ organizationId: activeOrg.id }) + setMyTasks(res || []) + } catch (err) { + console.error('Failed to load my tasks', err) + } finally { + setLoading(false) + } + } + loadMyTasks() + }, [activeOrg?.id]) + + const myTotalSeconds = myTasks.reduce( + (acc, t) => acc + calculateTaskTotalSeconds(t), + 0 + ) + const myCompletedCount = myTasks.filter((t) => t.status === 'COMPLETED').length + + const handleLogout = () => { + logout() + navigate('/login') + } + + if (!user) return null + + return ( +
+ {/* Header */} +
+

+ Meu Perfil & Conta +

+

+ Gerencie seus dados pessoais, credenciais de acesso e visualize seu desempenho. +

+
+ + {/* Main Profile Info Card */} +
+
+
+
+ {user.name ? user.name.charAt(0).toUpperCase() : user.email.charAt(0).toUpperCase()} +
+
+

{user.name || 'Usuário'}

+

+ + {user.email} +

+
+
+ + +
+ + {/* My Performance KPIs */} +
+

+ Meu Desempenho no Workspace Atual ({activeOrg?.name}) +

+ {loading ? ( +
+ + Carregando estatísticas pessoais... +
+ ) : ( +
+
+ + Tempo Registrado + + + {formatSecondsToTimer(myTotalSeconds)} + + + ({formatSecondsToHuman(myTotalSeconds)}) + +
+ +
+ + Tarefas Concluídas + + + {myCompletedCount} entregas + + + de {myTasks.length} atribuídas a você + +
+ +
+ + Workspace Ativo + + + {activeOrg?.name} + + + {activeOrg?.roles.join(', ')} + +
+
+ )} +
+ + {/* My Workspaces */} +
+
+

+ Meus Workspaces Cadastrados +

+ +
+ +
+ {user.organizations.map((org) => { + const isCurrent = org.id === activeOrg?.id + return ( +
setActiveOrg(org.id)} + className={`p-3.5 rounded-xl border cursor-pointer transition-all flex items-center justify-between gap-3 ${ + isCurrent + ? 'border-brand bg-brand/5 ring-1 ring-brand/30' + : 'border-border bg-surface-raised hover:border-zinc-700' + }`} + > +
+
+ {org.name.slice(0, 2).toUpperCase()} +
+
+

{org.name}

+

{org.roles.join(', ')}

+
+
+ + {isCurrent && ( + + Ativo + + )} +
+ ) + })} +
+
+
+
+ ) +}