371 lines
16 KiB
TypeScript
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;
|