feat: restructure navbar and fix dead-end navigation across pages

Restructure the AppShell navbar for better UX grouping: rename
"Organizações" to "Equipe", consolidate Org Dashboard and admin
items inside the Equipe dropdown, add a persistent "+ Nova Tarefa"
CTA button, and replace the isolated logout button with an avatar
dropdown showing user name and email. Add "Buscar Organização"
to the Equipe dropdown for all roles. Mobile nav simplified to
Dashboard · Tarefas · + · Equipe.

Per-page fixes: remove non-functional chevron and dead links from
ProjectsPage; replace the PDF export button in DashboardPage with
a working "Copiar Relatório" clipboard button; remove dead info
links from OrganizationsPage; remove the broken support link from
LoginPage; add Lista/Quadro view toggle to TaskListPage header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gabriel Santana de Menezes 2026-05-09 21:03:43 -04:00
parent 6e77f7b194
commit 96e35dc21d
6 changed files with 244 additions and 144 deletions

View file

@ -2,17 +2,20 @@ import React, { useState } from "react";
import {
Layout,
ListTodo,
PlusCircle,
Plus,
Columns,
LogOut,
Settings,
Building2,
Clock,
ChevronDown,
Shield,
FolderKanban,
BarChart3,
PieChart,
Search,
User,
Users,
PlusCircle,
} from "lucide-react";
import { NavLink, Outlet, Navigate, Link } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
@ -43,7 +46,11 @@ export function AppShell() {
const [showOrgModal, setShowOrgModal] = useState(false);
const [showOrgSwitcher, setShowOrgSwitcher] = useState(false);
const [showTasksDropdown, setShowTasksDropdown] = useState(false);
const [showOrgDropdown, setShowOrgDropdown] = useState(false);
const [showTeamDropdown, setShowTeamDropdown] = useState(false);
const [showAvatarDropdown, setShowAvatarDropdown] = useState(false);
const isAdmin =
activeOrg?.roles.includes("OWNER") || activeOrg?.roles.includes("ADMIN");
React.useEffect(() => {
if (isAuthenticated && user && user.organizations.length === 0) {
@ -181,10 +188,11 @@ export function AppShell() {
)}
</div>
{/* Desktop Navigation */}
<nav className="hidden items-center gap-1 md:flex">
<NavLink to="/dashboard" className={navLinkClassName}>
<BarChart3 size={18} />
Dashboard
Meu Dashboard
</NavLink>
{/* Tarefas Dropdown */}
@ -252,90 +260,72 @@ export function AppShell() {
)}
</div>
{(activeOrg?.roles.includes("OWNER") ||
activeOrg?.roles.includes("ADMIN")) && (
{isAdmin && (
<NavLink to="/projects" className={navLinkClassName}>
<FolderKanban size={18} />
Projetos
</NavLink>
)}
{(activeOrg?.roles.includes("OWNER") ||
activeOrg?.roles.includes("ADMIN")) && (
<NavLink to="/org-dashboard" className={navLinkClassName}>
<PieChart size={18} />
Org Dashboard
</NavLink>
)}
{/* Organizações Dropdown */}
{/* Equipe Dropdown */}
<div className="relative">
<NavLink
to="/organizations"
className={({ isActive }) => {
const isRequestsActive =
window.location.pathname === "/requests";
const isAdminActive = window.location.pathname === "/admin";
const path = window.location.pathname;
const active =
isActive || isRequestsActive || isAdminActive;
isActive ||
path === "/requests" ||
path === "/admin" ||
path === "/org-dashboard" ||
path === "/organizations/join";
return cn(
"inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-semibold transition-all duration-200",
active || showOrgDropdown
active || showTeamDropdown
? "bg-brand text-white shadow-md shadow-brand/20"
: "text-text-muted hover:bg-surface-muted hover:text-text-main",
);
}}
onClick={(e) => {
e.preventDefault();
setShowOrgDropdown(!showOrgDropdown);
setShowTeamDropdown(!showTeamDropdown);
}}
>
<Building2 size={18} />
Organizações
<Users size={18} />
Equipe
<ChevronDown
size={14}
className={cn(
"transition-transform",
showOrgDropdown && "rotate-180",
showTeamDropdown && "rotate-180",
)}
/>
</NavLink>
{showOrgDropdown && (
{showTeamDropdown && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setShowOrgDropdown(false)}
onClick={() => setShowTeamDropdown(false)}
/>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="absolute right-0 mt-2 z-50 w-56 rounded-2xl border border-border-soft bg-surface p-2 shadow-xl shadow-brand/10"
className="absolute right-0 mt-2 z-50 w-60 rounded-2xl border border-border-soft bg-surface p-2 shadow-xl shadow-brand/10"
>
{isAdmin && (
<>
<NavLink
to="/organizations"
onClick={() => setShowOrgDropdown(false)}
to="/org-dashboard"
onClick={() => setShowTeamDropdown(false)}
className={dropdownItemClassName}
>
<Building2 size={18} />
Minhas Organizações
<PieChart size={18} />
Dashboard da Org
</NavLink>
<NavLink
to="/requests"
onClick={() => setShowOrgDropdown(false)}
className={(props) =>
cn(dropdownItemClassName(props), "mt-1")
}
>
<Clock size={18} />
Minhas Solicitações
</NavLink>
{activeOrg?.roles.some(
(role) => role === "OWNER" || role === "ADMIN",
) && (
<NavLink
to="/admin"
onClick={() => setShowOrgDropdown(false)}
onClick={() => setShowTeamDropdown(false)}
className={(props) =>
cn(dropdownItemClassName(props), "mt-1")
}
@ -343,7 +333,37 @@ export function AppShell() {
<Settings size={18} />
Solicitações Pendentes
</NavLink>
<div className="my-2 border-t border-border-soft" />
</>
)}
<NavLink
to="/organizations"
onClick={() => setShowTeamDropdown(false)}
className={dropdownItemClassName}
>
<Building2 size={18} />
Minhas Organizações
</NavLink>
<NavLink
to="/requests"
onClick={() => setShowTeamDropdown(false)}
className={(props) =>
cn(dropdownItemClassName(props), "mt-1")
}
>
<Clock size={18} />
Minhas Solicitações
</NavLink>
<NavLink
to="/organizations/join"
onClick={() => setShowTeamDropdown(false)}
className={(props) =>
cn(dropdownItemClassName(props), "mt-1")
}
>
<Search size={18} />
Buscar Organização
</NavLink>
</motion.div>
</>
)}
@ -351,16 +371,60 @@ export function AppShell() {
</nav>
</div>
<div className="flex items-center gap-4">
<div className="hidden h-8 w-px bg-border-soft md:block" />
{/* Right Actions */}
<div className="flex items-center gap-3">
<button
onClick={logout}
className="ml-2 p-2 text-text-muted hover:text-danger transition-colors"
title="Sair"
<Link
to="/new"
className="hidden md:inline-flex items-center gap-2 rounded-xl bg-brand px-4 py-2 text-sm font-bold text-white shadow-md shadow-brand/20 transition-all hover:scale-[1.02] hover:bg-brand-dark active:scale-95"
>
<LogOut size={18} />
<Plus size={16} />
Nova Tarefa
</Link>
<div className="hidden h-8 w-px bg-border-soft md:block" />
{/* Avatar Dropdown */}
<div className="relative">
<button
onClick={() => setShowAvatarDropdown(!showAvatarDropdown)}
className="flex h-9 w-9 items-center justify-center rounded-full bg-brand/10 text-brand hover:bg-brand hover:text-white transition-all"
title={user?.name}
>
<User size={18} />
</button>
{showAvatarDropdown && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setShowAvatarDropdown(false)}
/>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="absolute right-0 mt-2 z-50 w-56 rounded-2xl border border-border-soft bg-surface p-2 shadow-xl shadow-brand/10"
>
<div className="px-3 py-3 border-b border-border-soft mb-1">
<p className="text-sm font-bold text-text-main truncate">
{user?.name}
</p>
<p className="text-xs text-text-muted truncate mt-0.5">
{user?.email}
</p>
</div>
<button
onClick={() => {
setShowAvatarDropdown(false);
logout();
}}
className="flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-bold text-text-muted hover:bg-danger/5 hover:text-danger transition-all"
>
<LogOut size={16} />
Sair
</button>
</motion.div>
</>
)}
</div>
</div>
</div>
@ -383,7 +447,7 @@ export function AppShell() {
}
>
<BarChart3 size={20} />
<span className="text-[10px]">Dash</span>
<span className="text-[10px]">Dashboard</span>
</NavLink>
{/* Mobile Tarefas Group */}
@ -437,80 +501,75 @@ export function AppShell() {
</>
)}
</div>
{(activeOrg?.roles.includes("OWNER") ||
activeOrg?.roles.includes("ADMIN")) && (
<NavLink to="/projects" className={navLinkClassName}>
<FolderKanban size={18} />
Projetos
</NavLink>
)}
{(activeOrg?.roles.includes("OWNER") ||
activeOrg?.roles.includes("ADMIN")) && (
<NavLink
to="/org-dashboard"
className={({ isActive }) =>
cn(navLinkClassName({ isActive }), "flex-col gap-0.5 py-1")
}
{/* Mobile + New Task */}
<Link
to="/new"
className={cn(
navLinkClassName({ isActive: window.location.pathname === "/new" }),
"flex-col gap-0.5 py-1",
)}
>
<PieChart size={20} />
<span className="text-[10px]">Org</span>
</NavLink>
)}
<Plus size={20} />
<span className="text-[10px]">Nova</span>
</Link>
{/* Mobile Organizações Group */}
{/* Mobile Equipe Group */}
<div className="relative">
<button
onClick={() => setShowOrgDropdown(!showOrgDropdown)}
onClick={() => setShowTeamDropdown(!showTeamDropdown)}
className={cn(
navLinkClassName({
isActive:
window.location.pathname === "/organizations" ||
window.location.pathname === "/requests" ||
window.location.pathname === "/admin",
isActive: [
"/organizations",
"/requests",
"/admin",
"/org-dashboard",
"/organizations/join",
"/projects",
].includes(window.location.pathname),
}),
"flex-col gap-0.5 py-1",
)}
>
<Building2 size={20} />
<span className="text-[10px]">Org</span>
<Users size={20} />
<span className="text-[10px]">Equipe</span>
</button>
{showOrgDropdown && (
{showTeamDropdown && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setShowOrgDropdown(false)}
onClick={() => setShowTeamDropdown(false)}
/>
<motion.div
initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
className="absolute bottom-full left-1/2 -translate-x-1/2 mb-4 z-50 w-48 rounded-2xl border border-border-soft bg-surface p-1 shadow-2xl"
className="absolute bottom-full right-0 mb-4 z-50 w-52 rounded-2xl border border-border-soft bg-surface p-1 shadow-2xl"
>
{isAdmin && (
<>
<NavLink
to="/organizations"
onClick={() => setShowOrgDropdown(false)}
to="/projects"
onClick={() => setShowTeamDropdown(false)}
className={dropdownItemClassName}
>
<Building2 size={18} />
Minhas Orgs
<FolderKanban size={18} />
Projetos
</NavLink>
<NavLink
to="/requests"
onClick={() => setShowOrgDropdown(false)}
to="/org-dashboard"
onClick={() => setShowTeamDropdown(false)}
className={(props) =>
cn(dropdownItemClassName(props), "mt-1")
}
>
<Clock size={18} />
Pedidos
<PieChart size={18} />
Dashboard da Org
</NavLink>
{activeOrg?.roles.some(
(role) => role === "OWNER" || role === "ADMIN",
) && (
<NavLink
to="/admin"
onClick={() => setShowOrgDropdown(false)}
onClick={() => setShowTeamDropdown(false)}
className={(props) =>
cn(dropdownItemClassName(props), "mt-1")
}
@ -518,7 +577,37 @@ export function AppShell() {
<Settings size={18} />
Gestão
</NavLink>
<div className="my-1 border-t border-border-soft" />
</>
)}
<NavLink
to="/organizations"
onClick={() => setShowTeamDropdown(false)}
className={dropdownItemClassName}
>
<Building2 size={18} />
Minhas Orgs
</NavLink>
<NavLink
to="/requests"
onClick={() => setShowTeamDropdown(false)}
className={(props) =>
cn(dropdownItemClassName(props), "mt-1")
}
>
<Clock size={18} />
Pedidos
</NavLink>
<NavLink
to="/organizations/join"
onClick={() => setShowTeamDropdown(false)}
className={(props) =>
cn(dropdownItemClassName(props), "mt-1")
}
>
<Search size={18} />
Buscar Org
</NavLink>
</motion.div>
</>
)}

View file

@ -10,7 +10,8 @@ import {
Layout,
FileText,
Filter,
Download,
Copy,
Check,
AlertCircle,
Loader2,
History,
@ -30,6 +31,7 @@ export const DashboardPage: React.FC = () => {
const [startDate, setStartDate] = useState(startOfMonth(new Date()));
const [endDate, setEndDate] = useState(endOfMonth(new Date()));
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const fetchDashboardData = async () => {
setIsLoading(true);
@ -57,6 +59,24 @@ export const DashboardPage: React.FC = () => {
return `${h}h ${m}m`;
};
const handleCopyReport = () => {
if (!data) return;
const header = "Tarefa\tProjeto\tConcluída em\tStatus";
const rows = data.finishedTasksInPeriod.map((task) =>
[
task.title,
task.project?.name ?? "-",
task.finishedAt
? format(new Date(task.finishedAt), "dd/MM/yyyy HH:mm")
: "-",
"Finalizado",
].join("\t"),
);
navigator.clipboard.writeText([header, ...rows].join("\n"));
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="space-y-10 pb-20">
{/* Header & Filters */}
@ -199,10 +219,6 @@ export const DashboardPage: React.FC = () => {
)
: "0h 0m"}
</div>
<button className="mt-6 inline-flex items-center gap-2 text-xs font-black uppercase tracking-widest bg-white/10 hover:bg-white/20 px-4 py-2 rounded-xl transition-all">
<Download size={14} />
Exportar Relatório
</button>
</div>
<TrendingUp
size={160}
@ -324,9 +340,12 @@ export const DashboardPage: React.FC = () => {
Relatório de Entregas
</h2>
</div>
<button className="flex items-center gap-2 text-sm font-bold text-brand hover:underline bg-brand/5 px-4 py-2 rounded-xl transition-all">
<Download size={16} />
Baixar PDF
<button
onClick={handleCopyReport}
className="flex items-center gap-2 text-sm font-bold text-brand bg-brand/5 px-4 py-2 rounded-xl transition-all hover:bg-brand/10 active:scale-95"
>
{copied ? <Check size={16} /> : <Copy size={16} />}
{copied ? "Copiado!" : "Copiar Relatório"}
</button>
</div>

View file

@ -106,13 +106,10 @@ export const LoginPage: React.FC = () => {
</button>
</form>
<div className="mt-10 pt-8 border-t border-border-soft text-center space-y-4">
<div className="mt-10 pt-8 border-t border-border-soft text-center">
<p className="text-text-muted text-sm font-medium">
Não tem uma conta? <Link to="/register" className="text-brand font-bold hover:underline">Criar conta</Link>
</p>
<p className="text-text-muted text-sm font-medium">
Precisa de ajuda? <span className="text-brand font-bold cursor-pointer hover:underline">Falar com suporte</span>
</p>
</div>
</div>
</motion.div>

View file

@ -7,7 +7,6 @@ import {
Users,
Settings,
ArrowRight,
ExternalLink,
ShieldCheck,
UserPlus,
X,
@ -238,10 +237,6 @@ export function OrganizationsPage() {
<p className="mt-3 text-text-muted leading-relaxed font-medium">
Organizações são espaços isolados para seus projetos e tarefas. Você pode alternar facilmente entre elas e gerenciar diferentes equipes.
</p>
<a href="#" className="mt-6 inline-flex items-center gap-1.5 text-sm font-bold text-accent hover:underline">
Entender hierarquia
<ExternalLink size={14} />
</a>
</div>
<div className="rounded-[2rem] border border-border-soft bg-surface p-8">
@ -252,10 +247,6 @@ export function OrganizationsPage() {
<p className="mt-3 text-text-muted leading-relaxed font-medium">
Crie uma organização em segundos e comece a convidar seus membros. O BeeTask facilita a colaboração desde o primeiro dia.
</p>
<a href="#" className="mt-6 inline-flex items-center gap-1.5 text-sm font-bold text-brand hover:underline">
Dicas de onboarding
<ExternalLink size={14} />
</a>
</div>
</div>

View file

@ -4,15 +4,12 @@ import {
FolderKanban,
Plus,
Search,
Building2,
ArrowRight,
Loader2,
X,
Target,
Rocket,
ShieldCheck,
ExternalLink,
ChevronRight
} from "lucide-react";
import { useAuth } from "../contexts/AuthContext";
import { projectService, type Project } from "../services/projectService";
@ -146,7 +143,7 @@ export const ProjectsPage: React.FC = () => {
</span>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="mt-8 flex items-center">
<div className="flex -space-x-2">
{[1, 2, 3].map(i => (
<div key={i} className="h-8 w-8 rounded-full border-2 border-surface bg-surface-muted flex items-center justify-center text-[10px] font-bold text-text-muted">
@ -154,9 +151,6 @@ export const ProjectsPage: React.FC = () => {
</div>
))}
</div>
<button className="flex h-10 w-10 items-center justify-center rounded-xl bg-app-bg text-text-muted transition-all hover:bg-brand hover:text-white">
<ChevronRight size={20} />
</button>
</div>
</div>
@ -233,10 +227,6 @@ export const ProjectsPage: React.FC = () => {
<p className="mt-3 text-text-muted leading-relaxed font-medium">
Cada projeto pode ser encarado como um objetivo específico. Use-os para separar demandas de diferentes clientes ou departamentos da sua organização.
</p>
<a href="#" className="mt-6 inline-flex items-center gap-1.5 text-sm font-bold text-brand hover:underline">
Melhores práticas de organização
<ExternalLink size={14} />
</a>
</div>
<div className="rounded-[2rem] border border-border-soft bg-surface p-8">
@ -247,10 +237,6 @@ export const ProjectsPage: React.FC = () => {
<p className="mt-3 text-text-muted leading-relaxed font-medium">
Projetos herdam as permissões da organização. Apenas membros autorizados podem visualizar e interagir com as tarefas de cada projeto.
</p>
<a href="#" className="mt-6 inline-flex items-center gap-1.5 text-sm font-bold text-accent hover:underline">
Ver detalhes de visibilidade
<ExternalLink size={14} />
</a>
</div>
</div>

View file

@ -4,7 +4,9 @@ import {
Calendar,
CheckCircle2,
Clock,
Columns,
Filter,
List,
Plus,
RefreshCw,
Search,
@ -107,6 +109,21 @@ export function TaskListPage() {
Visualize e organize suas atividades em tempo real.
</p>
</div>
<div className="flex items-center gap-3">
{/* View toggle */}
<div className="flex items-center rounded-xl border border-border-soft bg-surface p-1 shadow-sm">
<span className="inline-flex items-center gap-1.5 rounded-lg bg-brand px-3 py-2 text-xs font-bold text-white">
<List size={14} />
Lista
</span>
<Link
to="/board"
className="inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-xs font-bold text-text-muted transition-all hover:bg-surface-muted hover:text-text-main"
>
<Columns size={14} />
Quadro
</Link>
</div>
<Link
to="/new"
className="inline-flex items-center justify-center gap-2 rounded-xl bg-brand px-6 py-3 text-sm font-bold text-white shadow-lg shadow-brand/20 transition-all hover:scale-[1.02] hover:bg-brand-strong active:scale-95"
@ -115,6 +132,7 @@ export function TaskListPage() {
Criar Tarefa
</Link>
</div>
</div>