243 lines
8.9 KiB
TypeScript
243 lines
8.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom';
|
|
import {
|
|
Home,
|
|
TrendingUp,
|
|
Package,
|
|
Settings,
|
|
Menu,
|
|
X,
|
|
LogOut,
|
|
User,
|
|
Bell,
|
|
ChevronDown,
|
|
BarChart3,
|
|
Building
|
|
} from 'lucide-react';
|
|
import { useSelector, useDispatch } from 'react-redux';
|
|
import { RootState } from '../../store';
|
|
import { logout } from '../../store/slices/authSlice';
|
|
import { TenantSelector } from '../navigation/TenantSelector';
|
|
import { usePermissions } from '../../hooks/usePermissions';
|
|
|
|
interface LayoutProps {
|
|
// No props needed - using React Router
|
|
}
|
|
|
|
interface NavigationItem {
|
|
id: string;
|
|
label: string;
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
href: string;
|
|
requiresRole?: string[];
|
|
}
|
|
|
|
const Layout: React.FC<LayoutProps> = () => {
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const dispatch = useDispatch();
|
|
const { user } = useSelector((state: RootState) => state.auth);
|
|
const { hasRole } = usePermissions();
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
|
|
|
const navigation: NavigationItem[] = [
|
|
{ id: 'dashboard', label: 'Dashboard', icon: Home, href: '/app/dashboard' },
|
|
{ id: 'operations', label: 'Operaciones', icon: Package, href: '/app/operations' },
|
|
{
|
|
id: 'analytics',
|
|
label: 'Analytics',
|
|
icon: BarChart3,
|
|
href: '/app/analytics',
|
|
requiresRole: ['admin', 'manager']
|
|
},
|
|
{ id: 'settings', label: 'Configuración', icon: Settings, href: '/app/settings' },
|
|
];
|
|
|
|
// Filter navigation based on user role
|
|
const filteredNavigation = navigation.filter(item => {
|
|
if (!item.requiresRole) return true;
|
|
return item.requiresRole.some(role => hasRole(role));
|
|
});
|
|
|
|
const handleLogout = () => {
|
|
if (window.confirm('¿Estás seguro de que quieres cerrar sesión?')) {
|
|
dispatch(logout());
|
|
localStorage.removeItem('auth_token');
|
|
localStorage.removeItem('user_data');
|
|
localStorage.removeItem('selectedTenantId');
|
|
navigate('/');
|
|
}
|
|
};
|
|
|
|
const isActiveRoute = (href: string): boolean => {
|
|
if (href === '/app/dashboard') {
|
|
return location.pathname === '/app/dashboard' || location.pathname === '/app';
|
|
}
|
|
return location.pathname.startsWith(href);
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
{/* Top Navigation Bar */}
|
|
<nav className="bg-white shadow-soft border-b border-gray-200">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between h-16">
|
|
{/* Left side - Logo and Navigation */}
|
|
<div className="flex items-center">
|
|
{/* Mobile menu button */}
|
|
<button
|
|
type="button"
|
|
className="md:hidden p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
|
>
|
|
{isMobileMenuOpen ? (
|
|
<X className="h-6 w-6" />
|
|
) : (
|
|
<Menu className="h-6 w-6" />
|
|
)}
|
|
</button>
|
|
|
|
{/* Logo */}
|
|
<div className="flex items-center ml-4 md:ml-0">
|
|
<div className="h-8 w-8 bg-primary-500 rounded-lg flex items-center justify-center mr-3">
|
|
<span className="text-white text-sm font-bold">🥖</span>
|
|
</div>
|
|
<span className="text-xl font-bold text-gray-900">PanIA</span>
|
|
</div>
|
|
|
|
{/* Desktop Navigation */}
|
|
<div className="hidden md:flex md:ml-10 md:space-x-1">
|
|
{filteredNavigation.map((item) => {
|
|
const Icon = item.icon;
|
|
const isActive = isActiveRoute(item.href);
|
|
|
|
return (
|
|
<Link
|
|
key={item.id}
|
|
to={item.href}
|
|
className={`
|
|
flex items-center px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200
|
|
${isActive
|
|
? 'bg-primary-100 text-primary-700 shadow-soft'
|
|
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
|
|
}
|
|
`}
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
>
|
|
<Icon className="h-4 w-4 mr-2" />
|
|
{item.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right side - Tenant Selector, Notifications and User Menu */}
|
|
<div className="flex items-center space-x-4">
|
|
{/* Tenant Selector */}
|
|
<TenantSelector />
|
|
{/* Notifications */}
|
|
<button className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors relative">
|
|
<Bell className="h-5 w-5" />
|
|
<span className="absolute top-0 right-0 h-2 w-2 bg-red-500 rounded-full"></span>
|
|
</button>
|
|
|
|
{/* User Menu */}
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
|
|
className="flex items-center text-sm bg-white rounded-lg p-2 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
>
|
|
<div className="h-8 w-8 bg-primary-500 rounded-full flex items-center justify-center mr-2">
|
|
<User className="h-4 w-4 text-white" />
|
|
</div>
|
|
<span className="hidden md:block text-gray-700 font-medium">
|
|
{user.fullName?.split(' ')[0] || 'Usuario'}
|
|
</span>
|
|
<ChevronDown className="hidden md:block h-4 w-4 ml-1 text-gray-500" />
|
|
</button>
|
|
|
|
{/* User Dropdown */}
|
|
{isUserMenuOpen && (
|
|
<div className="absolute right-0 mt-2 w-48 bg-white rounded-xl shadow-strong border border-gray-200 py-1 z-50">
|
|
<div className="px-4 py-3 border-b border-gray-100">
|
|
<p className="text-sm font-medium text-gray-900">{user.fullName}</p>
|
|
<p className="text-sm text-gray-500">{user.email}</p>
|
|
</div>
|
|
<Link
|
|
to="/app/settings"
|
|
onClick={() => setIsUserMenuOpen(false)}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 flex items-center"
|
|
>
|
|
<Settings className="h-4 w-4 mr-2" />
|
|
Configuración
|
|
</Link>
|
|
<button
|
|
onClick={() => {
|
|
handleLogout();
|
|
setIsUserMenuOpen(false);
|
|
}}
|
|
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 flex items-center"
|
|
>
|
|
<LogOut className="h-4 w-4 mr-2" />
|
|
Cerrar sesión
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Navigation Menu */}
|
|
{isMobileMenuOpen && (
|
|
<div className="md:hidden border-t border-gray-200 bg-white">
|
|
<div className="px-2 pt-2 pb-3 space-y-1">
|
|
{filteredNavigation.map((item) => {
|
|
const Icon = item.icon;
|
|
const isActive = isActiveRoute(item.href);
|
|
|
|
return (
|
|
<Link
|
|
key={item.id}
|
|
to={item.href}
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
className={`
|
|
w-full flex items-center px-3 py-2 rounded-lg text-base font-medium transition-all duration-200
|
|
${isActive
|
|
? 'bg-primary-100 text-primary-700'
|
|
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
|
|
}
|
|
`}
|
|
>
|
|
<Icon className="h-5 w-5 mr-3" />
|
|
{item.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</nav>
|
|
|
|
{/* Main Content */}
|
|
<main className="flex-1">
|
|
<Outlet />
|
|
</main>
|
|
|
|
{/* Click outside handler for dropdowns */}
|
|
{(isUserMenuOpen || isMobileMenuOpen) && (
|
|
<div
|
|
className="fixed inset-0 z-40"
|
|
onClick={() => {
|
|
setIsUserMenuOpen(false);
|
|
setIsMobileMenuOpen(false);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Layout; |