Spaces:
Sleeping
Sleeping
| import React, { useEffect, useState } from "react"; | |
| import { cn } from "@/lib/utils"; | |
| import { NavLink, useLocation } from "react-router-dom"; | |
| import { getVersionString } from "@/lib/version"; | |
| import { | |
| BarChart3, | |
| Briefcase, | |
| Calendar, | |
| CheckSquare, | |
| Clipboard, | |
| ClipboardCheck, | |
| Grid3x3, | |
| Layers, | |
| LayoutDashboard, | |
| MessageSquare, | |
| Settings, | |
| Users, | |
| ShieldCheck, | |
| Package, | |
| Clock, | |
| Key, | |
| Shield, | |
| Bell, | |
| FileText, | |
| Link, | |
| FolderOpen, | |
| Loader2, | |
| CalendarRange, | |
| CalendarClock, | |
| Bug, | |
| Award, | |
| Gauge, | |
| BadgeCheck, | |
| ListChecks, | |
| CalendarDays, | |
| MessageCircle, | |
| CalendarCheck, | |
| BookOpen, | |
| ChevronDown, | |
| ChevronRight, | |
| User, | |
| Download, | |
| ActivitySquare, | |
| Heart, | |
| Search, | |
| X, | |
| CalendarPlus | |
| } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import Logo from "@/components/ui/logo"; | |
| import { Separator } from "@/components/ui/separator"; | |
| import { ScrollArea } from "@/components/ui/scroll-area"; | |
| import { useAuth } from "@/lib/auth-context"; | |
| import { roleFeatureApi } from "@/services/roleFeatureApi"; | |
| import { useIsMobile } from "@/hooks/use-mobile"; | |
| interface SidebarProps { | |
| isOpen: boolean; | |
| setIsOpen: (value: boolean) => void; | |
| } | |
| // Map of feature IDs to their respective icon components | |
| const featureIconMap: Record<number, React.ReactNode> = { | |
| 1: <LayoutDashboard className="w-5 h-5" />, // Dashboard | |
| 2: <Briefcase className="w-5 h-5" />, // Projects | |
| 3: <Package className="w-5 h-5" />, // Deliverables | |
| 4: <Users className="w-5 h-5" />, // Teams | |
| 5: <Users className="w-5 h-5" />, // Employees | |
| 6: <ShieldCheck className="w-5 h-5" />, // Roles | |
| 7: <Key className="w-5 h-5" />, // Permissions | |
| 8: <Shield className="w-5 h-5" />, // Role Features | |
| 9: <Clipboard className="w-5 h-5" />, // Backlog | |
| 10: <Grid3x3 className="w-5 h-5" />, // Issues | |
| 11: <CheckSquare className="w-5 h-5" />, // Tasks | |
| 12: <Clock className="w-5 h-5" />, // Time Logs | |
| 13: <Calendar className="w-5 h-5" />, // Calendar | |
| 14: <BarChart3 className="w-5 h-5" />, // Reports | |
| 15: <MessageSquare className="w-5 h-5" />, // Messages | |
| 16: <Settings className="w-5 h-5" />, // Settings | |
| 17: <Bell className="w-5 h-5" />, // Notifications | |
| 18: <FileText className="w-5 h-5" />, // Audit Logs | |
| 19: <Link className="w-5 h-5" />, // Integrations | |
| 20: <FolderOpen className="w-5 h-5" />, // File Management | |
| 21: <ClipboardCheck className="w-5 h-5" />, // Attendance | |
| 22: <Calendar className="w-5 h-5" />, // Leaves | |
| 23: <CalendarRange className="w-5 h-5" />, // Leaves Admin | |
| 24: <Layers className="w-5 h-5" />, // Leave Types | |
| 25: <CheckSquare className="w-5 h-5" />, // Todos | |
| 26: <CalendarClock className="w-5 h-5" />, // Holidays | |
| 27: <Bug className="w-5 h-5" />, // Bugs & Defects | |
| 28: <Award className="w-5 h-5" />, // Appraisals | |
| 29: <Gauge className="w-5 h-5" />, // Appraisal Cycles | |
| 30: <ListChecks className="w-5 h-5" />, // Assessment Areas | |
| 31: <BadgeCheck className="w-5 h-5" />, // Competencies | |
| 32: <BarChart3 className="w-5 h-5" />, // Manager Dashboard | |
| 33: <CalendarCheck className="w-5 h-5" />, // Meeting Rooms | |
| 34: <BookOpen className="w-5 h-5" />, | |
| 35: <MessageCircle className="w-5 h-5" />, // Communications | |
| 36: <ClipboardCheck className="w-5 h-5" />, // Role Assessment Questions | |
| 37: <MessageSquare className="w-5 h-5" />, // HR Remarks | |
| 38: <MessageSquare className="w-5 h-5" />, // Management Remarks | |
| 39: <Bug className="w-5 h-5" />, // Production Bugs | |
| 40: <CheckSquare className="w-5 h-5" />, // Test Cases | |
| 41: <User className="w-5 h-5" />, // My Profile | |
| 42: <CalendarRange className="w-5 h-5" />, // Leave Balances | |
| 43: <Briefcase className="w-5 h-5" />, // Employee Projects | |
| 44: <Heart className="w-5 h-5" />, // Health Dashboard | |
| 45: <Package className="w-5 h-5" />, // Asset Management | |
| 46: <Package className="w-5 h-5" />, // System Details | |
| 47: <Package className="w-5 h-5" />, // Asset Assignments | |
| 48: <ClipboardCheck className="w-5 h-5" />, // Asset Requests (Employee) | |
| 49: <ShieldCheck className="w-5 h-5" />, // Asset Request Management (Admin) | |
| 50: <BarChart3 className="w-5 h-5" />, // Gantt Chart | |
| 51: <Users className="w-5 h-5" />, // Resource Allocation | |
| 52: <Layers className="w-5 h-5" />, // Feature Management | |
| 53: <ShieldCheck className="w-5 h-5" />, // Role Permissions | |
| 54: <CalendarDays className="w-5 h-5" />, // Work From Home | |
| 55: <CalendarPlus className="w-5 h-5" />, // Add Comp Off Request | |
| 56: <BarChart3 className="w-5 h-5" />, // PMP (Project Management Plan) | |
| }; | |
| // Map of feature IDs to their respective paths | |
| const featurePathMap: Record<number, string> = { | |
| 1: "/dashboard", | |
| 2: "/projects", | |
| 3: "/deliverables", | |
| 4: "/teams", | |
| 5: "/employees", | |
| 6: "/roles", | |
| 7: "/permissions", | |
| 8: "/role-features", | |
| 9: "/backlog", | |
| 10: "/issues", | |
| 11: "/tasks", | |
| 12: "/time-logs", | |
| 13: "/calendar", | |
| 14: "/reports", | |
| 15: "/messages", | |
| 16: "/settings", | |
| 17: "/notifications", | |
| 18: "/audit-logs", | |
| 19: "/integrations", | |
| 20: "/file-management", | |
| 21: "/attendance", | |
| 22: "/leaves", | |
| 23: "/leaves-admin", | |
| 24: "/leave-types", | |
| 25: "/todos", | |
| 26: "/holidays", | |
| 27: "/bugs", | |
| 28: "/appraisals/my-appraisals", | |
| 29: "/appraisals/cycles", | |
| 30: "/appraisals/assessment-areas", | |
| 31: "/appraisals/competencies", | |
| 32: "/appraisals/manager-dashboard", | |
| 33: "/meeting-room/rooms", | |
| 34: "/meeting-room/bookings", | |
| 35: "/communications", | |
| 36: "/appraisals/role-questions", | |
| 37: "/appraisals/hr-remarks", | |
| 38: "/appraisals/management-remarks", | |
| 39: "/production-bugs", | |
| 40: "/test-cases", | |
| 41: "/my-profile", | |
| 42: "/leave-balances", | |
| 43: "/employee-projects", | |
| 44: "/health-dashboard", | |
| 45: "/assets", | |
| 46: "/system-details", | |
| 47: "/asset-assignments", | |
| 48: "/asset-requests", | |
| 49: "/admin/asset-requests", | |
| 50: "/gantt-chart", | |
| 51: "/resource-allocation", | |
| 52: "/features", | |
| 53: "/role-permissions", | |
| 54: "/leaves?wfh=true", | |
| 55: "/leaves?compoff=true", | |
| 56: "/pmp", | |
| }; | |
| // Map of feature IDs to their titles | |
| const featureTitleMap: Record<number, string> = { | |
| 1: "Dashboard", | |
| 2: "Projects", | |
| 3: "Deliverables", | |
| 4: "Teams", | |
| 5: "Employees", | |
| 6: "Roles", | |
| 7: "Permissions", | |
| 8: "Role Features", | |
| 9: "Backlog", | |
| 10: "Features", | |
| 11: "Tasks", | |
| 12: "Time Logs", | |
| 13: "Calendar", | |
| 14: "Reports", | |
| 15: "Messages", | |
| 16: "Settings", | |
| 17: "Notifications", | |
| 18: "Audit Logs", | |
| 19: "Integrations", | |
| 20: "File Management", | |
| 21: "Attendance", | |
| 22: "My Leaves", | |
| 23: "Leaves Management", | |
| 24: "Leave Types", | |
| 25: "Todos", | |
| 26: "Holidays", | |
| 27: "Bugs", | |
| 28: "My Appraisals", | |
| 29: "Appraisal Cycles", | |
| 30: "Assessment Areas", | |
| 31: "Competencies", | |
| 32: "Manager Appraisals", | |
| 33: "Meeting Rooms", | |
| 34: "Room Bookings", | |
| 35: "Communications", | |
| 36: "Role Questions", | |
| 37: "HR Remarks", | |
| 38: "Management Remarks", | |
| 39: "Production Bugs", | |
| 40: "Test Cases", | |
| 41: "My Profile", | |
| 42: "Leave Balances", | |
| 43: "Employee Projects", | |
| 44: "System Health", | |
| 45: "Assets", | |
| 46: "System Details", | |
| 47: "Asset Assignments", | |
| 48: "My Asset Requests", | |
| 49: "Asset Request Management", | |
| 50: "Gantt Chart", | |
| 51: "Resource Allocation", | |
| 52: "Feature Management", | |
| 53: "Role Permissions", | |
| 54: "Work From Home", | |
| 55: "Add Comp Off Request", | |
| 56: "PMP", | |
| }; | |
| // Feature categories for grouped sidebar | |
| const featureCategories = { | |
| dashboard: { | |
| title: "Dashboard", | |
| icon: <LayoutDashboard className="w-5 h-5" />, | |
| features: [1] | |
| }, | |
| projects: { | |
| title: "Project Management", | |
| icon: <Briefcase className="w-5 h-5" />, | |
| features: [2, 3, 4, 9, 10, 50, 56] | |
| }, | |
| tasks: { | |
| title: "Task Management", | |
| icon: <CheckSquare className="w-5 h-5" />, | |
| features: [11, 12, 25] | |
| }, | |
| bugs: { | |
| title: "QA Management", | |
| icon: <Bug className="w-5 h-5" />, | |
| features: [27, 39, 40] | |
| }, | |
| hr: { | |
| title: "HR Management", | |
| icon: <Users className="w-5 h-5" />, | |
| features: [5, 21, 22, 23, 24, 26, 42, 43, 51, 54, 55] | |
| }, | |
| appraisal: { | |
| title: "Appraisals", | |
| icon: <Award className="w-5 h-5" />, | |
| features: [28, 29, 30, 31, 32, 36, 37, 38] | |
| }, | |
| meeting: { | |
| title: "Meetings", | |
| icon: <CalendarCheck className="w-5 h-5" />, | |
| features: [33, 34] | |
| }, | |
| communication: { | |
| title: "Communication", | |
| icon: <MessageCircle className="w-5 h-5" />, | |
| features: [15, 35] | |
| }, | |
| assets: { | |
| title: "Asset Management", | |
| icon: <Package className="w-5 h-5" />, | |
| features: [45, 46, 47, 48, 49] | |
| }, | |
| admin: { | |
| title: "Administration", | |
| icon: <Settings className="w-5 h-5" />, | |
| features: [6, 7, 8, 16, 17, 18, 19, 20, 44, 52, 53] // Added health dashboard, features, and role permissions to admin category | |
| }, | |
| reports: { | |
| title: "Reports & Analytics", | |
| icon: <BarChart3 className="w-5 h-5" />, | |
| features: [13, 14] | |
| } | |
| }; | |
| // Function to get the category for a feature | |
| const getFeatureCategory = (featureId: number): string => { | |
| for (const [category, details] of Object.entries(featureCategories)) { | |
| if (details.features.includes(featureId)) { | |
| return category; | |
| } | |
| } | |
| return "other"; // Default category | |
| }; | |
| // Type definitions for grouped navigation items | |
| interface NavItem { | |
| title: string; | |
| icon: React.ReactNode; | |
| path: string; | |
| featureId: number; | |
| } | |
| interface NavGroup { | |
| title: string; | |
| icon: React.ReactNode; | |
| items: NavItem[]; | |
| } | |
| const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => { | |
| const { userData, isAuthenticated } = useAuth(); | |
| const [navItems, setNavItems] = useState<NavItem[]>([]); | |
| const [groupedNavItems, setGroupedNavItems] = useState<Record<string, NavGroup>>({}); | |
| const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({ | |
| // Default expanded groups | |
| dashboard: true, | |
| tasks: true, | |
| bugs: true | |
| }); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const location = useLocation(); | |
| const isMobile = useIsMobile(); | |
| // Filter nav items based on search query | |
| const filteredGroupedNavItems = React.useMemo(() => { | |
| if (!searchQuery.trim()) return groupedNavItems; | |
| const query = searchQuery.toLowerCase(); | |
| const filtered: Record<string, NavGroup> = {}; | |
| Object.entries(groupedNavItems).forEach(([category, group]) => { | |
| const filteredItems = group.items.filter(item => | |
| item.title.toLowerCase().includes(query) | |
| ); | |
| if (filteredItems.length > 0) { | |
| filtered[category] = { | |
| ...group, | |
| items: filteredItems | |
| }; | |
| } | |
| }); | |
| return filtered; | |
| }, [groupedNavItems, searchQuery]); | |
| // Auto-expand groups when searching | |
| useEffect(() => { | |
| if (searchQuery.trim()) { | |
| const newExpandedGroups: Record<string, boolean> = { ...expandedGroups }; | |
| Object.keys(filteredGroupedNavItems).forEach(category => { | |
| newExpandedGroups[category] = true; | |
| }); | |
| setExpandedGroups(newExpandedGroups); | |
| } | |
| }, [searchQuery, filteredGroupedNavItems]); | |
| // Check if we're on the download app page | |
| const isDownloadPage = location.pathname === "/download-app"; | |
| // Initialize expanded state for all categories | |
| useEffect(() => { | |
| const initialExpandedState: Record<string, boolean> = {}; | |
| const currentPath = window.location.pathname; | |
| // Find the active item's category | |
| let activeCategory = ""; | |
| Object.entries(featureCategories).forEach(([category, details]) => { | |
| // First, mark all categories as collapsed | |
| initialExpandedState[category] = false; | |
| // Then find which category has the active item | |
| if (details.features.some(featureId => { | |
| const path = featurePathMap[featureId] || ""; | |
| return isNavItemActive(path); | |
| })) { | |
| activeCategory = category; | |
| } | |
| }); | |
| // Only expand the category containing the active item (and dashboard) | |
| initialExpandedState["dashboard"] = true; // Dashboard always open | |
| if (activeCategory) { | |
| initialExpandedState[activeCategory] = true; | |
| } | |
| setExpandedGroups(initialExpandedState); | |
| }, []); | |
| // Monitor path changes to update expanded groups | |
| useEffect(() => { | |
| const handleRouteChange = () => { | |
| const currentPath = window.location.pathname; | |
| let activeCategory = ""; | |
| // Find which category contains the active item | |
| Object.entries(featureCategories).forEach(([category, details]) => { | |
| if (details.features.some(featureId => { | |
| const path = featurePathMap[featureId] || ""; | |
| return isNavItemActive(path); | |
| })) { | |
| activeCategory = category; | |
| } | |
| }); | |
| // Update expanded state | |
| if (activeCategory) { | |
| setExpandedGroups(prev => ({ | |
| ...prev, | |
| [activeCategory]: true | |
| })); | |
| } | |
| }; | |
| // Call once to set initial state | |
| handleRouteChange(); | |
| // Add event listener for navigation changes | |
| window.addEventListener('popstate', handleRouteChange); | |
| return () => { | |
| window.removeEventListener('popstate', handleRouteChange); | |
| }; | |
| }, [window.location.pathname]); | |
| // Effect to fetch role features when authentication state changes | |
| useEffect(() => { | |
| if (isAuthenticated) { | |
| fetchRoleFeatures(); | |
| } | |
| }, [isAuthenticated, userData?.roleId]); | |
| // Toggle group expansion | |
| const toggleGroup = (category: string) => { | |
| setExpandedGroups(prev => ({ | |
| ...prev, | |
| [category]: !prev[category] | |
| })); | |
| }; | |
| const fetchRoleFeatures = async () => { | |
| try { | |
| setIsLoading(true); | |
| // Admin (roleId 21) gets access to all features | |
| if (userData?.roleId === 21) { | |
| const allNavItems = Object.keys(featurePathMap).map(featureId => { | |
| const id = parseInt(featureId); | |
| return { | |
| title: featureTitleMap[id] || "Unknown", | |
| icon: featureIconMap[id] || <Layers className="w-5 h-5" />, | |
| path: featurePathMap[id] || "/", | |
| featureId: id | |
| }; | |
| }); | |
| // Sort navigation items by feature ID | |
| allNavItems.sort((a, b) => a.featureId - b.featureId); | |
| setNavItems(allNavItems); | |
| // Group the nav items by category | |
| const grouped = groupNavItems(allNavItems); | |
| setGroupedNavItems(grouped); | |
| setIsLoading(false); | |
| return; | |
| } | |
| // For client role, only show the Production Bugs module | |
| if (userData?.roleName?.toLowerCase() === "client" || userData?.roleId === 7) { // Assuming 7 is the client roleId, but also check roleName | |
| const clientNavItems = [ | |
| { | |
| title: featureTitleMap[39] || "Production Bugs", // Production Bugs feature | |
| icon: featureIconMap[39] || <Bug className="w-5 h-5" />, | |
| path: featurePathMap[39] || "/production-bugs", | |
| featureId: 39 | |
| }, | |
| // Add Dashboard as well | |
| { | |
| title: featureTitleMap[1] || "Dashboard", | |
| icon: featureIconMap[1] || <LayoutDashboard className="w-5 h-5" />, | |
| path: featurePathMap[1] || "/", | |
| featureId: 1 | |
| } | |
| ]; | |
| setNavItems(clientNavItems); | |
| // Group the nav items by category | |
| const grouped = groupNavItems(clientNavItems); | |
| setGroupedNavItems(grouped); | |
| setIsLoading(false); | |
| return; | |
| } | |
| // For other roles, fetch their specific features | |
| if (userData?.roleId) { | |
| const roleFeatures = await roleFeatureApi.getByRoleId(userData.roleId); | |
| // Get unique feature IDs (a role might have multiple permissions for the same feature) | |
| const uniqueFeatureIds = Array.from(new Set(roleFeatures.map(rf => rf.featureId))); | |
| // Make sure we have at least the Dashboard, Issues, Leaves, and Todos | |
| const essentialFeatures = [1, 10, 22, 25, 26, 27, 33, 34, 35, 39, 40, 42, 43, 45, 46, 47, 48, 50, 51, 54, 55, 56]; // Dashboard, Issues, Leaves, Todos, Holidays, Bugs, Meeting Rooms, Room Bookings, Communications, Production Bugs, Test Cases, Leave Balances, Employee Projects, Assets, System Details, Asset Assignments, My Asset Requests, Gantt Chart, Resource Allocation, Work From Home, Add Comp Off Request, PMP | |
| essentialFeatures.forEach(featId => { | |
| if (!uniqueFeatureIds.includes(featId)) { | |
| uniqueFeatureIds.push(featId); | |
| } | |
| }); | |
| // Add Asset Request Management only for admin roles | |
| const isAssetAdmin = userData.roleId === 1 || userData.roleId === 21 || | |
| userData.roleName?.toLowerCase() === "admin"; | |
| if (isAssetAdmin && !uniqueFeatureIds.includes(49)) { | |
| uniqueFeatureIds.push(49); // Asset Request Management | |
| } | |
| // Add health dashboard only for CEO, COO, and Admin roles | |
| // Assuming roleId 1 = CEO, roleId 2 = COO, roleId 21 = Admin | |
| const isExecutive = userData.roleId === 1 || userData.roleId === 2 || userData.roleId === 21 || | |
| userData.roleName?.toLowerCase() === "ceo" || | |
| userData.roleName?.toLowerCase() === "coo" || | |
| userData.roleName?.toLowerCase() === "admin"; | |
| if (isExecutive && !uniqueFeatureIds.includes(44)) { | |
| uniqueFeatureIds.push(44); // Health Dashboard | |
| } | |
| // Always include Appraisal features for HR roles (HR and HR Manager) | |
| if (userData.roleId === 10 || userData.roleId === 11) { // Assuming roleId 10 and 11 for HR and HR Manager | |
| const appraisalFeatures = [28, 29, 30, 31, 32, 36, 37, 38]; // My Appraisals, Cycles, Areas, Competencies, Manager Dashboard, Role Questions, HR Remarks, Management Remarks | |
| appraisalFeatures.forEach(featId => { | |
| if (!uniqueFeatureIds.includes(featId)) { | |
| uniqueFeatureIds.push(featId); | |
| } | |
| }); | |
| } else { | |
| // For non-HR roles, at least add My Appraisals | |
| if (!uniqueFeatureIds.includes(28)) { | |
| uniqueFeatureIds.push(28); // My Appraisals | |
| } | |
| } | |
| // Map feature IDs to nav items | |
| const userNavItems = uniqueFeatureIds.map(featureId => ({ | |
| title: featureTitleMap[featureId] || "Unknown", | |
| icon: featureIconMap[featureId] || <Layers className="w-5 h-5" />, | |
| path: featurePathMap[featureId] || "/", | |
| featureId: featureId | |
| })); | |
| // Sort navigation items by featureId | |
| userNavItems.sort((a, b) => a.featureId - b.featureId); | |
| setNavItems(userNavItems); | |
| // Group the nav items by category | |
| const grouped = groupNavItems(userNavItems); | |
| setGroupedNavItems(grouped); | |
| } | |
| } catch (error) { | |
| console.error("Error fetching role features:", error); | |
| // If there's an error in fetching role features, add these essential items | |
| const fallbackItems = [ | |
| { | |
| title: "Dashboard", | |
| icon: <LayoutDashboard className="w-5 h-5" />, | |
| path: "/dashboard", | |
| featureId: 1 | |
| }, | |
| { | |
| title: "Features", | |
| icon: <Grid3x3 className="w-5 h-5" />, | |
| path: "/issues", | |
| featureId: 10 | |
| }, | |
| { | |
| title: "Meeting Rooms", | |
| icon: <CalendarCheck className="w-5 h-5" />, | |
| path: "/meeting-room/rooms", | |
| featureId: 33 | |
| }, | |
| { | |
| title: "Room Bookings", | |
| icon: <BookOpen className="w-5 h-5" />, | |
| path: "/meeting-room/bookings", | |
| featureId: 34 | |
| }, | |
| { | |
| title: "Communications", | |
| icon: <MessageCircle className="w-5 h-5" />, | |
| path: "/communications", | |
| featureId: 35 | |
| }, | |
| { | |
| title: "Bugs", | |
| icon: <Bug className="w-5 h-5" />, | |
| path: "/bugs", | |
| featureId: 27 | |
| }, | |
| { | |
| title: "Production Bugs", | |
| icon: <Bug className="w-5 h-5" />, | |
| path: "/production-bugs", | |
| featureId: 39 | |
| }, | |
| { | |
| title: "Test Cases", | |
| icon: <CheckSquare className="w-5 h-5" />, | |
| path: "/test-cases", | |
| featureId: 40 | |
| }, | |
| { | |
| title: "My Appraisals", | |
| icon: <Award className="w-5 h-5" />, | |
| path: "/appraisals/my-appraisals", | |
| featureId: 28 | |
| }, | |
| { | |
| title: "My Leaves", | |
| icon: <Calendar className="w-5 h-5" />, | |
| path: "/leaves", | |
| featureId: 22 | |
| }, | |
| { | |
| title: "Leave Balances", | |
| icon: <CalendarRange className="w-5 h-5" />, | |
| path: "/leave-balances", | |
| featureId: 42 | |
| }, | |
| { | |
| title: "Assets", | |
| icon: <Package className="w-5 h-5" />, | |
| path: "/assets", | |
| featureId: 45 | |
| }, | |
| { | |
| title: "System Details", | |
| icon: <Package className="w-5 h-5" />, | |
| path: "/system-details", | |
| featureId: 46 | |
| }, | |
| { | |
| title: "Asset Assignments", | |
| icon: <Package className="w-5 h-5" />, | |
| path: "/asset-assignments", | |
| featureId: 47 | |
| }, | |
| { | |
| title: "My Asset Requests", | |
| icon: <ClipboardCheck className="w-5 h-5" />, | |
| path: "/asset-requests", | |
| featureId: 48 | |
| }, | |
| { | |
| title: "Gantt Chart", | |
| icon: <BarChart3 className="w-5 h-5" />, | |
| path: "/gantt-chart", | |
| featureId: 50 | |
| }, | |
| { | |
| title: "Resource Allocation", | |
| icon: <Users className="w-5 h-5" />, | |
| path: "/resource-allocation", | |
| featureId: 51 | |
| }, | |
| { | |
| title: "Employee Projects", | |
| icon: <Briefcase className="w-5 h-5" />, | |
| path: "/employee-projects", | |
| featureId: 43 | |
| }, | |
| ]; | |
| // Add Asset Request Management for admin users | |
| if (userData?.roleId === 1 || userData?.roleId === 21 || userData?.roleName?.toLowerCase() === 'admin') { | |
| fallbackItems.push({ | |
| title: "Asset Request Management", | |
| icon: <ShieldCheck className="w-5 h-5" />, | |
| path: "/admin/asset-requests", | |
| featureId: 49 | |
| }); | |
| } | |
| // Add HR-specific appraisal features for HR roles | |
| if (userData?.roleId === 10 || userData?.roleId === 11) { // Assuming roleId 10 and 11 for HR and HR Manager | |
| // Add essential appraisal management features for HR | |
| const hrAppraisalFeatures = [ | |
| { | |
| title: "Appraisal Cycles", | |
| icon: <Gauge className="w-5 h-5" />, | |
| path: "/appraisals/cycles", | |
| featureId: 29 | |
| }, | |
| { | |
| title: "Assessment Areas", | |
| icon: <ListChecks className="w-5 h-5" />, | |
| path: "/appraisals/assessment-areas", | |
| featureId: 30 | |
| }, | |
| { | |
| title: "Competencies", | |
| icon: <BadgeCheck className="w-5 h-5" />, | |
| path: "/appraisals/competencies", | |
| featureId: 31 | |
| }, | |
| { | |
| title: "Role Questions", | |
| icon: <ClipboardCheck className="w-5 h-5" />, | |
| path: "/appraisals/role-questions", | |
| featureId: 36 | |
| }, | |
| { | |
| title: "HR Remarks", | |
| icon: <MessageSquare className="w-5 h-5" />, | |
| path: "/appraisals/hr-remarks", | |
| featureId: 37 | |
| }, | |
| { | |
| title: "Management Remarks", | |
| icon: <MessageSquare className="w-5 h-5" />, | |
| path: "/appraisals/management-remarks", | |
| featureId: 38 | |
| }, | |
| { | |
| title: "Manager Appraisals", | |
| icon: <BarChart3 className="w-5 h-5" />, | |
| path: "/appraisals/manager-dashboard", | |
| featureId: 32 | |
| } | |
| ]; | |
| fallbackItems.push(...hrAppraisalFeatures); | |
| } | |
| setNavItems(fallbackItems); | |
| // Group the fallback items by category | |
| const grouped = groupNavItems(fallbackItems); | |
| setGroupedNavItems(grouped); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| // Function to group nav items by category | |
| const groupNavItems = (items: NavItem[]): Record<string, NavGroup> => { | |
| const grouped: Record<string, NavGroup> = {}; | |
| // Initialize groups from featureCategories | |
| Object.entries(featureCategories).forEach(([category, details]) => { | |
| grouped[category] = { | |
| title: details.title, | |
| icon: details.icon, | |
| items: [] | |
| }; | |
| }); | |
| // Add a generic "Other" category for any items that don't fit into predefined categories | |
| grouped["other"] = { | |
| title: "Other", | |
| icon: <Layers className="w-5 h-5" />, | |
| items: [] | |
| }; | |
| // Categorize each nav item | |
| items.forEach(item => { | |
| const category = getFeatureCategory(item.featureId); | |
| if (grouped[category]) { | |
| grouped[category].items.push(item); | |
| } else { | |
| grouped["other"].items.push(item); | |
| } | |
| }); | |
| // Remove empty categories | |
| Object.keys(grouped).forEach(category => { | |
| if (grouped[category].items.length === 0) { | |
| delete grouped[category]; | |
| } | |
| }); | |
| return grouped; | |
| }; | |
| // Function to check if a nav item is active | |
| const isNavItemActive = (path: string): boolean => { | |
| const currentPath = window.location.pathname; | |
| // Check for special menu highlight override | |
| const activeMenu = document.body.getAttribute('data-active-menu'); | |
| if (activeMenu === 'production-bugs' && path === "/production-bugs") { | |
| return true; | |
| } | |
| // If we're on production-bugs page and have the dashboard flag set, | |
| // the dashboard should be active and production-bugs should not be | |
| if (currentPath.includes('production-bugs')) { | |
| if (document.body.getAttribute('data-active-tab') === 'dashboard') { | |
| // Only highlight Dashboard when the special attribute is set | |
| return path === "/"; | |
| } else { | |
| // Otherwise highlight Production Bugs | |
| return path === "/production-bugs"; | |
| } | |
| } | |
| // For dashboard path specifically - only active when actually on the dashboard | |
| if (path === "/" && currentPath === "/") { | |
| return true; | |
| } | |
| // Check for exact match | |
| if (path === currentPath) return true; | |
| // Special cases for various paths - only highlight if we're actually on these paths | |
| if (path === "/employees" && currentPath.includes("employee") && !currentPath.includes("production-bugs")) return true; | |
| if (path === "/deliverables" && currentPath.includes("deliverable") && !currentPath.includes("production-bugs")) return true; | |
| if (path === "/bugs" && currentPath.includes("bugs") && !currentPath.includes("production-bugs")) return true; | |
| if (path === "/appraisals/my-appraisals" && | |
| (currentPath === "/appraisals/my-appraisals" || currentPath.includes("/appraisals/view")) && | |
| !currentPath.includes("production-bugs")) return true; | |
| if (path === "/appraisals/cycles" && currentPath.includes("/appraisals/cycles") && !currentPath.includes("production-bugs")) return true; | |
| if (path === "/appraisals/assessment-areas" && currentPath.includes("/appraisals/assessment-areas") && !currentPath.includes("production-bugs")) return true; | |
| if (path === "/appraisals/competencies" && currentPath.includes("/appraisals/competencies") && !currentPath.includes("production-bugs")) return true; | |
| if (path === "/appraisals/manager-dashboard" && currentPath.includes("/appraisals/manager-dashboard") && !currentPath.includes("production-bugs")) return true; | |
| if (path === "/appraisals/role-questions" && currentPath.includes("/appraisals/role-questions") && !currentPath.includes("production-bugs")) return true; | |
| if (path === "/appraisals/hr-remarks" && currentPath.includes("/appraisals/hr-remarks") && !currentPath.includes("production-bugs")) return true; | |
| if (path === "/appraisals/management-remarks" && currentPath.includes("/appraisals/management-remarks") && !currentPath.includes("production-bugs")) return true; | |
| if (path === "/meeting-room/rooms" && currentPath.includes("/meeting-room/rooms") && !currentPath.includes("production-bugs")) return true; | |
| if (path === "/meeting-room/bookings" && currentPath.includes("/meeting-room/bookings") && !currentPath.includes("production-bugs")) return true; | |
| if (path === "/communications" && currentPath.includes("/communications") && !currentPath.includes("production-bugs")) return true; | |
| return false; | |
| }; | |
| return ( | |
| <aside | |
| className={cn( | |
| "fixed inset-y-0 left-0 z-40 w-64 bg-sidebar text-sidebar-foreground border-r border-sidebar-border transition-transform duration-300 ease-in-out transform", | |
| isOpen ? "translate-x-0" : "-translate-x-full" | |
| )} | |
| > | |
| <div className="flex flex-col h-full"> | |
| <div className="h-16 flex items-center px-4 border-b border-sidebar-border"> | |
| <Logo className="text-sidebar-foreground" /> | |
| </div> | |
| {isAuthenticated && !isDownloadPage && ( | |
| <div className="px-4 py-3 border-b border-sidebar-border/50"> | |
| <div className="relative"> | |
| <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-sidebar-foreground/50" /> | |
| <Input | |
| type="text" | |
| placeholder="Search menu..." | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="pl-9 h-9 bg-sidebar-accent/30 border-sidebar-border/50 text-sidebar-foreground placeholder:text-sidebar-foreground/40 focus-visible:ring-sidebar-primary" | |
| /> | |
| {searchQuery && ( | |
| <button | |
| onClick={() => setSearchQuery("")} | |
| className="absolute right-2.5 top-2.5 text-sidebar-foreground/50 hover:text-sidebar-foreground" | |
| > | |
| <X className="h-4 w-4" /> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| <ScrollArea className="flex-1 px-3 py-4"> | |
| {isLoading && isAuthenticated && !isDownloadPage ? ( | |
| <div className="flex flex-col items-center justify-center py-4 space-y-2"> | |
| <Loader2 className="h-5 w-5 animate-spin text-sidebar-foreground" /> | |
| <span className="text-xs text-sidebar-foreground/70">Loading menu items...</span> | |
| </div> | |
| ) : ( | |
| <nav className="flex flex-col gap-1"> | |
| {!isAuthenticated ? ( | |
| // Unauthenticated user view | |
| <div className="mt-6 pt-4"> | |
| <NavLink | |
| to="/download-app" | |
| className={({ isActive }) => | |
| cn( | |
| "flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-all", | |
| isActive || location.pathname.includes("/download-app") | |
| ? "bg-sidebar-primary text-sidebar-primary-foreground" | |
| : "text-primary hover:bg-sidebar-accent/50 hover:text-primary-foreground" | |
| ) | |
| } | |
| > | |
| <Download className="w-5 h-5" /> | |
| <span>Download Mobile App</span> | |
| </NavLink> | |
| <div className="mt-6"> | |
| <NavLink | |
| to="/login" | |
| className="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-all bg-primary text-primary-foreground hover:bg-primary/90" | |
| > | |
| <span>Log In</span> | |
| </NavLink> | |
| </div> | |
| </div> | |
| ) : ( | |
| // Authenticated user view - always show full menu regardless of page | |
| <> | |
| {/* Dashboard (often kept separate at the top) */} | |
| {filteredGroupedNavItems["dashboard"] && ( | |
| <div className="mb-2"> | |
| {filteredGroupedNavItems["dashboard"].items.map((item) => ( | |
| <NavLink | |
| key={item.path} | |
| to={item.path} | |
| onClick={() => { | |
| // When going to dashboard, clear any attributes that might affect highlighting | |
| document.body.removeAttribute('data-active-tab'); | |
| document.body.removeAttribute('data-active-menu'); | |
| sessionStorage.removeItem('from-dashboard'); | |
| sessionStorage.removeItem('from-sidebar'); | |
| sessionStorage.removeItem('manual-view-selection'); | |
| // Close the Bug Tracking menu group | |
| setExpandedGroups(prev => ({ | |
| ...prev, | |
| bugs: false // "bugs" is the category key for Bug Tracking | |
| })); | |
| }} | |
| className={({ isActive }) => | |
| cn( | |
| "flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-all", | |
| isActive || isNavItemActive(item.path) | |
| ? "bg-sidebar-primary text-sidebar-primary-foreground" | |
| : "text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground" | |
| ) | |
| } | |
| > | |
| {item.icon} | |
| <span>{item.title}</span> | |
| </NavLink> | |
| ))} | |
| </div> | |
| )} | |
| {/* My Profile Link */} | |
| {(!searchQuery || "my profile".includes(searchQuery.toLowerCase())) && ( | |
| <div className="mb-4"> | |
| <NavLink | |
| to={`/my-profile`} | |
| className={({ isActive }) => | |
| cn( | |
| "flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-all", | |
| isActive || window.location.pathname.includes('/my-profile') || window.location.pathname.includes('/profile') | |
| ? "bg-sidebar-primary text-sidebar-primary-foreground" | |
| : "text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground" | |
| ) | |
| } | |
| > | |
| <User className="w-5 h-5" /> | |
| <span>My Profile</span> | |
| </NavLink> | |
| </div> | |
| )} | |
| {/* Grouped menu items */} | |
| {Object.entries(filteredGroupedNavItems) | |
| .filter(([category]) => category !== "dashboard") // Skip dashboard as it's already shown above | |
| .map(([category, group]) => ( | |
| <div key={category} className="mb-1"> | |
| {/* Group Header */} | |
| <button | |
| onClick={() => toggleGroup(category)} | |
| className="w-full flex items-center justify-between px-3 py-2 text-sm font-medium text-sidebar-foreground/80 hover:text-sidebar-foreground transition-colors" | |
| > | |
| <div className="flex items-center gap-3"> | |
| {group.icon} | |
| <span>{group.title}</span> | |
| </div> | |
| {expandedGroups[category] ? | |
| <ChevronDown className="h-4 w-4" /> : | |
| <ChevronRight className="h-4 w-4" /> | |
| } | |
| </button> | |
| {/* Group Items */} | |
| {expandedGroups[category] && ( | |
| <div className="ml-4 mt-1 space-y-1 border-l-2 border-sidebar-border/50 pl-3"> | |
| {group.items.map((item) => ( | |
| <NavLink | |
| key={item.path} | |
| to={item.path} | |
| state={item.path === "/production-bugs" ? { source: "sidebar" } : undefined} | |
| onClick={() => { | |
| // Close sidebar on mobile | |
| if (isMobile) { | |
| setIsOpen(false); | |
| } | |
| // For any non-dashboard item, always clear the dashboard highlighting | |
| document.body.removeAttribute('data-active-tab'); | |
| if (item.path === "/production-bugs") { | |
| // Clear dashboard marker and set sidebar marker | |
| sessionStorage.removeItem('from-dashboard'); | |
| sessionStorage.setItem('from-sidebar', 'true'); | |
| // Ensure only Production Bugs is highlighted | |
| document.body.setAttribute('data-active-menu', 'production-bugs'); | |
| } else { | |
| // For other menu items, clear any special attributes | |
| sessionStorage.removeItem('from-dashboard'); | |
| sessionStorage.removeItem('from-sidebar'); | |
| document.body.removeAttribute('data-active-menu'); | |
| } | |
| }} | |
| className={({ isActive }) => | |
| cn( | |
| "flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-all", | |
| isActive || isNavItemActive(item.path) | |
| ? "bg-sidebar-primary text-sidebar-primary-foreground" | |
| : "text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground" | |
| ) | |
| } | |
| > | |
| {item.icon} | |
| <span>{item.title}</span> | |
| </NavLink> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| )) | |
| } | |
| {/* Empty state for search results */} | |
| {searchQuery && Object.keys(filteredGroupedNavItems).length === 0 && !("my profile".includes(searchQuery.toLowerCase())) && ( | |
| <div className="px-3 py-8 text-center"> | |
| <p className="text-sm text-sidebar-foreground/50">No results found for "{searchQuery}"</p> | |
| </div> | |
| )} | |
| {/* Admin Section - Only visible to admins */} | |
| {userData?.roleId === 21 && (!searchQuery || "features".includes(searchQuery.toLowerCase())) && ( | |
| <> | |
| <div className="mt-4 mb-2"> | |
| <div className="text-xs font-semibold text-sidebar-foreground/50 uppercase px-3"> | |
| Admin Tools | |
| </div> | |
| </div> | |
| <NavLink | |
| to="/features" | |
| className={({ isActive }) => | |
| cn( | |
| "flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-all", | |
| isActive || window.location.pathname.includes("/features") | |
| ? "bg-sidebar-primary text-sidebar-primary-foreground" | |
| : "text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground" | |
| ) | |
| } | |
| > | |
| <Key className="w-5 h-5" /> | |
| <span>Features</span> | |
| </NavLink> | |
| </> | |
| )} | |
| {/* Download App Section - Visible to all users */} | |
| {(!searchQuery || "download mobile app".includes(searchQuery.toLowerCase())) && ( | |
| <div className="mt-6 pt-4 border-t border-sidebar-border/30"> | |
| <NavLink | |
| to="/download-app" | |
| className={({ isActive }) => | |
| cn( | |
| "flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-all", | |
| isActive || window.location.pathname.includes("/download-app") | |
| ? "bg-sidebar-primary text-sidebar-primary-foreground" | |
| : "text-primary hover:bg-sidebar-accent/50 hover:text-primary-foreground" | |
| ) | |
| } | |
| > | |
| <Download className="w-5 h-5" /> | |
| <span>Download Mobile App</span> | |
| </NavLink> | |
| </div> | |
| )} | |
| </> | |
| )} | |
| </nav> | |
| )} | |
| </ScrollArea> | |
| <Separator className="bg-sidebar-border/50" /> | |
| <div className="p-4 text-xs text-sidebar-foreground/70"> | |
| <div className="font-medium">Synthesys PM Tool</div> | |
| <div className="mt-1">v{getVersionString()}</div> | |
| </div> | |
| </div> | |
| </aside> | |
| ); | |
| }; | |
| export default Sidebar; | |