Files
GSB-Construction/resources/js/Components/AppSidebar.tsx

256 lines
11 KiB
TypeScript

import { Link, usePage } from '@inertiajs/react';
import { ChevronDown, ChevronsUpDown, LogOut, Settings, Building2 } from 'lucide-react';
import { useState } from 'react';
import { cn } from '@/lib/utils';
import { navigationConfig, type NavGroup } from '@/lib/nav-config';
import { PageProps, SidebarBadges } from '@/types';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarRail,
} from '@/Components/ui/sidebar';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/Components/ui/dropdown-menu';
import { Avatar, AvatarFallback } from '@/Components/ui/avatar';
function getInitials(name: string): string {
return name
.split(' ')
.map(w => w[0])
.join('')
.toUpperCase()
.slice(0, 2);
}
function NavCollapsible({
item,
badges,
}: {
item: NavGroup['items'][number] & { children?: { title: string; href: string; routeMatch: string; badgeKey?: string }[] };
badges?: SidebarBadges;
}) {
const isAnyChildActive = item.children?.some(child => {
try { return route().current(child.routeMatch); } catch { return false; }
}) ?? false;
const isParentActive = (() => {
try { return route().current(item.routeMatch); } catch { return false; }
})();
const isActive = isAnyChildActive || isParentActive;
const [open, setOpen] = useState(isActive);
const Icon = item.icon;
const totalBadge = item.children?.reduce((sum, child) => {
if (child.badgeKey && badges) {
return sum + (badges[child.badgeKey as keyof SidebarBadges] ?? 0);
}
return sum;
}, 0) ?? 0;
return (
<SidebarMenuItem>
<SidebarMenuButton
onClick={() => setOpen(!open)}
isActive={isActive}
tooltip={item.title}
>
<Icon className="size-4" />
<span className="flex-1">{item.title}</span>
{totalBadge > 0 && !open && (
<span className="ml-auto flex h-5 min-w-5 items-center justify-center rounded-full bg-emerald-500/20 px-1.5 text-[10px] font-semibold text-emerald-400 tabular-nums">
{totalBadge}
</span>
)}
<ChevronDown
className={cn(
'ml-auto size-4 shrink-0 transition-transform duration-200',
open && 'rotate-180'
)}
/>
</SidebarMenuButton>
{open && (
<SidebarMenuSub>
{item.children!.map(child => {
const childActive = (() => {
try { return route().current(child.routeMatch); } catch { return false; }
})();
const badgeCount = child.badgeKey && badges
? badges[child.badgeKey as keyof SidebarBadges] ?? 0
: 0;
return (
<SidebarMenuSubItem key={child.href}>
<SidebarMenuSubButton
render={<Link href={route(child.href)} />}
isActive={childActive}
>
<span>{child.title}</span>
{badgeCount > 0 && (
<span className="ml-auto flex h-4 min-w-4 items-center justify-center rounded-full bg-emerald-500/20 px-1 text-[10px] font-semibold text-emerald-400 tabular-nums">
{badgeCount}
</span>
)}
</SidebarMenuSubButton>
</SidebarMenuSubItem>
);
})}
</SidebarMenuSub>
)}
</SidebarMenuItem>
);
}
export default function AppSidebar() {
const { auth, sidebarBadges } = usePage<PageProps>().props;
return (
<Sidebar collapsible="icon" className="border-r-0">
{/* Header */}
<SidebarHeader className="border-b border-sidebar-border px-3 py-4">
<Link href="/" className="flex items-center gap-2.5">
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-emerald-500/15 text-emerald-400">
<Building2 className="size-4" />
</div>
<div className="flex flex-col gap-0.5 leading-none group-data-[collapsible=icon]:hidden">
<span className="text-sm font-bold tracking-tight">GSB Construction</span>
<span className="text-[10px] font-medium text-sidebar-foreground/50 uppercase tracking-widest">ERP System</span>
</div>
</Link>
</SidebarHeader>
{/* Navigation */}
<SidebarContent>
{navigationConfig.map(group => {
// Filter items based on permissions
const filteredItems = group.items.filter(item => {
if (auth.roles.includes('Super Admin') || auth.roles.includes('admin')) return true;
if (!item.permissions || item.permissions.length === 0) return true;
return item.permissions.some(p => auth.permissions.includes(p));
});
if (filteredItems.length === 0) return null;
return (
<SidebarGroup key={group.label}>
<SidebarGroupLabel className="text-[10px] uppercase tracking-widest font-semibold text-sidebar-foreground/40">
{group.label}
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{filteredItems.map(item => {
if (item.children && item.children.length > 0) {
return (
<NavCollapsible
key={item.href}
item={item}
badges={sidebarBadges}
/>
);
}
const isActive = (() => {
try { return route().current(item.routeMatch); } catch { return false; }
})();
const badgeCount = item.badgeKey && sidebarBadges
? sidebarBadges[item.badgeKey] ?? 0
: 0;
const Icon = item.icon;
return (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton
render={<Link href={route(item.href)} />}
isActive={isActive}
tooltip={item.title}
>
<Icon className="size-4" />
<span>{item.title}</span>
</SidebarMenuButton>
{badgeCount > 0 && (
<SidebarMenuBadge className="bg-emerald-500/20 text-emerald-400 text-[10px] font-semibold">
{badgeCount}
</SidebarMenuBadge>
)}
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
})}
</SidebarContent>
<SidebarRail />
{/* Footer — User Profile */}
<SidebarFooter className="border-t border-sidebar-border">
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<SidebarMenuButton
size="lg"
className="data-open:bg-sidebar-accent data-open:text-sidebar-accent-foreground"
render={<DropdownMenuTrigger />}
>
<Avatar className="size-8 rounded-lg">
<AvatarFallback className="rounded-lg bg-emerald-500/15 text-emerald-400 text-xs font-bold">
{getInitials(auth.user.name)}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{auth.user.name}</span>
<span className="truncate text-xs text-sidebar-foreground/60">{auth.user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
<DropdownMenuContent
className="w-[--anchor-width] min-w-56 rounded-lg"
side="top"
align="start"
sideOffset={4}
>
<DropdownMenuItem render={<Link href={route('profile.edit')} />}>
<Settings className="size-4" />
Profile Settings
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
render={<Link href={route('logout')} method="post" as="button" className="w-full" />}
>
<LogOut className="size-4" />
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
);
}