mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 15:48:11 +00:00
fix ui
This commit is contained in:
@@ -1,48 +1,48 @@
|
||||
import { FooterDropdownMenu } from "./navigationtabs";
|
||||
import { ChevronUp, User2 } from "lucide-react";
|
||||
|
||||
const recordOfficerFooters: FooterDropdownMenu | null = {
|
||||
label: "Settings",
|
||||
triggerIcon: ChevronUp,
|
||||
items: [
|
||||
{
|
||||
label: "Settings",
|
||||
href: "#/officersettings",
|
||||
},
|
||||
{
|
||||
label: "Letter Templates",
|
||||
href: "#/letterTemplates",
|
||||
},
|
||||
],
|
||||
};
|
||||
const userFooters = (permissions: string[]): FooterDropdownMenu => {
|
||||
const dropDown: FooterDropdownMenu = {
|
||||
icon: User2,
|
||||
label: "Settings",
|
||||
triggerIcon: ChevronUp,
|
||||
items: [
|
||||
{
|
||||
label: "Sign out",
|
||||
action: () => {},
|
||||
},
|
||||
{ label: "Delegation", href: "#/delegation" },
|
||||
],
|
||||
};
|
||||
if (permissions.includes("")) {
|
||||
dropDown.items.push({
|
||||
label: "Upload Teeter and Signature",
|
||||
href: "#/uploadTeeterandSignature",
|
||||
});
|
||||
}
|
||||
|
||||
return dropDown;
|
||||
};
|
||||
export const footerItems = (
|
||||
permissions: string[]
|
||||
): FooterDropdownMenu | null => {
|
||||
if (permissions.includes("can:dispatchRecords")) {
|
||||
return recordOfficerFooters;
|
||||
} else {
|
||||
return userFooters(permissions);
|
||||
}
|
||||
};
|
||||
import { FooterDropdownMenu } from "./navigationtabs";
|
||||
import { ChevronUp, User2 } from "lucide-react";
|
||||
|
||||
const recordOfficerFooters: FooterDropdownMenu | null = {
|
||||
label: "Settings",
|
||||
triggerIcon: ChevronUp,
|
||||
items: [
|
||||
{
|
||||
label: "Settings",
|
||||
href: "#/officersettings",
|
||||
},
|
||||
{
|
||||
label: "Letter Templates",
|
||||
href: "#/letterTemplates",
|
||||
},
|
||||
],
|
||||
};
|
||||
const userFooters = (permissions: string[]): FooterDropdownMenu => {
|
||||
const dropDown: FooterDropdownMenu = {
|
||||
icon: User2,
|
||||
label: "Settings",
|
||||
triggerIcon: ChevronUp,
|
||||
items: [
|
||||
{
|
||||
label: "Sign out",
|
||||
action: () => {},
|
||||
},
|
||||
{ label: "Delegation", href: "#/delegation" },
|
||||
],
|
||||
};
|
||||
if (permissions.includes("")) {
|
||||
dropDown.items.push({
|
||||
label: "Upload Teeter and Signature",
|
||||
href: "#/uploadTeeterandSignature",
|
||||
});
|
||||
}
|
||||
|
||||
return dropDown;
|
||||
};
|
||||
export const footerItems = (
|
||||
permissions: string[]
|
||||
): FooterDropdownMenu | null => {
|
||||
if (permissions.includes("can:dispatchRecords")) {
|
||||
return recordOfficerFooters;
|
||||
} else {
|
||||
return userFooters(permissions);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,121 +1,121 @@
|
||||
import {
|
||||
Download,
|
||||
//Calendar,
|
||||
FileText,
|
||||
//Home,
|
||||
//Inbox,
|
||||
LayoutGrid,
|
||||
//Search,
|
||||
SquareCheck,
|
||||
Upload,
|
||||
//Settings,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { NavOptions } from "./navigationtabs";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
// Management section items
|
||||
export const managementItems: NavOptions[] = [
|
||||
{
|
||||
title: t("Record Management") || "Record Management",
|
||||
url: "/record-management/userRecords",
|
||||
icon: FileText,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
title: t("User Management") || "User Management",
|
||||
url: "/user-management",
|
||||
icon: Users,
|
||||
perms: ["view:user-management", "manage:users"],
|
||||
roles: ["super_admin", "admin", "unit_admin"],
|
||||
},
|
||||
// {
|
||||
// title: t("Workflow Management"),
|
||||
// url: "workflowManagement",
|
||||
// icon: GitBranch,
|
||||
// },
|
||||
// {
|
||||
// title: t("Project Management"),
|
||||
// url: "projectManagement",
|
||||
// icon: Briefcase,
|
||||
// },
|
||||
// {
|
||||
// title: t("Document Management"),
|
||||
// url: "documentManagement",
|
||||
// icon: FileArchive,
|
||||
// },
|
||||
// {
|
||||
// title: t("Knowledge & Asset Management"),
|
||||
// url: "knowledgeManagement",
|
||||
// icon: BookOpen,
|
||||
// },
|
||||
// {
|
||||
// title: t("Report Management"),
|
||||
// url: "reportManagement",
|
||||
// icon: BarChart3,
|
||||
// },
|
||||
];
|
||||
|
||||
export const useNavItems = (permissions: string[]): NavOptions[] => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Main navigation items based on the image
|
||||
const mainNavItems: NavOptions[] = [
|
||||
{
|
||||
title: t("nav.dashboard"),
|
||||
url: "dashboard",
|
||||
isActive: true,
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
];
|
||||
|
||||
const userNavs: NavOptions[] = [
|
||||
{
|
||||
title: t("nav.dashboard"),
|
||||
url: "dashboard",
|
||||
isActive: true,
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
{
|
||||
title: t("nav.outgoing"),
|
||||
url: "userRecords",
|
||||
icon: Upload,
|
||||
},
|
||||
{
|
||||
title: t("nav.incoming"),
|
||||
url: "userIncoming",
|
||||
icon: Download,
|
||||
count: true,
|
||||
items: [
|
||||
{
|
||||
title: t("nav.external"),
|
||||
url: "userIncoming",
|
||||
count: true,
|
||||
},
|
||||
{
|
||||
title: t("nav.internal"),
|
||||
url: "userIncoming/internal",
|
||||
count: true,
|
||||
},
|
||||
{
|
||||
title: t("nav.cc"),
|
||||
url: "userIncoming/cc",
|
||||
count: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const approvalPermissionKeys = ["can:sendToRO", "can:approveRecords"];
|
||||
if (approvalPermissionKeys.some((key) => permissions.includes(key))) {
|
||||
userNavs.push({
|
||||
title: t("nav.approval"),
|
||||
url: "approval",
|
||||
icon: SquareCheck,
|
||||
count: true,
|
||||
items: [],
|
||||
});
|
||||
}
|
||||
|
||||
return [...mainNavItems, ...managementItems];
|
||||
};
|
||||
import {
|
||||
Download,
|
||||
//Calendar,
|
||||
FileText,
|
||||
//Home,
|
||||
//Inbox,
|
||||
LayoutGrid,
|
||||
//Search,
|
||||
SquareCheck,
|
||||
Upload,
|
||||
//Settings,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { NavOptions } from "./navigationtabs";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
// Management section items
|
||||
export const managementItems: NavOptions[] = [
|
||||
{
|
||||
title: t("Record Management") || "Record Management",
|
||||
url: "/record-management/userRecords",
|
||||
icon: FileText,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
title: t("User Management") || "User Management",
|
||||
url: "/user-management",
|
||||
icon: Users,
|
||||
perms: ["view:user-management", "manage:users"],
|
||||
roles: ["super_admin", "admin", "unit_admin"],
|
||||
},
|
||||
// {
|
||||
// title: t("Workflow Management"),
|
||||
// url: "workflowManagement",
|
||||
// icon: GitBranch,
|
||||
// },
|
||||
// {
|
||||
// title: t("Project Management"),
|
||||
// url: "projectManagement",
|
||||
// icon: Briefcase,
|
||||
// },
|
||||
// {
|
||||
// title: t("Document Management"),
|
||||
// url: "documentManagement",
|
||||
// icon: FileArchive,
|
||||
// },
|
||||
// {
|
||||
// title: t("Knowledge & Asset Management"),
|
||||
// url: "knowledgeManagement",
|
||||
// icon: BookOpen,
|
||||
// },
|
||||
// {
|
||||
// title: t("Report Management"),
|
||||
// url: "reportManagement",
|
||||
// icon: BarChart3,
|
||||
// },
|
||||
];
|
||||
|
||||
export const useNavItems = (permissions: string[]): NavOptions[] => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Main navigation items based on the image
|
||||
const mainNavItems: NavOptions[] = [
|
||||
{
|
||||
title: t("nav.dashboard"),
|
||||
url: "dashboard",
|
||||
isActive: true,
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
];
|
||||
|
||||
const userNavs: NavOptions[] = [
|
||||
{
|
||||
title: t("nav.dashboard"),
|
||||
url: "dashboard",
|
||||
isActive: true,
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
{
|
||||
title: t("nav.outgoing"),
|
||||
url: "userRecords",
|
||||
icon: Upload,
|
||||
},
|
||||
{
|
||||
title: t("nav.incoming"),
|
||||
url: "userIncoming",
|
||||
icon: Download,
|
||||
count: true,
|
||||
items: [
|
||||
{
|
||||
title: t("nav.external"),
|
||||
url: "userIncoming",
|
||||
count: true,
|
||||
},
|
||||
{
|
||||
title: t("nav.internal"),
|
||||
url: "userIncoming/internal",
|
||||
count: true,
|
||||
},
|
||||
{
|
||||
title: t("nav.cc"),
|
||||
url: "userIncoming/cc",
|
||||
count: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const approvalPermissionKeys = ["can:sendToRO", "can:approveRecords"];
|
||||
if (approvalPermissionKeys.some((key) => permissions.includes(key))) {
|
||||
userNavs.push({
|
||||
title: t("nav.approval"),
|
||||
url: "approval",
|
||||
icon: SquareCheck,
|
||||
count: true,
|
||||
items: [],
|
||||
});
|
||||
}
|
||||
|
||||
return [...mainNavItems, ...managementItems];
|
||||
};
|
||||
|
||||
@@ -1,262 +1,262 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { Button } from "@/shared/common/ui/button";
|
||||
|
||||
interface SidebarToggleProps {
|
||||
isOpen: boolean | undefined;
|
||||
setIsOpen?: () => void;
|
||||
}
|
||||
|
||||
export function SidebarToggle({ isOpen, setIsOpen }: SidebarToggleProps) {
|
||||
return (
|
||||
<div className="invisible lg:visible absolute top-[12px] -right-[16px] z-20">
|
||||
<Button
|
||||
onClick={() => setIsOpen?.()}
|
||||
className="rounded-md w-8 h-8"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
<ChevronLeft
|
||||
className={cn(
|
||||
"h-4 w-4 transition-transform ease-in-out duration-700",
|
||||
isOpen === false ? "rotate-180" : "rotate-0"
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { Button } from "@/shared/common/ui/button";
|
||||
|
||||
interface SidebarToggleProps {
|
||||
isOpen: boolean | undefined;
|
||||
setIsOpen?: () => void;
|
||||
}
|
||||
|
||||
export function SidebarToggle({ isOpen, setIsOpen }: SidebarToggleProps) {
|
||||
return (
|
||||
<div className="invisible lg:visible absolute top-[12px] -right-[16px] z-20">
|
||||
<Button
|
||||
onClick={() => setIsOpen?.()}
|
||||
className="rounded-md w-8 h-8"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
<ChevronLeft
|
||||
className={cn(
|
||||
"h-4 w-4 transition-transform ease-in-out duration-700",
|
||||
isOpen === false ? "rotate-180" : "rotate-0"
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user