Files
SR-SYSTEM/src/components/layout/Sidebar.tsx
T

371 lines
16 KiB
TypeScript

"use client";
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import {
LayoutDashboard,
Ticket,
Link2,
Monitor,
FileText,
Plus,
Lightbulb,
ChevronDown,
PlusCircle,
ArrowUpRight,
ArrowDownLeft,
History,
BookOpen,
Users,
ClipboardCheck,
CalendarDays,
Settings,
CheckSquare,
Briefcase,
Shield,
} from 'lucide-react';
import { useAuthStore } from '@/stores';
import apiClient from '@/services/apiClient';
import type { MenuPermission } from '@/types';
const Sidebar = ({ isCollapsed }: { isCollapsed?: boolean }) => {
const pathname = usePathname();
const router = useRouter();
const [isItamOpen, setIsItamOpen] = useState(pathname.startsWith('/itam'));
const [isAdminOpen, setIsAdminOpen] = useState(pathname.startsWith('/settings'));
const [menuPerms, setMenuPerms] = useState<Record<string, MenuPermission>>({});
const [permsLoaded, setPermsLoaded] = useState(false);
const user = useAuthStore((s) => s.user);
const role = user?.roleCode;
useEffect(() => {
apiClient.get('/menu-permissions').then((res: any) => {
if (res?.success && res.data) {
const map: Record<string, MenuPermission> = {};
res.data.forEach((p: MenuPermission) => { map[p.menuKey] = p; });
setMenuPerms(map);
}
}).catch(() => {}).finally(() => setPermsLoaded(true));
}, []);
const canAccess = (menuKey: string): boolean => {
if (!permsLoaded) return true; // show all until loaded
const perm = menuPerms[menuKey];
if (!perm) return true;
try {
const roles: string[] = JSON.parse(perm.allowedRoles);
return role ? roles.includes(role) : false;
} catch { return true; }
};
const menuItems = [
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
{ name: 'SR Issues', href: '/issues', icon: Ticket },
{ name: 'Dispatch', href: '/dispatch', icon: Link2 },
];
const itamSubItems = [
{ name: 'Asset Hub', href: '/itam', icon: Monitor },
{ name: 'Register', href: '/itam/register', icon: PlusCircle },
{ name: 'Check-out', href: '/itam/checkout', icon: ArrowUpRight },
{ name: 'Check-in', href: '/itam/checkin', icon: ArrowDownLeft },
{ name: 'History', href: '/itam', icon: History },
];
const projects = [
{ name: 'Samsung Pay SI', dot: 'bg-dot-green' },
{ name: 'LG CNS SM', dot: 'bg-dot-orange' },
{ name: 'Kakao Maintenance', dot: 'bg-dot-blue' },
];
return (
<aside
className={`bg-surface border-r border-border flex flex-col justify-between flex-shrink-0 transition-all duration-300 relative z-20 overflow-hidden ${
isCollapsed ? 'w-[64px]' : 'w-[250px]'
}`}
>
<div className="flex-1 overflow-y-auto no-scrollbar">
{/* 로고 영역 */}
<div className={`h-16 flex items-center border-b border-border transition-all sticky top-0 bg-surface z-10 ${isCollapsed ? 'justify-center' : 'px-6'}`}>
<div className="w-6 h-6 rounded-full bg-gradient-to-tr from-primary to-purple-400 flex-shrink-0 shadow-lg shadow-primary/20"></div>
{!isCollapsed && (
<h1 className="font-outfit font-semibold text-lg text-text-main tracking-wide ml-3 whitespace-nowrap">SR System.</h1>
)}
</div>
{/* 메인 메뉴 */}
<div className={`py-6 ${isCollapsed ? 'px-1' : 'px-4'}`}>
{!isCollapsed && (
<p className="text-[10px] font-bold text-text-muted tracking-[0.12em] mb-4 px-2 uppercase whitespace-nowrap">Main Menu</p>
)}
<nav className="space-y-1">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
title={isCollapsed ? item.name : ''}
className={`flex items-center rounded-xl font-bold text-xs transition-all ${
isCollapsed ? 'justify-center py-3' : 'px-3 py-2.5'
} ${
isActive
? 'bg-primary text-white shadow-lg shadow-primary/20'
: 'text-text-muted hover:bg-gray-50 hover:text-text-main'
}`}
>
<Icon className={`w-4.5 h-4.5 flex-shrink-0 ${isCollapsed ? '' : 'mr-3'}`} />
{!isCollapsed && <span className="whitespace-nowrap">{item.name}</span>}
</Link>
);
})}
{/* Projects */}
{canAccess('projects') && (
<Link
href="/projects"
className={`flex items-center rounded-xl font-bold text-xs transition-all ${
isCollapsed ? 'justify-center py-3' : 'px-3 py-2.5'
} ${
pathname === '/projects'
? 'bg-primary text-white shadow-lg shadow-primary/20'
: 'text-text-muted hover:bg-gray-50 hover:text-text-main'
}`}
>
<Briefcase className={`w-4.5 h-4.5 flex-shrink-0 ${isCollapsed ? '' : 'mr-3'}`} />
{!isCollapsed && <span className="whitespace-nowrap">Projects</span>}
</Link>
)}
{/* ITAM Group Menu */}
{canAccess('itam') && (
<div className="space-y-1">
<button
onClick={() => !isCollapsed && setIsItamOpen(!isItamOpen)}
className={`w-full flex items-center justify-between rounded-xl font-bold text-xs transition-all ${
isCollapsed ? 'justify-center py-3' : 'px-3 py-2.5'
} ${
pathname.startsWith('/itam') && !isCollapsed
? 'text-primary'
: 'text-text-muted hover:bg-gray-50 hover:text-text-main'
}`}
>
<div className="flex items-center">
<Monitor className={`w-4.5 h-4.5 flex-shrink-0 ${isCollapsed ? '' : 'mr-3'}`} />
{!isCollapsed && <span className="whitespace-nowrap">ITAM (Assets)</span>}
</div>
{!isCollapsed && (
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-300 ${isItamOpen ? 'rotate-180' : ''}`} />
)}
</button>
{/* ITAM Sub Items */}
{isItamOpen && !isCollapsed && (
<div className="pl-6 space-y-1 mt-1 animate-in slide-in-from-top-2 duration-300">
{itamSubItems.map((sub) => {
const SubIcon = sub.icon;
const isActive = pathname === sub.href;
return (
<Link
key={sub.name}
href={sub.href}
className={`flex items-center px-3 py-2 rounded-lg text-[11px] font-bold transition-all ${
isActive
? 'text-primary bg-indigo-50/50'
: 'text-text-muted hover:text-text-main hover:bg-gray-50'
}`}
>
<SubIcon className="w-3.5 h-3.5 mr-2.5" />
<span className="whitespace-nowrap">{sub.name}</span>
</Link>
);
})}
</div>
)}
</div>
)}
<Link
href="/work-reports"
className={`flex items-center rounded-xl font-bold text-xs transition-all ${
isCollapsed ? 'justify-center py-3' : 'px-3 py-2.5'
} ${
pathname === '/work-reports'
? 'bg-primary text-white shadow-lg shadow-primary/20'
: 'text-text-muted hover:bg-gray-50 hover:text-text-main'
}`}
>
<FileText className={`w-4.5 h-4.5 flex-shrink-0 ${isCollapsed ? '' : 'mr-3'}`} />
{!isCollapsed && <span className="whitespace-nowrap">Work Reports</span>}
</Link>
<Link
href="/daily-reports"
className={`flex items-center rounded-xl font-bold text-xs transition-all ${
isCollapsed ? 'justify-center py-3' : 'px-3 py-2.5'
} ${
pathname === '/daily-reports'
? 'bg-primary text-white shadow-lg shadow-primary/20'
: 'text-text-muted hover:bg-gray-50 hover:text-text-main'
}`}
>
<ClipboardCheck className={`w-4.5 h-4.5 flex-shrink-0 ${isCollapsed ? '' : 'mr-3'}`} />
{!isCollapsed && <span className="whitespace-nowrap">Daily Report</span>}
</Link>
{canAccess('reports-review') && (
<Link
href="/work-reports/review"
className={`flex items-center rounded-xl font-bold text-xs transition-all ${
isCollapsed ? 'justify-center py-3' : 'px-3 py-2.5'
} ${
pathname === '/work-reports/review'
? 'bg-primary text-white shadow-lg shadow-primary/20'
: 'text-text-muted hover:bg-gray-50 hover:text-text-main'
}`}
>
<CheckSquare className={`w-4.5 h-4.5 flex-shrink-0 ${isCollapsed ? '' : 'mr-3'}`} />
{!isCollapsed && <span className="whitespace-nowrap">Report Review</span>}
</Link>
)}
{canAccess('settings') && (
<>
<button
onClick={() => !isCollapsed && setIsAdminOpen(!isAdminOpen)}
className={`w-full flex items-center justify-between rounded-xl font-bold text-xs transition-all ${
isCollapsed ? 'justify-center py-3' : 'px-3 py-2.5'
} ${
pathname.startsWith('/settings') && !isCollapsed
? 'text-primary'
: 'text-text-muted hover:bg-gray-50 hover:text-text-main'
}`}
>
<div className="flex items-center">
<Settings className={`w-4.5 h-4.5 flex-shrink-0 ${isCollapsed ? '' : 'mr-3'}`} />
{!isCollapsed && <span className="whitespace-nowrap">Admin</span>}
</div>
{!isCollapsed && (
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-300 ${isAdminOpen ? 'rotate-180' : ''}`} />
)}
</button>
{isAdminOpen && !isCollapsed && (
<div className="pl-6 space-y-1 mt-1">
{[
{ name: 'Codes', href: '/settings/codes', icon: Settings },
{ name: 'Accounts', href: '/settings/accounts', icon: Users },
{ name: 'Batch Employees', href: '/settings/batch/employees', icon: FileText },
{ name: 'System', href: '/settings/system', icon: Monitor },
{ name: 'Notification', href: '/settings/notification', icon: FileText },
{ name: 'Audit Log', href: '/settings/audit', icon: History },
{ name: 'Menu Perms', href: '/settings/menus', icon: Shield },
].map((sub) => {
const SubIcon = sub.icon;
const isActive = pathname === sub.href;
return (
<Link key={sub.name} href={sub.href}
className={`flex items-center gap-2.5 px-3 py-2 rounded-xl text-xs font-bold transition-all ${
isActive ? 'bg-primary/10 text-primary' : 'text-text-muted hover:text-text-main'
}`}>
<SubIcon className="w-3.5 h-3.5" />
{sub.name}
</Link>
);
})}
</div>
)}
</>
)}
<Link
href="/calendar"
className={`flex items-center rounded-xl font-bold text-xs transition-all ${
isCollapsed ? 'justify-center py-3' : 'px-3 py-2.5'
} ${
pathname === '/calendar'
? 'bg-primary text-white shadow-lg shadow-primary/20'
: 'text-text-muted hover:bg-gray-50 hover:text-text-main'
}`}
>
<CalendarDays className={`w-4.5 h-4.5 flex-shrink-0 ${isCollapsed ? '' : 'mr-3'}`} />
{!isCollapsed && <span className="whitespace-nowrap">Calendar</span>}
</Link>
{canAccess('employees') && (
<Link
href="/employees"
className={`flex items-center rounded-xl font-bold text-xs transition-all ${
isCollapsed ? 'justify-center py-3' : 'px-3 py-2.5'
} ${
pathname === '/employees'
? 'bg-primary text-white shadow-lg shadow-primary/20'
: 'text-text-muted hover:bg-gray-50 hover:text-text-main'
}`}
>
<Users className={`w-4.5 h-4.5 flex-shrink-0 ${isCollapsed ? '' : 'mr-3'}`} />
{!isCollapsed && <span className="whitespace-nowrap">Employees</span>}
</Link>
)}
</nav>
</div>
{/* 내 프로젝트 */}
<div className={`border-t border-border mt-4 pt-6 ${isCollapsed ? 'px-1 py-2' : 'px-4 py-2'}`}>
<div className={`flex items-center justify-between mb-4 ${isCollapsed ? 'justify-center' : 'px-2'}`}>
{!isCollapsed ? (
<>
<p className="text-[10px] font-bold text-text-muted tracking-[0.12em] uppercase whitespace-nowrap">My Projects</p>
<button onClick={() => router.push('/dispatch')} className="text-text-muted hover:text-text-main">
<Plus className="w-3.5 h-3.5" />
</button>
</>
) : (
<p className="text-[10px] font-bold text-text-muted uppercase">MY</p>
)}
</div>
<nav className="space-y-1">
{projects.map((project) => (
<Link
key={project.name}
href="/dispatch"
title={isCollapsed ? project.name : ''}
className={`flex items-center hover:bg-gray-50 rounded-xl text-xs font-bold text-text-muted transition-colors ${
isCollapsed ? 'justify-center py-2' : 'px-3 py-2'
}`}
>
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${project.dot} ${isCollapsed ? '' : 'mr-3'}`}></span>
{!isCollapsed && <span className="whitespace-nowrap">{project.name}</span>}
</Link>
))}
</nav>
</div>
</div>
{/* 하단 정보 카드 */}
<div className={`${isCollapsed ? 'p-2' : 'p-6'}`}>
{isCollapsed ? (
<button className="w-10 h-10 mx-auto bg-primary rounded-xl flex items-center justify-center text-white shadow-soft hover:bg-primary-hover transition-colors">
<Lightbulb className="w-5 h-5" />
</button>
) : (
<div className="bg-indigo-50/50 rounded-2xl p-4 border border-indigo-100/50 text-center relative overflow-hidden group">
<div className="absolute -top-4 -right-4 w-12 h-12 bg-primary/10 rounded-full opacity-50 blur-xl group-hover:scale-150 transition-transform duration-700"></div>
<div className="w-8 h-8 mx-auto bg-white rounded-full flex items-center justify-center shadow-sm mb-3">
<span className="text-lg"><Lightbulb className="w-4.5 h-4.5 text-primary" /></span>
</div>
<h4 className="font-bold text-xs text-slate-800 mb-1.5 whitespace-nowrap">Need Assistance?</h4>
<p className="text-[10px] text-slate-500 leading-relaxed mb-4">현장 장비 부족이나 기술적 이슈는 즉시 보고하세요.</p>
<button onClick={() => router.push('/issues')} className="w-full py-2 bg-white rounded-xl text-[10px] font-bold text-primary shadow-sm hover:shadow-md transition-all border border-indigo-100">
HQ에 긴급 메시지
</button>
</div>
)}
</div>
</aside>
);
};
export default Sidebar;