Files
edr-platform/apps/edr-freight-web/backoffice/src/shared/navigation/navigationtabs.tsx
natib21 e6e44e773b fix ui
2026-07-10 11:25:59 +00:00

263 lines
10 KiB
TypeScript

import { ChevronRight, LucideIcon, LucideProps, Menu } from "lucide-react";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarFooter,
SidebarMenuBadge,
SidebarHeader,
useSidebar,
} from "@/shared/common/ui/sidebar";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/shared/common/ui/collapsible";
import { managementItems } from "./navigationItems";
import { NavLink, useLocation } from "react-router-dom";
import { useAuth } from "@/shared/context/AuthContext";
import { hasAccess } from "../utils/permissionHelper";
import { Button } from "@/shared/common/ui/button";
import { useTenantConfig } from "@/layout/components/TenantConfig";
// Routes nested under /userRecords/ (like editUserRecord/:id) trigger
// NavLink's auto-highlight on the Outgoing item even when the user arrived
// from Approval or Collaborations. `?from=...` is set by those entry points
// so the sidebar can ignore the auto-match and keep Outgoing inactive.
const isOutgoingNavSuppressedFor = (
pathname: string,
search: string,
): boolean => {
if (!pathname.includes("/userRecords/editUserRecord/")) return false;
const from = new URLSearchParams(search).get("from");
return from === "approval" || from === "collaborations";
};
export interface NavOptions {
title: string;
url: string;
isActive?: boolean;
icon?: React.ForwardRefExoticComponent<
Omit<LucideProps, "ref"> & React.RefAttributes<SVGSVGElement>
>;
items?: NavOptions[];
count?: any;
perms?: string[];
roles?: string[];
}
export interface FooterDropdownItem {
label: string;
href?: string;
action?: () => void;
}
export interface FooterDropdownMenu {
icon?: LucideIcon;
label: string;
triggerIcon?: LucideIcon;
items: FooterDropdownItem[];
}
export function NavigationTabs() {
const { user } = useAuth();
const userPermissions = user?.permissions.map((r) => r.key) || [];
const userRoles = user?.roles?.map((r) => r.key) || [];
const { toggleSidebar } = useSidebar();
const location = useLocation();
const { config: tenantConfig } = useTenantConfig();
const suppressOutgoingHighlight = isOutgoingNavSuppressedFor(
location.pathname,
location.search,
);
return (
<Sidebar collapsible="icon" className="border-r bg-white">
<SidebarHeader className="p-4 border-b">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onClick={toggleSidebar}
className="p-1"
>
<Menu className="h-5 w-5" />
</Button>
<div className="w-8 h-8 bg-primary-500 rounded-md flex items-center justify-center">
<svg
width="20"
height="20"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 36H36"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinecap="round"
/>
<path
d="M31.2 20.4H26.4C23.4216 20.4 22.8 21.0216 22.8 24V36H34.8V24C34.8 21.0216 34.1784 20.4 31.2 20.4Z"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinejoin="round"
/>
<path
d="M27.6002 36H13.2002V15.6C13.2002 12.6216 13.8218 12 16.8002 12H24.0002C26.9786 12 27.6002 12.6216 27.6002 15.6V20.4"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinejoin="round"
/>
<path
d="M13.2002 16.8H16.8002M13.2002 21.6H16.8002M13.2002 26.4H16.8002"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinecap="round"
/>
<path
d="M27.6001 25.2H30.0001M27.6001 28.8H30.0001"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinecap="round"
/>
<path
d="M28.8 36V32.4"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<div>
<h3 className="font-semibold text-gray-800">{tenantConfig.appName}</h3>
<p className="text-xs text-gray-500">system</p>
</div>
</div>
</SidebarHeader>
<SidebarContent className="p-2">
<SidebarGroup>
<SidebarMenu>
{managementItems
.filter((item) => hasAccess(item, userPermissions, userRoles))
.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={
item.isActive && item.items ? item.items.length > 0 : false
}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
className={
item.title?.includes("Management")
? "text-gray-600 hover:bg-gray-100 rounded-md my-1"
: "text-gray-800 hover:bg-gray-100 rounded-md my-1 font-medium"
}
>
{item.icon && (
<item.icon
className={
item.title?.includes("Management")
? "h-5 w-5 text-gray-500"
: "h-5 w-5 text-gray-700"
}
/>
)}
<SidebarMenuSubButton asChild>
<NavLink
to={item.url}
className={({ isActive }) => {
const effectiveActive =
isActive &&
!(
suppressOutgoingHighlight &&
item.url.endsWith("/userRecords")
);
return `flex items-center px-3 py-2 rounded-md text-sm transition-colors ${
effectiveActive
? item.url.startsWith("/user-management")
? "bg-primary-100 text-primary-900"
: "bg-primary text-primary-foreground"
: "text-gray-700 hover:bg-gray-100"
}`;
}}
>
<span>{item.title}</span>
</NavLink>
</SidebarMenuSubButton>
{item.count && (
<SidebarMenuBadge>{item.count}</SidebarMenuBadge>
)}
{item.items && item.items.length > 0 && (
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
)}
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
{item.items && item.items.length > 0 && (
<SidebarMenuSub>
{item.items
.filter((sItem) =>
hasAccess(sItem, userPermissions, userRoles)
)
.map((subItem: NavOptions) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<NavLink
to={subItem.url}
className={({ isActive }) => {
const effectiveActive =
isActive &&
!(
suppressOutgoingHighlight &&
(subItem.url === "userRecords" ||
subItem.url.endsWith("/userRecords"))
);
return `flex items-center px-3 py-2 rounded-md text-sm transition-colors ${
effectiveActive
? subItem.url.startsWith("/user-management")
? "bg-primary-100 text-primary-900"
: "bg-primary text-primary-foreground"
: "text-gray-700 hover:bg-gray-100"
}`;
}}
>
<span>{subItem.title}</span>
</NavLink>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
)}
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="p-4 border-t mt-auto">
<div className="text-xs text-gray-500 text-center">
Powered by <span className="font-semibold">TRIA</span>
</div>
</SidebarFooter>
</Sidebar>
);
}