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

257 lines
12 KiB
TypeScript

import { Link, usePage } from '@inertiajs/react';
import { ChevronDown, 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,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarRail,
} from '@/Components/ui/sidebar';
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 {
if (child.routeMatch === 'bids.*' && route().current('bids.my')) {
return false;
}
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 {
if (child.routeMatch === 'bids.*' && route().current('bids.my')) {
return false;
}
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, child.params)} />}
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 => {
const filteredItems = group.items.map(item => {
const isAdminUser = auth.user.user_type === 'admin' ||
auth.roles.includes('Super Admin') ||
auth.roles.includes('admin');
const isBiddingManager = auth.user.user_type === 'admin' ||
auth.roles.includes('Super Admin') ||
auth.roles.includes('admin') ||
auth.roles.includes('Project Manager');
const isSiteOperationsUser = auth.roles.some(role => [
'Site Technical',
'Construction Supervisor',
'Site Operations',
'Site Engineer',
'Site Supervisor',
].includes(role));
const isContractorPortalUser = auth.user.user_type === 'contractor' || auth.user.contractor_id !== null;
const isContractorAdminUser = !isSiteOperationsUser && (
auth.roles.includes('Main Contractor Admin') ||
auth.roles.includes('Contractor Admin') ||
auth.user.user_type === 'contractor' ||
auth.user.contractor_id !== null
);
const hasParentAccess = item.title === 'Bidding'
? !isSiteOperationsUser && (isBiddingManager || isContractorPortalUser)
: isAdminUser ||
!item.permissions ||
item.permissions.length === 0 ||
item.permissions.some(p => auth.permissions.includes(p));
if (!hasParentAccess) return null;
if (item.children && item.children.length > 0) {
const filteredChildren = item.children.filter(child => {
if (child.href === 'retention.index') {
if (isSiteOperationsUser) return false;
return isAdminUser || isBiddingManager || isContractorAdminUser;
}
if (isAdminUser) return true;
if (!child.permissions || child.permissions.length === 0) return true;
return child.permissions.some(p => auth.permissions.includes(p));
});
if (filteredChildren.length === 0) return null;
return {
...item,
children: item.title === 'Bidding'
? filteredChildren.filter(child => {
if (child.title === 'Bid Packages') return isBiddingManager;
if (child.title === 'My Bids') return isContractorPortalUser;
return true;
})
: filteredChildren
};
}
return item;
}).filter(Boolean) as NavGroup['items'];
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, item.params)} />}
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 />
</Sidebar>
);
}