0052725b88
- Created UserViewSheet component to display user details including username, full name, role, status, and timestamps. - Created RoleViewSheet component to display role details including name, level, status, creation date, and associated permissions. - Introduced permissions configuration in a new permissions.ts file, defining categories and permission codes for better management. - Implemented middleware for authentication, ensuring access control for protected routes.
224 lines
7.8 KiB
TypeScript
224 lines
7.8 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { useEffect, useState } from 'react'
|
|
import { usePathname } from 'next/navigation'
|
|
import { HugeiconsIcon } from '@hugeicons/react'
|
|
import { ChevronRightIcon, UserIcon } from '@hugeicons/core-free-icons'
|
|
|
|
import {
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarFooter,
|
|
SidebarGroup,
|
|
SidebarHeader,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
SidebarMenuSub,
|
|
SidebarMenuSubButton,
|
|
SidebarMenuSubItem,
|
|
SidebarRail,
|
|
} from '@/components/ui/sidebar'
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from '@/components/ui/collapsible'
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip'
|
|
import { Button } from '@/components/ui/button'
|
|
import { useAuth } from '@/contexts/auth-context'
|
|
import { DASHBOARD_ITEM, CONFIGURATION_ITEM, NAV_GROUPS, type NavGroup, type NavItem } from '@/config/nav'
|
|
|
|
function canSee(item: NavItem, permissions: Set<string>): boolean {
|
|
if (!item.permission) return true
|
|
return permissions.has(item.permission)
|
|
}
|
|
|
|
function isGroupVisible(group: NavGroup, permissions: Set<string>): boolean {
|
|
return group.items.some((item) => canSee(item, permissions))
|
|
}
|
|
|
|
export function AppSidebar() {
|
|
const { user, permissions, logout } = useAuth()
|
|
const pathname = usePathname()
|
|
|
|
const dashboardActive = pathname === DASHBOARD_ITEM.href
|
|
const configurationActive =
|
|
pathname === CONFIGURATION_ITEM.href || pathname.startsWith(CONFIGURATION_ITEM.href + '/')
|
|
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({})
|
|
|
|
useEffect(() => {
|
|
setOpenGroups((prev) => {
|
|
const next = { ...prev }
|
|
|
|
for (const group of NAV_GROUPS) {
|
|
if (!isGroupVisible(group, permissions)) continue
|
|
|
|
const visibleItems = group.items.filter((item) => canSee(item, permissions))
|
|
const groupActive = visibleItems.some(
|
|
(item) =>
|
|
pathname === item.href ||
|
|
(item.href !== '/' && pathname.startsWith(item.href)),
|
|
)
|
|
|
|
if (groupActive) {
|
|
next[group.title] = true
|
|
} else if (!(group.title in next)) {
|
|
next[group.title] = false
|
|
}
|
|
}
|
|
|
|
return next
|
|
})
|
|
}, [pathname, permissions])
|
|
|
|
return (
|
|
<Sidebar collapsible="icon">
|
|
<SidebarHeader>
|
|
<div className="flex items-center gap-2 px-2 py-1">
|
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold">
|
|
D
|
|
</div>
|
|
<span className="truncate font-semibold text-sidebar-foreground group-data-[collapsible=icon]:hidden">
|
|
Distribution
|
|
</span>
|
|
</div>
|
|
</SidebarHeader>
|
|
|
|
<SidebarContent className="gap-0">
|
|
{/* Dashboard — standalone, not inside a collapsible group */}
|
|
<SidebarGroup className="py-1">
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton
|
|
render={<Link href={DASHBOARD_ITEM.href} />}
|
|
isActive={dashboardActive}
|
|
tooltip={DASHBOARD_ITEM.title}
|
|
>
|
|
<HugeiconsIcon icon={DASHBOARD_ITEM.icon} size={18} />
|
|
<span>{DASHBOARD_ITEM.title}</span>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
|
|
{/* Collapsible groups */}
|
|
{NAV_GROUPS.map((group) => {
|
|
if (!isGroupVisible(group, permissions)) return null
|
|
|
|
const visibleItems = group.items.filter((item) =>
|
|
canSee(item, permissions),
|
|
)
|
|
const groupActive = visibleItems.some(
|
|
(item) =>
|
|
pathname === item.href ||
|
|
(item.href !== '/' && pathname.startsWith(item.href)),
|
|
)
|
|
|
|
return (
|
|
<Collapsible
|
|
key={group.title}
|
|
open={openGroups[group.title] ?? groupActive}
|
|
onOpenChange={(open) =>
|
|
setOpenGroups((prev) => ({ ...prev, [group.title]: open }))
|
|
}
|
|
className="group/collapsible"
|
|
>
|
|
<SidebarGroup className="py-0">
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton
|
|
render={<CollapsibleTrigger />}
|
|
isActive={groupActive}
|
|
tooltip={group.title}
|
|
>
|
|
<HugeiconsIcon icon={group.icon} size={18} />
|
|
<span>{group.title}</span>
|
|
<HugeiconsIcon
|
|
icon={ChevronRightIcon}
|
|
size={14}
|
|
className="ml-auto shrink-0 transition-transform duration-200 group-data-[open]/collapsible:rotate-90 group-data-[collapsible=icon]:hidden"
|
|
/>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
|
|
<CollapsibleContent>
|
|
<SidebarMenuSub>
|
|
{visibleItems.map((item) => {
|
|
const isActive =
|
|
pathname === item.href ||
|
|
(item.href !== '/' &&
|
|
pathname.startsWith(item.href))
|
|
return (
|
|
<Tooltip key={item.href}>
|
|
<SidebarMenuSubItem>
|
|
<SidebarMenuSubButton
|
|
render={<TooltipTrigger render={<Link href={item.href} />} />}
|
|
isActive={isActive}
|
|
>
|
|
<HugeiconsIcon icon={item.icon} size={15} />
|
|
<span>{item.title}</span>
|
|
</SidebarMenuSubButton>
|
|
</SidebarMenuSubItem>
|
|
<TooltipContent side="right">{item.description ?? item.title}</TooltipContent>
|
|
</Tooltip>
|
|
)
|
|
})}
|
|
</SidebarMenuSub>
|
|
</CollapsibleContent>
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
</Collapsible>
|
|
)
|
|
})}
|
|
|
|
{/* Configuration — standalone, no sub-nav */}
|
|
{canSee(CONFIGURATION_ITEM, permissions) && (
|
|
<SidebarGroup className="py-1">
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton
|
|
render={<Link href={CONFIGURATION_ITEM.href} />}
|
|
isActive={configurationActive}
|
|
tooltip={CONFIGURATION_ITEM.title}
|
|
>
|
|
<HugeiconsIcon icon={CONFIGURATION_ITEM.icon} size={18} />
|
|
<span>{CONFIGURATION_ITEM.title}</span>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
)}
|
|
</SidebarContent>
|
|
|
|
<SidebarFooter>
|
|
<div className="flex items-center gap-2 px-2 py-1 group-data-[collapsible=icon]:justify-center">
|
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
<HugeiconsIcon icon={UserIcon} size={16} />
|
|
</div>
|
|
<div className="flex min-w-0 flex-1 flex-col group-data-[collapsible=icon]:hidden">
|
|
<span className="truncate text-sm font-medium text-sidebar-foreground">
|
|
{user?.username ?? '—'}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={logout}
|
|
className="h-auto justify-start p-0 text-xs text-muted-foreground hover:text-foreground hover:bg-transparent"
|
|
>
|
|
Sign out
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</SidebarFooter>
|
|
|
|
<SidebarRail />
|
|
</Sidebar>
|
|
)
|
|
}
|