smart_ims/src/widgets/layout/MainLayout.tsx

274 lines
16 KiB
TypeScript

import { useState } from 'react';
import { Outlet, Link, useLocation } from 'react-router-dom';
import { useAuth } from '../../shared/auth/AuthContext';
import { useSystem } from '../../shared/context/SystemContext';
import { Settings, LogOut, Box, ChevronDown, ChevronRight, Layers, User as UserIcon, Video, Shield, Info, Home, UserCog } from 'lucide-react';
import type { IModuleDefinition } from '../../core/types';
import './MainLayout.css';
interface MainLayoutProps {
modulesList: IModuleDefinition[];
}
export function MainLayout({ modulesList }: MainLayoutProps) {
const location = useLocation();
const { user, logout } = useAuth();
const { modules } = useSystem();
const [expandedModules, setExpandedModules] = useState<string[]>(['asset-management']);
const isAdmin = user ? (['admin', 'supervisor'] as string[]).includes(user.role) : false;
const checkRole = (requiredRoles: string[]) => {
if (!user) return false;
// Supervisor possesses all rights
if (user.role === 'supervisor') return true;
return requiredRoles.includes(user.role);
};
const toggleModule = (moduleId: string) => {
setExpandedModules(prev =>
prev.includes(moduleId)
? prev.filter(id => id !== moduleId)
: [...prev, moduleId]
);
};
const isModuleActive = (code: string) => modules[code]?.active;
return (
<div className="layout-container">
<aside className="sidebar">
<div className="sidebar-header">
<div className="brand">
<Box className="brand-icon" size={24} />
<span className="brand-text">Smart IMS</span>
</div>
</div>
<nav className="sidebar-nav">
{/* Platform Home */}
<div className="module-group mb-4">
<Link to="/home" className={`nav-item ${location.pathname === '/home' ? 'active' : ''}`}>
<Home size={18} />
<span> </span>
</Link>
</div>
{/* System Management (Platform Core) */}
{isAdmin && (
<div className="module-group">
<button
className={`module-header ${expandedModules.includes('sys_mgmt') ? 'active' : ''}`}
onClick={() => toggleModule('sys_mgmt')}
>
<div className="module-title">
<Settings size={18} />
<span> </span>
</div>
{expandedModules.includes('sys_mgmt') ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
{expandedModules.includes('sys_mgmt') && (
<div className="module-items">
<Link to="/admin/settings" className={`nav-item ${location.pathname.includes('/admin/settings') ? 'active' : ''}`}>
<Settings size={18} />
<span> </span>
</Link>
<Link to="/admin/users" className={`nav-item ${location.pathname.includes('/admin/users') ? 'active' : ''}`}>
<UserIcon size={18} />
<span> </span>
</Link>
<Link to="/admin/license" className={`nav-item ${location.pathname.includes('/admin/license') ? 'active' : ''}`}>
<Shield size={18} />
<span>/ </span>
</Link>
<Link to="/admin/version" className={`nav-item ${location.pathname.includes('/admin/version') ? 'active' : ''}`}>
<Info size={18} />
<span> </span>
</Link>
</div>
)}
</div>
)}
{/* Dynamic Modules Injection */}
{modulesList.map((mod) => {
const moduleKey = mod.moduleName.split('-')[0];
if (!isModuleActive(moduleKey)) return null;
// Check roles with hierarchy
if (mod.requiredRoles && !checkRole(mod.requiredRoles)) return null;
const hasSubMenu = mod.routes.filter(r => r.label).length > 1;
const isExpanded = expandedModules.includes(mod.moduleName);
return (
<div key={mod.moduleName} className="module-group">
{hasSubMenu ? (
<>
<button
className={`module-header ${isExpanded ? 'active' : ''}`}
onClick={() => toggleModule(mod.moduleName)}
>
<div className="module-title">
{mod.moduleName === 'cctv' ? <Video size={18} /> : <Layers size={18} />}
<span>{mod.label || (mod.moduleName.split('-')[0].charAt(0).toUpperCase() + mod.moduleName.split('-')[0].slice(1) + ' 관리')}</span>
</div>
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
{isExpanded && (
<div className="module-items">
{(() => {
const groupedRoutes: Record<string, typeof mod.routes> = {};
const ungroupedRoutes: typeof mod.routes = [];
mod.routes.filter(r => {
const isLabelVisible = !!r.label;
const isSidebarPosition = !r.position || r.position === 'sidebar';
const isSettingsRoute = r.path.includes('settings');
const hasPermission = user?.role !== 'user' || !isSettingsRoute;
return isLabelVisible && isSidebarPosition && hasPermission;
}).forEach(route => {
if (route.group) {
if (!groupedRoutes[route.group]) groupedRoutes[route.group] = [];
groupedRoutes[route.group].push(route);
} else {
ungroupedRoutes.push(route);
}
});
return (
<>
{ungroupedRoutes.map(route => (
<Link
key={`${mod.moduleName}-${route.path}`}
to={`${mod.basePath}${route.path}`}
className={`nav-item ${location.pathname.startsWith(`${mod.basePath}${route.path}`) ? 'active' : ''}`}
>
{route.icon || <Box size={18} />}
<span>{route.label}</span>
</Link>
))}
{Object.entries(groupedRoutes).map(([groupName, routes]) => (
<div key={groupName} className="menu-group-section">
<div className="menu-group-label text-xs font-bold text-slate-500 uppercase px-3 py-2 mt-2 mb-1">
{groupName}
</div>
{routes.map(route => (
<Link
key={`${mod.moduleName}-${route.path}`}
to={`${mod.basePath}${route.path}`}
className={`nav-item ${location.pathname.startsWith(`${mod.basePath}${route.path}`) ? 'active' : ''}`}
>
{route.icon || <Box size={18} />}
<span>{route.label}</span>
</Link>
))}
</div>
))}
</>
);
})()}
</div>
)}
</>
) : (
<Link
to={`${mod.basePath}${mod.routes[0]?.path || ''}`}
className={`module-header ${location.pathname.startsWith(mod.basePath) ? 'active' : ''}`}
style={{ textDecoration: 'none' }}
>
<div className="module-title">
<Video size={18} />
<span>{mod.label || mod.moduleName.split('-')[0].toUpperCase()}</span>
</div>
</Link>
)}
</div>
);
})}
<div className="sidebar-divider my-4 border-t border-slate-100 opacity-50"></div>
{/* User Preferences - Accessible to all roles */}
<div className="module-group">
<Link to="/preferences" className={`nav-item ${location.pathname === '/preferences' ? 'active' : ''}`}>
<UserCog size={18} />
<span> </span>
</Link>
</div>
</nav>
<div className="sidebar-footer">
<div className="user-info">
<div className="user-avatar bg-slate-700 text-white flex items-center justify-center rounded-full w-8 h-8 text-xs font-bold">
{user?.name?.slice(0, 2) || 'USER'}
</div>
<div className="user-details">
<span className="user-name">{user?.name}</span>
<span className="user-role text-xs text-slate-400 capitalize">{user?.role}</span>
</div>
</div>
<button className="logout-btn" onClick={logout} title="로그아웃">
<LogOut size={18} />
</button>
</div>
</aside>
<main className="main-content">
<header className="top-header">
<div className="header-tabs">
{/* Dynamic Tabs based on Active Module 'top' routes */}
{(() => {
// Find active module based on current path
const activeModule = modulesList.find(m => location.pathname.startsWith(m.basePath));
if (!activeModule) return null;
const topRoutes = location.pathname.includes('/settings')
? []
: activeModule.routes.filter(r => r.position === 'top');
return topRoutes.map(route => (
<Link
key={`${activeModule.moduleName}-top-${route.path}`}
to={`${activeModule.basePath}${route.path}`}
className={`tab-item ${location.pathname.startsWith(`${activeModule.basePath}${route.path}`) ? 'active' : ''}`}
>
{route.label}
</Link>
));
})()}
{/* Asset Settings Specific Tabs */}
{location.pathname.startsWith('/asset/settings') && (
<div className="header-tabs">
{[
{ id: 'basic', label: '기본 설정' },
{ id: 'category', label: '카테고리 관리' },
{ id: 'location', label: '설치 위치' },
{ id: 'status', label: '자산 상태' },
{ id: 'maintenance', label: '정비 구분' }
].map(tab => (
<Link
key={tab.id}
to={`/asset/settings?tab=${tab.id}`}
className={`tab-item ${(new URLSearchParams(location.search).get('tab') || 'basic') === tab.id ? 'active' : ''}`}
>
{tab.label}
</Link>
))}
</div>
)}
</div>
<div id="header-portal-root" className="header-portal-root"></div>
</header>
<div className="content-area">
<Outlet />
</div>
</main>
</div>
);
}