+ {/* Header */}
+
+
+
+
+ Categorias & Tags
+
+
+ {activeOrg?.name || 'Workspace'}
+
+
+
+ Classifique suas tarefas por tipos de trabalho (Bug, Feature, Reunião, Design) para métricas precisas.
+
+
+
+
+
+
+
+
+
+
+ Nova Categoria
+
+
+
+
+ {error && (
+
+
+
setError(null)}
+ className="text-rose-400 hover:text-rose-200 text-xs font-bold"
+ >
+ Fechar
+
+
+ )}
+
+ {/* Search Bar */}
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="Buscar categoria por nome..."
+ className="w-full pl-9 pr-3 py-2 rounded-xl border border-border bg-surface-raised text-xs text-foreground placeholder-muted/60 focus:outline-none focus:ring-2 focus:ring-accent/40 focus:border-accent transition-all"
+ />
+
+
+ {filteredCategories.length} {filteredCategories.length === 1 ? 'categoria' : 'categorias'}
+
+
+
+ {/* Grid of Categories */}
+ {loading ? (
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+ ) : filteredCategories.length === 0 ? (
+
+
+
+
+
+ Nenhuma categoria cadastrada
+
+
+ {searchQuery
+ ? 'Nenhuma categoria encontrada com este termo.'
+ : 'Categorias permitem segmentar seus fluxos de trabalho e auditar onde o tempo é consumido.'}
+
+
+
+ Criar Primeira Categoria
+
+
+ ) : (
+
+ {filteredCategories.map((category) => {
+ const stats = categoryStats.get(category.id) || {
+ totalTasks: 0,
+ totalSeconds: 0,
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {category.name}
+
+
+ ID #{category.id}
+
+
+
+
+
handleOpenEditModal(category)}
+ className="p-1.5 rounded-lg text-muted hover:text-foreground hover:bg-surface-raised transition-colors"
+ title="Editar categoria"
+ >
+
+
+
+
+ {/* Metrics Bar */}
+
+
+
+
+
Tarefas
+
+ {stats.totalTasks} associadas
+
+
+
+
+
+
+
Tempo Total
+
+ {formatSecondsToHuman(stats.totalSeconds)}
+
+
+
+
+
+
+ {/* Footer Link */}
+
+
+
navigate('/tasks')}
+ className="inline-flex items-center gap-1 text-xs font-semibold text-accent hover:underline"
+ >
+ Ver no Quadro
+
+
+
+
+ )
+ })}
+
+ )}
+
+ {/* Modal Criar/Editar Categoria */}
+ {isModalOpen && (
+
+
setIsModalOpen(false)}
+ />
+
+
+
+
+
+
+
+
+ {editingCategory ? 'Editar Categoria' : 'Nova Categoria'}
+
+
+ {editingCategory
+ ? 'Atualize o nome ou cor da tag.'
+ : 'Crie uma categoria para agrupar tipos de trabalho.'}
+
+
+
+
setIsModalOpen(false)}
+ className="p-1.5 rounded-lg text-muted hover:text-foreground hover:bg-surface-raised transition-colors"
+ >
+
+
+
+
+
+
+
+ )}
+
+ )
+}
diff --git a/new-frontend/src/pages/ProjectsPage.tsx b/new-frontend/src/pages/ProjectsPage.tsx
new file mode 100644
index 0000000..ff935f0
--- /dev/null
+++ b/new-frontend/src/pages/ProjectsPage.tsx
@@ -0,0 +1,436 @@
+import React, { useState, useEffect, useCallback, useMemo } from 'react'
+import { useNavigate } from 'react-router-dom'
+import {
+ FolderKanban,
+ Plus,
+ Search,
+ RefreshCw,
+ Clock,
+ CheckCircle2,
+ ListTodo,
+ Sparkles,
+ ArrowRight,
+ Folder,
+ X,
+ Loader2,
+ AlertCircle,
+} from 'lucide-react'
+import { useAuth } from '../contexts/AuthContext'
+import { projectService } from '../services/projectService'
+import { taskService } from '../services/taskService'
+import type { Project, Task } from '../types/task'
+import { calculateTaskTotalSeconds, formatSecondsToHuman } from '../lib/timeUtils'
+
+const COLOR_PRESETS = [
+ '#0f766e', // Teal / Brand
+ '#d97706', // Amber / Accent
+ '#10b981', // Emerald
+ '#3b82f6', // Blue
+ '#6366f1', // Indigo
+ '#8b5cf6', // Violet
+ '#ec4899', // Pink
+ '#f43f5e', // Rose
+ '#14b8a6', // Cyan
+ '#64748b', // Slate
+]
+
+export const ProjectsPage: React.FC = () => {
+ const { activeOrg } = useAuth()
+ const navigate = useNavigate()
+
+ const [projects, setProjects] = useState
([])
+ const [tasks, setTasks] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [refreshing, setRefreshing] = useState(false)
+ const [searchQuery, setSearchQuery] = useState('')
+ const [error, setError] = useState(null)
+
+ // Modal
+ const [isModalOpen, setIsModalOpen] = useState(false)
+ const [name, setName] = useState('')
+ const [color, setColor] = useState(COLOR_PRESETS[0])
+ const [submitting, setSubmitting] = useState(false)
+
+ const activeOrgId = activeOrg?.id
+
+ const loadData = useCallback(async () => {
+ if (!activeOrgId) return
+ try {
+ setRefreshing(true)
+ const [projsRes, tasksRes] = await Promise.all([
+ projectService.listProjects(activeOrgId),
+ taskService.listTasks({ organizationId: activeOrgId }).catch(() => []),
+ ])
+ setProjects(projsRes || [])
+ setTasks(tasksRes || [])
+ } catch (err: unknown) {
+ console.error(err)
+ setError('Falha ao carregar projetos.')
+ } finally {
+ setLoading(false)
+ setRefreshing(false)
+ }
+ }, [activeOrgId])
+
+ useEffect(() => {
+ loadData()
+ }, [loadData])
+
+ // Aggregate project statistics
+ const projectStats = useMemo(() => {
+ const statsMap = new Map<
+ number,
+ { totalTasks: number; completedTasks: number; totalSeconds: number }
+ >()
+
+ tasks.forEach((task) => {
+ if (!task.project?.id) return
+ const pId = task.project.id
+ const current = statsMap.get(pId) || {
+ totalTasks: 0,
+ completedTasks: 0,
+ totalSeconds: 0,
+ }
+
+ current.totalTasks += 1
+ if (task.status === 'COMPLETED') {
+ current.completedTasks += 1
+ }
+ current.totalSeconds += calculateTaskTotalSeconds(task)
+ statsMap.set(pId, current)
+ })
+
+ return statsMap
+ }, [tasks])
+
+ const filteredProjects = useMemo(() => {
+ if (!searchQuery.trim()) return projects
+ const q = searchQuery.toLowerCase()
+ return projects.filter((p) => p.name.toLowerCase().includes(q))
+ }, [projects, searchQuery])
+
+ const handleCreateProject = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!activeOrgId || !name.trim()) return
+
+ setSubmitting(true)
+ setError(null)
+ try {
+ const created = await projectService.createProject(activeOrgId, {
+ name: name.trim(),
+ color,
+ icon: 'folder',
+ })
+ setProjects((prev) => [...prev, created])
+ setIsModalOpen(false)
+ setName('')
+ setColor(COLOR_PRESETS[0])
+ } catch (err: unknown) {
+ console.error(err)
+ setError(err instanceof Error ? err.message : 'Falha ao criar projeto.')
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Projetos
+
+
+ {activeOrg?.name || 'Workspace'}
+
+
+
+ Organize suas entregas em iniciativas, acompanhe o progresso e o tempo dedicado.
+
+
+
+
+
+
+
+
+
setIsModalOpen(true)}
+ className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-brand text-white font-semibold text-xs hover:bg-brand-hover shadow-md shadow-brand/20 transition-all"
+ >
+
+ Novo Projeto
+
+
+
+
+ {error && (
+
+
+
setError(null)}
+ className="text-rose-400 hover:text-rose-200 text-xs font-bold"
+ >
+ Fechar
+
+
+ )}
+
+ {/* Search Bar */}
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="Buscar projeto por nome..."
+ className="w-full pl-9 pr-3 py-2 rounded-xl border border-border bg-surface-raised text-xs text-foreground placeholder-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/40 focus:border-brand transition-all"
+ />
+
+
+ {filteredProjects.length} {filteredProjects.length === 1 ? 'projeto' : 'projetos'}
+
+
+
+ {/* Grid of Projects */}
+ {loading ? (
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+ ) : filteredProjects.length === 0 ? (
+
+
+
+
+
+ Nenhum projeto cadastrado
+
+
+ {searchQuery
+ ? 'Nenhum projeto encontrado com este termo de busca.'
+ : 'Projetos ajudam a agrupar demandas por produtos, clientes ou sprints.'}
+
+
setIsModalOpen(true)}
+ className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-brand text-white text-xs font-semibold hover:bg-brand-hover shadow-md shadow-brand/20 transition-all"
+ >
+
+ Criar Primeiro Projeto
+
+
+ ) : (
+
+ {filteredProjects.map((project) => {
+ const stats = projectStats.get(project.id) || {
+ totalTasks: 0,
+ completedTasks: 0,
+ totalSeconds: 0,
+ }
+ const progress =
+ stats.totalTasks > 0
+ ? Math.round((stats.completedTasks / stats.totalTasks) * 100)
+ : 0
+
+ return (
+
+
+ {/* Top Bar with Icon & Color badge */}
+
+
+
+
+
+
+
+ {project.name}
+
+
+ ID #{project.id}
+
+
+
+
+
+ {/* Metrics Bar */}
+
+
+
+
+
Tarefas
+
+ {stats.totalTasks} ({stats.completedTasks} concl.)
+
+
+
+
+
+
+
Tempo Total
+
+ {formatSecondsToHuman(stats.totalSeconds)}
+
+
+
+
+
+ {/* Progress Bar */}
+
+
+ Conclusão
+ {progress}%
+
+
+
+
+
+ {/* Footer Link */}
+
+
+
+ {stats.completedTasks} entregues
+
+
navigate('/tasks')}
+ className="inline-flex items-center gap-1 text-xs font-semibold text-brand hover:underline"
+ >
+ Ver Tarefas
+
+
+
+
+ )
+ })}
+
+ )}
+
+ {/* Modal Novo Projeto */}
+ {isModalOpen && (
+
+
setIsModalOpen(false)}
+ />
+
+
+
+
+
+
+
+
Novo Projeto
+
Crie um projeto para agrupar demandas.
+
+
+
setIsModalOpen(false)}
+ className="p-1.5 rounded-lg text-muted hover:text-foreground hover:bg-surface-raised transition-colors"
+ >
+
+
+
+
+
+
+
+ )}
+
+ )
+}
diff --git a/new-frontend/src/services/categoryService.ts b/new-frontend/src/services/categoryService.ts
index bb256aa..2ea055a 100644
--- a/new-frontend/src/services/categoryService.ts
+++ b/new-frontend/src/services/categoryService.ts
@@ -12,4 +12,15 @@ export const categoryService = {
body: data,
})
},
+
+ async updateCategory(
+ orgId: number,
+ id: number,
+ data: { name: string; color?: string; icon?: string }
+ ): Promise
{
+ return apiFetch(`/organizations/${orgId}/categories/${id}`, {
+ method: 'PUT',
+ body: data,
+ })
+ },
}