diff --git a/new-frontend/src/App.tsx b/new-frontend/src/App.tsx index b943950..c92cccd 100644 --- a/new-frontend/src/App.tsx +++ b/new-frontend/src/App.tsx @@ -2,15 +2,42 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import { AuthProvider } from './contexts/AuthContext' import { LandingPage } from './pages/LandingPage' import { AuthPage } from './pages/AuthPage' +import { ProtectedRoute } from './components/layout/ProtectedRoute' +import { AppShell } from './components/layout/AppShell' +import { DashboardPage } from './pages/DashboardPage' +import { OrganizationsPage } from './pages/OrganizationsPage' export function App() { return ( + {/* Rotas Públicas */} } /> } /> } /> + + {/* Rotas Protegidas dentro do AppShell */} + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Fallback */} } /> diff --git a/new-frontend/src/components/layout/AppShell.tsx b/new-frontend/src/components/layout/AppShell.tsx new file mode 100644 index 0000000..482dc1a --- /dev/null +++ b/new-frontend/src/components/layout/AppShell.tsx @@ -0,0 +1,26 @@ +import { useState } from 'react' +import { Outlet } from 'react-router-dom' +import { Sidebar } from './Sidebar' +import { Header } from './Header' + +export const AppShell = () => { + const [isMobileOpen, setIsMobileOpen] = useState(false) + + return ( +
+ {/* Sidebar Navigation */} + setIsMobileOpen(false)} + /> + + {/* Main Content Area */} +
+
setIsMobileOpen(true)} /> +
+ +
+
+
+ ) +} diff --git a/new-frontend/src/components/layout/Header.tsx b/new-frontend/src/components/layout/Header.tsx new file mode 100644 index 0000000..8b11bf8 --- /dev/null +++ b/new-frontend/src/components/layout/Header.tsx @@ -0,0 +1,273 @@ +import { useState, useRef, useEffect } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { + Card, + Button, + Chip, + ChipLabel, + Avatar, + AvatarFallback, + AvatarImage, +} from '@heroui/react' +import { + Building2, + ChevronDown, + Plus, + Clock, + User, + LogOut, + Settings, + PlusCircle, + Menu as MenuIcon, + Check, +} from 'lucide-react' +import { useAuth } from '../../contexts/AuthContext' + +interface HeaderProps { + onToggleMobileSidebar: () => void +} + +export const Header = ({ onToggleMobileSidebar }: HeaderProps) => { + const { user, activeOrg, setActiveOrg, logout } = useAuth() + const [showOrgDropdown, setShowOrgDropdown] = useState(false) + const [showUserDropdown, setShowUserDropdown] = useState(false) + + const orgRef = useRef(null) + const userRef = useRef(null) + const navigate = useNavigate() + + // Fechar dropdowns ao clicar fora + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (orgRef.current && !orgRef.current.contains(e.target as Node)) { + setShowOrgDropdown(false) + } + if (userRef.current && !userRef.current.contains(e.target as Node)) { + setShowUserDropdown(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + + const userInitials = user?.name + ? user.name + .split(' ') + .slice(0, 2) + .map((n) => n[0]) + .join('') + .toUpperCase() + : 'U' + + return ( +
+ {/* Left Area: Mobile Menu Trigger + Workspace Switcher */} +
+ + + {/* Workspace Selector Dropdown */} +
+ + + {/* Org Dropdown Menu */} + {showOrgDropdown && ( + +
+ + Workspaces + + + {user?.organizations?.length || 0} + +
+ +
+ {user?.organizations?.map((org) => { + const isActive = org.id === activeOrg?.id + return ( + + ) + })} +
+ +
+ setShowOrgDropdown(false)} + className="flex items-center gap-2 p-2 rounded-xl text-xs font-semibold text-brand hover:bg-brand-soft/50 transition-colors" + > + + Gerenciar / Criar Workspace + +
+
+ )} +
+
+ + {/* Right Area: Active Tracker Pill + Action Button + User Profile */} +
+ {/* Active Timer Pill Banner */} +
+
+ + + Modo Foco Ativo + + + v3.0 + +
+ + {/* Create Task Button */} + + + {/* User Profile Menu Dropdown */} +
+ + + {/* User Dropdown Menu */} + {showUserDropdown && ( + +
+

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

+

+ {user?.email} +

+
+ + {activeOrg?.roles[0] || 'MEMBRO'} + + + {activeOrg?.name} + +
+
+ +
+ setShowUserDropdown(false)} + className="flex items-center gap-2.5 p-2 rounded-xl text-xs font-semibold text-text-muted hover:text-text-main hover:bg-surface-muted transition-colors" + > + + Meu Perfil & Avatar + + + setShowUserDropdown(false)} + className="flex items-center gap-2.5 p-2 rounded-xl text-xs font-semibold text-text-muted hover:text-text-main hover:bg-surface-muted transition-colors" + > + + Configurações do Workspace + +
+ +
+ +
+
+ )} +
+
+
+ ) +} diff --git a/new-frontend/src/components/layout/ProtectedRoute.tsx b/new-frontend/src/components/layout/ProtectedRoute.tsx new file mode 100644 index 0000000..f8d0127 --- /dev/null +++ b/new-frontend/src/components/layout/ProtectedRoute.tsx @@ -0,0 +1,32 @@ +import { Navigate, useLocation } from 'react-router-dom' +import { useAuth } from '../../contexts/AuthContext' + +export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { + const { user, isAuthenticated, isLoading } = useAuth() + const location = useLocation() + + if (isLoading) { + return ( +
+
+ 🐝 +
+
+
+ Carregando seu workspace... +
+
+ ) + } + + if (!isAuthenticated) { + return + } + + // Se logado mas sem organizações cadastradas, direciona para o onboarding/criação de workspace + if (user && user.organizations.length === 0 && location.pathname !== '/organizations') { + return + } + + return <>{children} +} diff --git a/new-frontend/src/components/layout/Sidebar.tsx b/new-frontend/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..61cd747 --- /dev/null +++ b/new-frontend/src/components/layout/Sidebar.tsx @@ -0,0 +1,152 @@ +import { NavLink } from 'react-router-dom' +import { + LayoutDashboard, + ListTodo, + FolderKanban, + Tags, + BarChart3, + Users, + Settings, + X, + Building2, +} from 'lucide-react' +import { Chip, ChipLabel } from '@heroui/react' +import { useAuth } from '../../contexts/AuthContext' + +interface SidebarProps { + isMobileOpen: boolean + onCloseMobile: () => void +} + +export const Sidebar = ({ isMobileOpen, onCloseMobile }: SidebarProps) => { + const { activeOrg } = useAuth() + + const navItems = [ + { + group: 'Principal', + links: [ + { to: '/dashboard', label: 'Meu Dashboard', icon: }, + { to: '/tasks', label: 'Tarefas & Board', icon: }, + ], + }, + { + group: 'Estrutura', + links: [ + { to: '/projects', label: 'Projetos', icon: }, + { to: '/categories', label: 'Categorias', icon: }, + ], + }, + { + group: 'Gestão & Equipe', + links: [ + { to: '/analytics', label: 'Analytics da Org', icon: }, + { to: '/members', label: 'Membros & Squads', icon: }, + { to: '/organization/settings', label: 'Configurações', icon: }, + ], + }, + ] + + const navContent = ( +
+ {/* Brand & Logo */} +
+ +
+ 🐝 +
+
+
+ + BeeTask + + + v3.0 + +
+

+ Workspace Ágil +

+
+
+ + {/* Mobile close button */} + +
+ + {/* Nav List */} +
+ {navItems.map((group, idx) => ( +
+

+ {group.group} +

+
+ {group.links.map((link) => ( + + `flex items-center gap-3 px-3 py-2.5 rounded-xl text-xs font-semibold transition-all ${ + isActive + ? 'bg-brand text-white shadow-md shadow-brand/20 font-bold' + : 'text-text-muted hover:text-text-main hover:bg-surface-muted' + }` + } + > + {link.icon} + {link.label} + + ))} +
+
+ ))} +
+ + {/* Bottom Workspace Card Info */} +
+
+
+ +
+
+

+ {activeOrg?.name || 'Workspace Padrão'} +

+ + {activeOrg?.roles[0] || 'Ativo'} + +
+
+
+
+ ) + + return ( + <> + {/* Desktop Sidebar (Permanent) */} + + + {/* Mobile Sidebar (Drawer Overlay) */} + {isMobileOpen && ( +
+
+
+ {navContent} +
+
+ )} + + ) +} diff --git a/new-frontend/src/pages/DashboardPage.tsx b/new-frontend/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..2c22277 --- /dev/null +++ b/new-frontend/src/pages/DashboardPage.tsx @@ -0,0 +1,374 @@ +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 { + Clock, + CheckCircle2, + FolderKanban, + Play, + Square, + Users, + TrendingUp, + Plus, + Sparkles, + ArrowRight, + Flame, +} from 'lucide-react' +import { useAuth } from '../contexts/AuthContext' + +export const DashboardPage = () => { + const { user, activeOrg } = useAuth() + + // Simulação de cronômetro local interativo + const [isRunning, setIsRunning] = useState(false) + const [timerSeconds, setTimerSeconds] = useState(1450) + + useEffect(() => { + let interval: ReturnType + if (isRunning) { + interval = setInterval(() => { + setTimerSeconds((prev) => prev + 1) + }, 1000) + } + return () => clearInterval(interval) + }, [isRunning]) + + 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')}` + } + + 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 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', + }, + ] + + return ( +
+ {/* Top Welcome Banner */} +
+
+
+

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

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

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

+
+ +
+ + + + + + +
+
+ + {/* KPI Stats Grid */} +
+ {kpis.map((kpi, idx) => ( + + +
+ + {kpi.label} + +
+ {kpi.icon} +
+
+
+

+ {kpi.value} +

+

+ {kpi.sub} +

+
+
+
+ ))} +
+ + {/* Main Grid: Active Task Tracker + Recent Tasks */} +
+ {/* Active Session & Pomodoro Card (Left 5 cols) */} +
+ + +
+
+ +
+
+ + Sessão em Foco + + + Time tracking em tempo real + +
+
+ + {isRunning ? "ATIVO" : "PAUSADO"} + +
+ + +
+
+ + Frontend v3 + + + Squad Core + +
+

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

+
+ + {/* Big Timer display */} +
+

+ Tempo na Tarefa +

+

+ {formatTimer(timerSeconds)} +

+
+ +
+ +
+
+ + + Sessão vinculada ao usuário + + {user?.email} + + +
+ + {/* Quick Shortcuts */} + +

+ Acesso Rápido +

+
+ + + Ver Projetos + + + + Métricas da Org + +
+
+
+ + {/* Recent Tasks List (Right 7 cols) */} +
+
+

+ + Tarefas Recentes da Sprint +

+ + Ver todas + + +
+ +
+ {recentTasks.map((task) => ( + +
+
+ + {task.project} + + + {task.category} + +
+

+ {task.title} +

+
+ +
+ + {task.time} + + + {task.status} + +
+
+ ))} +
+ + {/* Team Active Members Banner */} + +
+
+ +
+
+

+ Squad Ativo no Workspace +

+

+ 3 colaboradores com sessões registradas hoje +

+
+
+ +
+ + + GB + + + + + AL + + + + + MS + + +
+
+
+
+
+ ) +} diff --git a/new-frontend/src/pages/OrganizationsPage.tsx b/new-frontend/src/pages/OrganizationsPage.tsx new file mode 100644 index 0000000..937d297 --- /dev/null +++ b/new-frontend/src/pages/OrganizationsPage.tsx @@ -0,0 +1,242 @@ +import { useState, type FormEvent } from 'react' +import { useNavigate } from 'react-router-dom' +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Button, + Input, + Chip, + ChipLabel, + Alert, + AlertTitle, + AlertDescription, +} from '@heroui/react' +import { + Building2, + Plus, + ArrowRight, + Users, + ShieldCheck, +} from 'lucide-react' +import { useAuth } from '../contexts/AuthContext' +import { organizationService } from '../services/organizationService' + +export const OrganizationsPage = () => { + const { user, activeOrg, setActiveOrg, refreshUser } = useAuth() + const navigate = useNavigate() + + const [newOrgName, setNewOrgName] = useState('') + const [isCreating, setIsCreating] = useState(false) + const [errorMessage, setErrorMessage] = useState(null) + const [successMessage, setSuccessMessage] = useState(null) + + const handleCreateOrg = async (e: FormEvent) => { + e.preventDefault() + if (!newOrgName.trim()) return + + setIsCreating(true) + setErrorMessage(null) + setSuccessMessage(null) + + try { + const created = await organizationService.create(newOrgName.trim()) + await refreshUser() + setActiveOrg(created.id) + setSuccessMessage(`Workspace "${newOrgName}" criado com sucesso!`) + setNewOrgName('') + setTimeout(() => { + navigate('/dashboard') + }, 800) + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : 'Erro ao criar organização.' + setErrorMessage(msg) + } finally { + setIsCreating(false) + } + } + + const handleSelectOrg = (id: number) => { + setActiveOrg(id) + navigate('/dashboard') + } + + return ( +
+ {/* Page Header */} +
+
+ + Gestão de Workspaces Corporativos +
+

+ Seus Workspaces & Organizações +

+

+ Selecione uma organização ativa para acessar projetos, tarefas e métricas da equipe, + ou crie um novo workspace dedicado para sua empresa ou squad. +

+
+ + {/* Notifications */} + {errorMessage && ( + +
+ Não foi possível criar + {errorMessage} +
+
+ )} + + {successMessage && ( + +
+ Sucesso + {successMessage} +
+
+ )} + +
+ {/* Existing Organizations (7 cols) */} +
+
+

+ + Workspaces Vinculados ({user?.organizations?.length || 0}) +

+
+ + {user?.organizations && user.organizations.length > 0 ? ( +
+ {user.organizations.map((org) => { + const isActive = org.id === activeOrg?.id + return ( + handleSelectOrg(org.id)} + > +
+
+ {org.name.slice(0, 2).toUpperCase()} +
+
+
+

+ {org.name} +

+ {isActive && ( + + ATIVO + + )} +
+

+ Papel: {org.roles.join(', ')} +

+
+
+ + +
+ ) + })} +
+ ) : ( + +
+ +
+

+ Nenhum workspace encontrado +

+

+ Você ainda não faz parte de nenhuma organização. Crie o seu primeiro workspace ao lado para começar! +

+
+ )} +
+ + {/* Create New Workspace Form (5 cols) */} +
+ + +
+ +
+ + Criar Novo Workspace + + + Você será automaticamente configurado como OWNER. + +
+ + +
+
+ + setNewOrgName(e.target.value)} + placeholder="Ex: Minha Empresa Corp" + className="w-full px-3.5 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-xs text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent transition-all" + /> +
+ + +
+
+ +
+ + Multi-tenancy com isolamento de dados +
+
+
+
+
+ ) +} diff --git a/new-frontend/src/services/organizationService.ts b/new-frontend/src/services/organizationService.ts new file mode 100644 index 0000000..98531d4 --- /dev/null +++ b/new-frontend/src/services/organizationService.ts @@ -0,0 +1,42 @@ +import { apiFetch } from '../lib/api' + +export type CreateOrgResponse = { + id: number + name: string + ownerId: number +} + +export type SearchOrgOutput = { + id: number + name: string +} + +export type JoinRequestOutput = { + organizationId: number + organizationName: string + status: 'PENDING' | 'APPROVED' | 'REJECTED' + createdAt: string +} + +export const organizationService = { + async create(name: string): Promise { + return apiFetch('/organizations', { + method: 'POST', + body: { name }, + }) + }, + + async search(query: string): Promise { + return apiFetch(`/organizations/search?q=${encodeURIComponent(query)}`) + }, + + async requestJoin(organizationId: number): Promise { + return apiFetch(`/organizations/${organizationId}/join`, { + method: 'POST', + }) + }, + + async listMyRequests(): Promise { + return apiFetch('/organizations/my-requests') + }, +}