HEFESTO v1.0 - Sistema de Controle Orçamentário para Facilities
- Backend NestJS com 12 módulos - Frontend React com dashboard e gestão - Manuais técnico e de negócios (MD + PDF) - Workflow de aprovação com alçadas - RBAC com 6 perfis de acesso
This commit is contained in:
255
frontend/src/components/Layout.tsx
Normal file
255
frontend/src/components/Layout.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
FileText,
|
||||
Wallet,
|
||||
ClipboardList,
|
||||
Building2,
|
||||
BarChart3,
|
||||
Users,
|
||||
LogOut,
|
||||
Menu,
|
||||
X,
|
||||
Flame,
|
||||
ChevronLeft,
|
||||
Bell,
|
||||
Search
|
||||
} from 'lucide-react'
|
||||
import { User } from '../types'
|
||||
|
||||
interface NavItem {
|
||||
path: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
adminOnly?: boolean;
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ path: '/app', label: 'Dashboard', icon: <LayoutDashboard className="w-5 h-5" /> },
|
||||
{ path: '/app/demandas', label: 'Demandas', icon: <FileText className="w-5 h-5" /> },
|
||||
{ path: '/app/orcamentos', label: 'Orçamentos', icon: <Wallet className="w-5 h-5" /> },
|
||||
{ path: '/app/ordens-servico', label: 'Ordens de Serviço', icon: <ClipboardList className="w-5 h-5" /> },
|
||||
{ path: '/app/fornecedores', label: 'Fornecedores', icon: <Building2 className="w-5 h-5" /> },
|
||||
{ path: '/app/relatorios', label: 'Relatórios', icon: <BarChart3 className="w-5 h-5" /> },
|
||||
{ path: '/app/usuarios', label: 'Usuários', icon: <Users className="w-5 h-5" />, adminOnly: true },
|
||||
]
|
||||
|
||||
export default function Layout() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true)
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
useEffect(() => {
|
||||
const userData = localStorage.getItem('user')
|
||||
if (userData) {
|
||||
setUser(JSON.parse(userData))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
const isAdmin = user?.perfil?.toLowerCase() === 'admin' || user?.perfil?.toLowerCase() === 'administrador'
|
||||
|
||||
const filteredNavItems = navItems.filter(item => !item.adminOnly || isAdmin)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex">
|
||||
{/* Desktop Sidebar */}
|
||||
<aside
|
||||
className={`hidden lg:flex flex-col bg-secondary fixed h-screen transition-all duration-300 z-40 ${
|
||||
sidebarOpen ? 'w-64' : 'w-20'
|
||||
}`}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="p-4 flex items-center justify-between border-b border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-lg">
|
||||
<Flame className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
{sidebarOpen && (
|
||||
<span className="font-bold text-white text-xl tracking-tight">HEFESTO</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
className="p-2 rounded-lg text-white/60 hover:text-white hover:bg-white/10 transition-all"
|
||||
>
|
||||
<ChevronLeft className={`w-5 h-5 transition-transform ${!sidebarOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-4 space-y-2 overflow-y-auto">
|
||||
{filteredNavItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
end={item.path === '/app'}
|
||||
className={({ isActive }) => `
|
||||
flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200
|
||||
${isActive
|
||||
? 'bg-gradient-to-r from-primary to-accent text-white shadow-lg shadow-primary/30'
|
||||
: 'text-white/70 hover:text-white hover:bg-white/10'
|
||||
}
|
||||
${!sidebarOpen ? 'justify-center px-3' : ''}
|
||||
`}
|
||||
>
|
||||
{item.icon}
|
||||
{sidebarOpen && <span className="font-medium">{item.label}</span>}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* User section */}
|
||||
<div className="p-4 border-t border-white/10">
|
||||
{sidebarOpen ? (
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary/20 to-accent/20 flex items-center justify-center">
|
||||
<span className="text-white font-semibold">
|
||||
{user?.nome?.charAt(0).toUpperCase() || 'U'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white font-medium truncate">{user?.nome || 'Usuário'}</p>
|
||||
<p className="text-white/50 text-sm truncate">{user?.perfil || 'Perfil'}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={`flex items-center gap-3 w-full px-4 py-3 rounded-xl text-white/70 hover:text-white hover:bg-red-500/20 transition-all ${
|
||||
!sidebarOpen ? 'justify-center px-3' : ''
|
||||
}`}
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
{sidebarOpen && <span className="font-medium">Sair</span>}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
{mobileMenuOpen && (
|
||||
<div
|
||||
className="lg:hidden fixed inset-0 bg-black/50 z-40"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mobile Sidebar */}
|
||||
<aside
|
||||
className={`lg:hidden fixed inset-y-0 left-0 w-72 bg-secondary z-50 transform transition-transform duration-300 ${
|
||||
mobileMenuOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<div className="p-4 flex items-center justify-between border-b border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-accent flex items-center justify-center">
|
||||
<Flame className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<span className="font-bold text-white text-xl">HEFESTO</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="p-2 rounded-lg text-white/60 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="p-4 space-y-2">
|
||||
{filteredNavItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
end={item.path === '/app'}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className={({ isActive }) => `
|
||||
flex items-center gap-3 px-4 py-3 rounded-xl transition-all
|
||||
${isActive
|
||||
? 'bg-gradient-to-r from-primary to-accent text-white'
|
||||
: 'text-white/70 hover:text-white hover:bg-white/10'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{item.icon}
|
||||
<span className="font-medium">{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 border-t border-white/10">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary/20 to-accent/20 flex items-center justify-center">
|
||||
<span className="text-white font-semibold">
|
||||
{user?.nome?.charAt(0).toUpperCase() || 'U'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white font-medium truncate">{user?.nome || 'Usuário'}</p>
|
||||
<p className="text-white/50 text-sm truncate">{user?.perfil || 'Perfil'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-3 w-full px-4 py-3 rounded-xl text-white/70 hover:text-white hover:bg-red-500/20 transition-all"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
<span className="font-medium">Sair</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className={`flex-1 transition-all duration-300 ${sidebarOpen ? 'lg:ml-64' : 'lg:ml-20'}`}>
|
||||
{/* Header */}
|
||||
<header className="bg-white border-b border-border sticky top-0 z-30">
|
||||
<div className="flex items-center justify-between px-4 lg:px-6 h-16">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
className="lg:hidden p-2 rounded-lg text-gray hover:text-text hover:bg-gray-100 transition-all"
|
||||
>
|
||||
<Menu className="w-6 h-6" />
|
||||
</button>
|
||||
<div className="hidden sm:flex items-center gap-2 px-4 py-2 bg-gray-50 rounded-xl">
|
||||
<Search className="w-5 h-5 text-gray-light" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar..."
|
||||
className="bg-transparent border-none outline-none text-sm w-48 placeholder-gray-light"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="p-2 rounded-xl text-gray hover:text-primary hover:bg-primary/5 transition-all relative">
|
||||
<Bell className="w-5 h-5" />
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-primary rounded-full"></span>
|
||||
</button>
|
||||
<div className="hidden sm:flex items-center gap-2 pl-3 border-l border-border">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-accent flex items-center justify-center">
|
||||
<span className="text-white text-sm font-semibold">
|
||||
{user?.nome?.charAt(0).toUpperCase() || 'U'}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-text">{user?.nome?.split(' ')[0] || 'Usuário'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="p-4 lg:p-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user