Spaces:
Sleeping
Sleeping
| import { NavLink, useLocation } from 'react-router-dom'; | |
| import { cn } from '@/lib/utils'; | |
| import { useAuth } from '@/contexts/AuthContext'; | |
| import { getRoleLabel } from '@/data/dummyData'; | |
| import { | |
| LayoutDashboard, | |
| FileText, | |
| Warehouse, | |
| ClipboardCheck, | |
| Award, | |
| Receipt, | |
| Users, | |
| Settings, | |
| LogOut, | |
| ChevronLeft, | |
| ChevronRight, | |
| Package, | |
| Factory, | |
| BarChart3, | |
| Fish, | |
| X, | |
| Calendar, | |
| } from 'lucide-react'; | |
| import { useState, useEffect } from 'react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Avatar, AvatarFallback } from '@/components/ui/avatar'; | |
| import { useIsMobile } from '@/hooks/use-mobile'; | |
| interface NavItem { | |
| label: string; | |
| href: string; | |
| icon: React.ComponentType<{ className?: string }>; | |
| roles?: string[]; | |
| } | |
| const navItems: NavItem[] = [ | |
| { label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard }, | |
| { label: 'Hatcheries', href: '/hatcheries', icon: Fish }, | |
| { label: 'Farms', href: '/farms', icon: Warehouse }, | |
| { label: 'Applications', href: '/applications', icon: FileText }, | |
| { label: 'Audits', href: '/audits', icon: ClipboardCheck }, | |
| { label: 'FOI Applications', href: '/foi/applications', icon: ClipboardCheck, roles: ['foi'] }, | |
| { label: 'FO Field Visits', href: '/fo/visits', icon: ClipboardCheck, roles: ['fo'] }, | |
| { label: 'CC Officer Audits', href: '/cc-officer/audits', icon: ClipboardCheck, roles: ['cc_officer'] }, | |
| { label: 'Certificates', href: '/certificates', icon: Award }, | |
| { label: 'CC Certification', href: '/cc-officer/certification', icon: Award, roles: ['cc_officer'] }, | |
| { label: 'JD Approvals', href: '/jd/approvals', icon: ClipboardCheck, roles: ['jd'] }, | |
| { label: 'Director Approvals', href: '/director/approvals', icon: ClipboardCheck, roles: ['director'] }, | |
| { label: 'Surveillance Schedule', href: '/surveillance-schedule', icon: Calendar }, | |
| { label: 'Raw Materials', href: '/raw-materials', icon: Package }, | |
| { label: 'Processors', href: '/processors', icon: Factory }, | |
| { label: 'TA Claims', href: '/ta-claims', icon: Receipt }, | |
| { label: 'Reports', href: '/reports', icon: BarChart3 }, | |
| { label: 'Users', href: '/users', icon: Users, roles: ['admin'] }, | |
| { label: 'Settings', href: '/settings', icon: Settings }, | |
| ]; | |
| interface SidebarProps { | |
| mobileOpen: boolean; | |
| onMobileClose: () => void; | |
| } | |
| export function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) { | |
| const [collapsed, setCollapsed] = useState(false); | |
| const { user, logout } = useAuth(); | |
| const location = useLocation(); | |
| const isMobile = useIsMobile(); | |
| // Close sidebar on mobile when route changes | |
| useEffect(() => { | |
| if (isMobile && mobileOpen) { | |
| onMobileClose(); | |
| } | |
| }, [location.pathname]); | |
| const filteredNavItems = navItems.filter(item => { | |
| if (!item.roles) return true; | |
| return user && item.roles.includes(user.role); | |
| }); | |
| const roleSpecificNavItems = filteredNavItems.filter((item) => { | |
| if (!user) return false; | |
| if (user.role === 'cc_officer') { | |
| const allowedForCcOfficer = new Set<string>([ | |
| '/dashboard', | |
| '/hatcheries', | |
| '/farms', | |
| '/applications', | |
| '/cc-officer/audits', | |
| '/certificates', | |
| '/cc-officer/certification', | |
| ]); | |
| return allowedForCcOfficer.has(item.href); | |
| } | |
| if (user.role === 'foi') { | |
| const allowedForFoi = new Set<string>([ | |
| '/dashboard', | |
| '/foi/applications', | |
| ]); | |
| return allowedForFoi.has(item.href); | |
| } | |
| if (user.role === 'fo') { | |
| const allowedForFo = new Set<string>([ | |
| '/dashboard', | |
| '/fo/visits', | |
| ]); | |
| return allowedForFo.has(item.href); | |
| } | |
| if (user.role === 'jd') { | |
| const allowedForJd = new Set<string>([ | |
| '/dashboard', | |
| '/jd/approvals', | |
| '/surveillance-schedule', | |
| ]); | |
| return allowedForJd.has(item.href); | |
| } | |
| if (user.role === 'director') { | |
| const allowedForDirector = new Set<string>([ | |
| '/dashboard', | |
| '/director/approvals', | |
| '/surveillance-schedule', | |
| ]); | |
| return allowedForDirector.has(item.href); | |
| } | |
| return true; | |
| }); | |
| return ( | |
| <> | |
| {/* Mobile overlay */} | |
| {isMobile && mobileOpen && ( | |
| <div | |
| className="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm" | |
| onClick={onMobileClose} | |
| /> | |
| )} | |
| <aside | |
| className={cn( | |
| 'fixed left-0 top-0 z-50 h-screen transition-all duration-300 ease-in-out', | |
| 'bg-sidebar border-r border-sidebar-border', | |
| isMobile ? ( | |
| mobileOpen ? 'translate-x-0 w-64' : '-translate-x-full w-64' | |
| ) : ( | |
| collapsed ? 'w-[72px]' : 'w-64' | |
| ) | |
| )} | |
| > | |
| {/* Logo */} | |
| <div className="flex h-16 items-center justify-between px-4 border-b border-sidebar-border"> | |
| {(!collapsed || isMobile) && ( | |
| <div className="flex items-center gap-2"> | |
| <div className="w-8 h-8 rounded-lg bg-sidebar-primary flex items-center justify-center"> | |
| <Fish className="w-5 h-5 text-sidebar-primary-foreground" /> | |
| </div> | |
| <div> | |
| <h1 className="text-sm font-bold text-sidebar-foreground">SHAPHARI</h1> | |
| <p className="text-[10px] text-sidebar-foreground/60">MPEDA Portal</p> | |
| </div> | |
| </div> | |
| )} | |
| {collapsed && !isMobile && ( | |
| <div className="w-8 h-8 mx-auto rounded-lg bg-sidebar-primary flex items-center justify-center"> | |
| <Fish className="w-5 h-5 text-sidebar-primary-foreground" /> | |
| </div> | |
| )} | |
| {isMobile && ( | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="text-sidebar-foreground hover:bg-sidebar-accent" | |
| onClick={onMobileClose} | |
| > | |
| <X className="h-5 w-5" /> | |
| </Button> | |
| )} | |
| </div> | |
| {/* Toggle Button - Desktop only */} | |
| {!isMobile && ( | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="absolute -right-3 top-20 z-50 h-6 w-6 rounded-full border border-sidebar-border bg-sidebar text-sidebar-foreground hover:bg-sidebar-accent" | |
| onClick={() => setCollapsed(!collapsed)} | |
| > | |
| {collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />} | |
| </Button> | |
| )} | |
| {/* Navigation */} | |
| <nav className="flex-1 overflow-y-auto py-4 px-3 h-[calc(100vh-180px)]"> | |
| <ul className="space-y-1"> | |
| {roleSpecificNavItems.map((item) => { | |
| const isActive = location.pathname === item.href; | |
| return ( | |
| <li key={item.href}> | |
| <NavLink | |
| to={item.href} | |
| className={cn( | |
| 'sidebar-nav-item', | |
| isActive && 'sidebar-nav-item-active' | |
| )} | |
| title={collapsed && !isMobile ? item.label : undefined} | |
| > | |
| <item.icon className={cn('h-5 w-5 flex-shrink-0', isActive && 'text-sidebar-primary')} /> | |
| {(!collapsed || isMobile) && <span className="truncate">{item.label}</span>} | |
| </NavLink> | |
| </li> | |
| ); | |
| })} | |
| </ul> | |
| </nav> | |
| {/* User Profile */} | |
| <div className="absolute bottom-0 left-0 right-0 border-t border-sidebar-border bg-sidebar p-3"> | |
| {user && ( | |
| <div className={cn('flex items-center gap-3', collapsed && !isMobile && 'justify-center')}> | |
| <Avatar className="h-9 w-9 flex-shrink-0"> | |
| <AvatarFallback className="bg-sidebar-primary text-sidebar-primary-foreground text-xs"> | |
| {user.name.split(' ').map(n => n[0]).join('')} | |
| </AvatarFallback> | |
| </Avatar> | |
| {(!collapsed || isMobile) && ( | |
| <div className="flex-1 min-w-0"> | |
| <p className="text-sm font-medium text-sidebar-foreground truncate">{user.name}</p> | |
| <p className="text-xs text-sidebar-foreground/60 truncate">{getRoleLabel(user.role)}</p> | |
| </div> | |
| )} | |
| {(!collapsed || isMobile) && ( | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 text-sidebar-foreground/60 hover:text-sidebar-foreground hover:bg-sidebar-accent" | |
| onClick={logout} | |
| title="Logout" | |
| > | |
| <LogOut className="h-4 w-4" /> | |
| </Button> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </aside> | |
| </> | |
| ); | |
| } | |