Files
bakery-ia/frontend/src/components/layout/Layout.tsx
Urtzi Alfaro 8914786973 New Frontend
2025-08-16 20:13:40 +02:00

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;