383 lines
15 KiB
TypeScript
383 lines
15 KiB
TypeScript
import React, { useState, Fragment, useEffect, useRef } from "react";
|
|
import { Outlet, Link, useLocation } from "react-router-dom";
|
|
import { useAuth } from "../../contexts/AuthContext";
|
|
import {
|
|
BellIcon,
|
|
ChevronDownIcon,
|
|
MoonIcon,
|
|
SunIcon,
|
|
HomeIcon,
|
|
ScaleIcon,
|
|
UserGroupIcon,
|
|
UserIcon,
|
|
ClipboardDocumentCheckIcon,
|
|
DocumentTextIcon,
|
|
DocumentCheckIcon,
|
|
ArrowUpTrayIcon,
|
|
ShieldCheckIcon,
|
|
CurrencyDollarIcon,
|
|
CalculatorIcon,
|
|
} from "@heroicons/react/24/outline";
|
|
import { Menu, Transition } from "@headlessui/react";
|
|
import api from "../../services/api";
|
|
import { useTheme } from "../../contexts/ThemeContext";
|
|
|
|
// Importando o ícone para cálculos judiciais
|
|
import CalculateIcon from '@mui/icons-material/Calculate';
|
|
// Importar ícones específicos para subcategorias de cálculos
|
|
import MoneyIcon from '@mui/icons-material/Money';
|
|
import ChildCareIcon from '@mui/icons-material/ChildCare';
|
|
|
|
export const MainLayout: React.FC = () => {
|
|
const [isSidebarOpen, setSidebarOpen] = useState(true);
|
|
const location = useLocation();
|
|
const { user, signOut } = useAuth();
|
|
const [userProfile, setUserProfile] = useState<any>(null);
|
|
const { isDarkMode, toggleDarkMode } = useTheme();
|
|
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
|
|
const submenuTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
// Adicionar efeito para buscar perfil do usuário
|
|
useEffect(() => {
|
|
const getUserProfile = async () => {
|
|
try {
|
|
const response = await api.get("/auth/me"); // Removido o /api pois já está no baseURL
|
|
console.log("Response data:", response.data); // Debug log
|
|
setUserProfile(response.data);
|
|
} catch (error) {
|
|
console.error("Erro ao buscar perfil:", error);
|
|
}
|
|
};
|
|
getUserProfile();
|
|
}, []);
|
|
|
|
// Console log para debug
|
|
useEffect(() => {
|
|
console.log("Perfil do usuário:", userProfile);
|
|
}, [userProfile]);
|
|
|
|
const menuItems = [
|
|
{ path: "/dashboard", label: "Dashboard", icon: HomeIcon },
|
|
{ path: "/processos", label: "Processos", icon: ScaleIcon },
|
|
{ path: "/processos-administrativos", label: "Proc.Adm", icon: ScaleIcon },
|
|
{
|
|
path: null,
|
|
label: "Provas",
|
|
icon: DocumentCheckIcon,
|
|
subitems: [
|
|
{
|
|
path: '/provas/upload',
|
|
label: 'Upload',
|
|
roles: [],
|
|
icon: ArrowUpTrayIcon
|
|
},
|
|
{
|
|
path: '/provas/admin',
|
|
label: 'Admin',
|
|
roles: ['ADVOGADO_ADMIN'],
|
|
icon: ShieldCheckIcon
|
|
}
|
|
]
|
|
},
|
|
{ path: "/clientes", label: "Clientes", icon: UserGroupIcon },
|
|
{ path: "/atendimentos", label: "Atendimentos", icon: UserIcon },
|
|
{ path: "/tarefas", label: "Tarefas", icon: ClipboardDocumentCheckIcon },
|
|
{ path: "/documentos", label: "Documentos", icon: DocumentTextIcon },
|
|
{
|
|
path: null,
|
|
label: "Cálculos",
|
|
icon: CalculateIcon,
|
|
roles: ['ADVOGADO_ADMIN', 'ADVOGADO', 'ASSISTENTE'],
|
|
subitems: [
|
|
{
|
|
path: '/calculos',
|
|
label: 'Dashboard',
|
|
roles: ['ADVOGADO_ADMIN', 'ADVOGADO', 'ASSISTENTE'],
|
|
icon: HomeIcon
|
|
},
|
|
{
|
|
path: '/calculos/pensao-alimenticia',
|
|
label: 'Pensão Alimentícia',
|
|
roles: ['ADVOGADO_ADMIN', 'ADVOGADO', 'ASSISTENTE'],
|
|
icon: ChildCareIcon
|
|
},
|
|
{
|
|
path: '/calculos/trabalhistas',
|
|
label: 'Trabalhistas',
|
|
roles: ['ADVOGADO_ADMIN', 'ADVOGADO', 'ASSISTENTE'],
|
|
icon: MoneyIcon
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
// Função para verificar se o caminho atual corresponde ao item do menu
|
|
const isCurrentPath = (path: string) => {
|
|
return location.pathname.startsWith(path);
|
|
};
|
|
|
|
// Função para lidar com hover no menu
|
|
const handleMenuMouseEnter = (menuLabel: string) => {
|
|
if (submenuTimeoutRef.current) {
|
|
clearTimeout(submenuTimeoutRef.current);
|
|
submenuTimeoutRef.current = null;
|
|
}
|
|
setActiveSubmenu(menuLabel);
|
|
|
|
// Atualiza a posição vertical do submenu
|
|
const menuItem = document.querySelector(`[data-menu-label="${menuLabel}"]`);
|
|
if (menuItem) {
|
|
const rect = menuItem.getBoundingClientRect();
|
|
document.documentElement.style.setProperty('--menu-item-top', `${rect.top}px`);
|
|
}
|
|
};
|
|
|
|
// Função para lidar com mouseleave do menu
|
|
const handleMenuMouseLeave = () => {
|
|
submenuTimeoutRef.current = setTimeout(() => {
|
|
setActiveSubmenu(null);
|
|
}, 300); // Atraso de 300ms antes de fechar o submenu
|
|
};
|
|
|
|
// Função para lidar com mouseenter no submenu
|
|
const handleSubmenuMouseEnter = () => {
|
|
if (submenuTimeoutRef.current) {
|
|
clearTimeout(submenuTimeoutRef.current);
|
|
submenuTimeoutRef.current = null;
|
|
}
|
|
};
|
|
|
|
// Limpar timeout quando o componente é desmontado
|
|
useEffect(() => {
|
|
return () => {
|
|
if (submenuTimeoutRef.current) {
|
|
clearTimeout(submenuTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div className="flex h-screen bg-gray-100 dark:bg-gray-900">
|
|
{/* Sidebar com barra de rolagem */}
|
|
<div className="w-20 flex flex-col bg-gradient-to-b from-blue-900 via-blue-800 to-blue-900 dark:from-blue-950 dark:via-blue-900 dark:to-blue-950 text-white">
|
|
{/* Logo - fixo no topo */}
|
|
<div className="h-16 flex-shrink-0 flex items-center justify-center border-b border-white/10">
|
|
<span className="text-xl font-bold">A</span>
|
|
</div>
|
|
|
|
{/* Menu de navegação vertical com scroll */}
|
|
<nav className="flex-1 overflow-y-auto">
|
|
<div className="py-4">
|
|
{menuItems.map((item, index) => (
|
|
<div
|
|
key={item.path || item.label}
|
|
className="px-2 mb-2"
|
|
>
|
|
{!item.subitems ? (
|
|
<Link
|
|
to={item.path}
|
|
className={`flex flex-col items-center justify-center p-3 rounded-lg transition-colors ${
|
|
isCurrentPath(item.path)
|
|
? "bg-white/20"
|
|
: "hover:bg-white/10"
|
|
}`}
|
|
title={item.label}
|
|
>
|
|
<item.icon className="h-6 w-6 text-white opacity-80" />
|
|
<span className="text-xs mt-1 text-white/80">{item.label}</span>
|
|
</Link>
|
|
) : (
|
|
<div
|
|
className="relative"
|
|
data-menu-label={item.label}
|
|
onMouseEnter={() => handleMenuMouseEnter(item.label)}
|
|
onMouseLeave={handleMenuMouseLeave}
|
|
>
|
|
<div
|
|
className={`flex flex-col items-center justify-center p-3 rounded-lg transition-colors hover:bg-white/10 cursor-pointer ${
|
|
item.subitems.some(subitem => isCurrentPath(subitem.path))
|
|
? "bg-white/20"
|
|
: ""
|
|
}`}
|
|
title={item.label}
|
|
>
|
|
<item.icon className="h-6 w-6 text-white opacity-80" />
|
|
<span className="text-xs mt-1 text-white/80">{item.label}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</nav>
|
|
|
|
{/* Submenus renderizados fora da estrutura do menu principal */}
|
|
{menuItems
|
|
.filter(item => item.subitems)
|
|
.map(item => (
|
|
<div
|
|
key={`submenu-${item.label}`}
|
|
className={`fixed left-20 z-50 ${activeSubmenu === item.label ? 'block' : 'hidden'}`}
|
|
style={{ top: 'var(--menu-item-top, 0)' }}
|
|
onMouseEnter={handleSubmenuMouseEnter}
|
|
onMouseLeave={handleMenuMouseLeave}
|
|
>
|
|
<div
|
|
className="transition-all duration-200 ml-2"
|
|
style={{ transformOrigin: 'left center' }}
|
|
>
|
|
<div className="bg-blue-800/95 backdrop-blur-sm rounded-lg py-2 px-1 shadow-xl w-[140px] border border-blue-700/30">
|
|
{item.subitems
|
|
.filter(subitem =>
|
|
!subitem.roles?.length ||
|
|
(userProfile?.perfil && subitem.roles.includes(userProfile.perfil))
|
|
)
|
|
.map((subitem) => (
|
|
<Link
|
|
key={subitem.path}
|
|
to={subitem.path}
|
|
className={`block px-3 py-2 hover:bg-blue-700/50 rounded-lg transition-colors ${
|
|
isCurrentPath(subitem.path) ? "bg-blue-700/70" : ""
|
|
}`}
|
|
>
|
|
<div className="flex items-center space-x-2">
|
|
<subitem.icon className="h-5 w-5 text-white opacity-80" />
|
|
<span className="text-sm text-white/90">{subitem.label}</span>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div className="flex-1 flex flex-col">
|
|
{/* Header */}
|
|
<header className="h-16 bg-white dark:bg-gray-800 shadow-sm">
|
|
<div className="h-full px-4 flex items-center justify-between">
|
|
<h1 className="text-xl font-semibold text-gray-800 dark:text-gray-200">
|
|
{menuItems.find((item) => item.path === location.pathname)
|
|
?.label || "Dashboard"}
|
|
</h1>
|
|
<div className="flex items-center space-x-4">
|
|
{/* Adicionar botão de toggle do tema antes do menu do usuário */}
|
|
<button
|
|
onClick={toggleDarkMode}
|
|
className="p-2 rounded-lg text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-400"
|
|
title={isDarkMode ? "Modo claro" : "Modo escuro"}
|
|
>
|
|
{isDarkMode ? (
|
|
<SunIcon className="h-5 w-5" />
|
|
) : (
|
|
<MoonIcon className="h-5 w-5" />
|
|
)}
|
|
</button>
|
|
|
|
{/* Notificações */}
|
|
<button className="text-gray-600 hover:text-gray-800 relative">
|
|
<BellIcon className="h-6 w-6" />
|
|
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center">
|
|
3
|
|
</span>
|
|
</button>
|
|
|
|
{/* Menu do usuário */}
|
|
<Menu as="div" className="relative">
|
|
<Menu.Button className="flex items-center space-x-3 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg p-2 transition-colors">
|
|
<img
|
|
src={`https://ui-avatars.com/api/?name=${user?.nome || "User"}&background=0D8ABC&color=fff`}
|
|
alt={user?.nome}
|
|
className="w-8 h-8 rounded-full"
|
|
/>
|
|
<div className="text-left hidden sm:block">
|
|
<div className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
|
{user?.nome}
|
|
</div>
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">{user?.cargo}</div>
|
|
</div>
|
|
<ChevronDownIcon className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
|
</Menu.Button>
|
|
|
|
<Transition
|
|
as={Fragment}
|
|
enter="transition ease-out duration-100"
|
|
enterFrom="transform opacity-0 scale-95"
|
|
enterTo="transform opacity-100 scale-100"
|
|
leave="transition ease-in duration-75"
|
|
leaveFrom="transform opacity-100 scale-100"
|
|
leaveTo="transform opacity-0 scale-95"
|
|
>
|
|
<Menu.Items className="absolute right-0 mt-2 w-48 rounded-md bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
|
<div className="py-1">
|
|
<Menu.Item>
|
|
{({ active }) => (
|
|
<Link
|
|
to="/perfil"
|
|
className={`${
|
|
active ? "bg-gray-100 dark:bg-gray-700" : ""
|
|
} block px-4 py-2 text-sm text-gray-700 dark:text-gray-200`}
|
|
>
|
|
Meu Perfil
|
|
</Link>
|
|
)}
|
|
</Menu.Item>
|
|
|
|
{/* Novo item - visível apenas para ADMIN */}
|
|
{userProfile?.perfil === "ADVOGADO_ADMIN" && (
|
|
<Menu.Item>
|
|
{({ active }) => (
|
|
<Link
|
|
to="/usuarios"
|
|
className={`${
|
|
active ? "bg-gray-100 dark:bg-gray-700" : ""
|
|
} block px-4 py-2 text-sm text-gray-700 dark:text-gray-200`}
|
|
>
|
|
Gerenciar Usuários
|
|
</Link>
|
|
)}
|
|
</Menu.Item>
|
|
)}
|
|
|
|
<Menu.Item>
|
|
{({ active }) => (
|
|
<a
|
|
href="#configuracoes"
|
|
className={`${
|
|
active ? "bg-gray-100 dark:bg-gray-700" : ""
|
|
} block px-4 py-2 text-sm text-gray-700 dark:text-gray-200`}
|
|
>
|
|
Configurações
|
|
</a>
|
|
)}
|
|
</Menu.Item>
|
|
<Menu.Item>
|
|
{({ active }) => (
|
|
<button
|
|
onClick={signOut}
|
|
className={`${
|
|
active ? "bg-gray-100 dark:bg-gray-700" : ""
|
|
} block w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400`}
|
|
>
|
|
Sair
|
|
</button>
|
|
)}
|
|
</Menu.Item>
|
|
</div>
|
|
</Menu.Items>
|
|
</Transition>
|
|
</Menu>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Content */}
|
|
<main className="flex-1 overflow-auto p-6 bg-gray-100 dark:bg-gray-900">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|