MPEDA / src /components /layout /Sidebar.tsx
sarveshpatel's picture
Upload 139 files
f305a41 verified
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>
</>
);
}