256 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|