import React, { createContext, useContext, useState, useEffect } from 'react'; import { allPageData, SidebarItem } from './data'; import { useLanguage } from '../../providers/language/LanguageContext'; import { resolveLanguageText, GenericPageData } from './pageInterface'; import { usePermissions } from '../../hooks/usePermissions'; import { FaHome, FaHatWizard, FaBriefcase, FaBuilding } from 'react-icons/fa'; import { RiFolderSettingsFill } from 'react-icons/ri'; import { SidebarSubmenuItemData } from '../../components/Sidebar/sidebarTypes'; // Configuration for parent groups that don't have a page definition // Maps parentPath (can be nested like "start.real-estate") to icon and default order const parentGroupConfig: Record>; defaultOrder?: number; }> = { 'start': { icon: FaHome, defaultOrder: 1 }, 'start.real-estate': { icon: FaBuilding, defaultOrder: 1 }, 'start.trustee': { icon: FaBriefcase, defaultOrder: 2 }, 'trustee': { icon: FaBriefcase, defaultOrder: 2 }, 'administration': { icon: RiFolderSettingsFill, defaultOrder: 3 }, 'admin': { icon: FaHatWizard, defaultOrder: 4 } }; interface SidebarContextType { sidebarItems: SidebarItem[]; loading: boolean; error: string | null; refreshSidebar: () => Promise; } const SidebarContext = createContext(undefined); export const useSidebar = () => { const context = useContext(SidebarContext); if (!context) { throw new Error('useSidebar must be used within a SidebarProvider'); } return context; }; interface SidebarProviderProps { children: React.ReactNode; } export const SidebarProvider: React.FC = ({ children }) => { const [sidebarItems, setSidebarItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Get translation function from language context const { t } = useLanguage(); const { canView, preloadUiPermissions } = usePermissions(); // Helper type for navigation tree nodes interface NavigationNode { id: string; pathSegment: string; fullPath: string; // Full dot-notation path (e.g., "start.real-estate") name: string; icon?: React.ComponentType>; order: number; page?: GenericPageData; // If this node represents an actual page children: Map; // Keyed by path segment pages: GenericPageData[]; // Direct child pages } // Helper function to resolve node name const resolveNodeName = (pathSegment: string, fullPath: string, page?: GenericPageData): string => { if (page) { return resolveLanguageText(page.name, t); } // Try translation key (e.g., "start.real-estate.title") const translationKey = `${fullPath}.title`; const translated = t(translationKey); if (translated !== translationKey) { return translated; } // Try just the segment (e.g., "real-estate.title") const segmentKey = `${pathSegment}.title`; const segmentTranslated = t(segmentKey); if (segmentTranslated !== segmentKey) { return segmentTranslated; } // Fallback to capitalized segment return pathSegment.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' '); }; // Helper function to resolve node icon const resolveNodeIcon = (pathSegment: string, fullPath: string, page?: GenericPageData): React.ComponentType> | undefined => { if (page?.icon) { return page.icon; } // Check parentGroupConfig for nested paths first (e.g., "start.real-estate") if (parentGroupConfig[fullPath]?.icon) { return parentGroupConfig[fullPath].icon; } // Check parentGroupConfig for top-level segments (e.g., "start") if (fullPath === pathSegment && parentGroupConfig[pathSegment]?.icon) { return parentGroupConfig[pathSegment].icon; } return undefined; }; // Helper function to resolve node order const resolveNodeOrder = (pathSegment: string, fullPath: string, page?: GenericPageData, childPages: GenericPageData[] = []): number => { if (page?.order !== undefined) { return page.order; } // Check parentGroupConfig for top-level segments if (fullPath === pathSegment && parentGroupConfig[pathSegment]?.defaultOrder !== undefined) { return parentGroupConfig[pathSegment].defaultOrder!; } // Use minimum order of child pages if (childPages.length > 0) { const childOrders = childPages.map(p => p.order ?? 0); return Math.min(...childOrders); } return 0; }; // Build navigation tree from page data const buildNavigationTree = (): Map => { const rootNodes = new Map(); // Process all pages with parent paths const pagesWithParents = allPageData.filter( page => page.parentPath && !page.hide && page.showInSidebar !== false ); for (const page of pagesWithParents) { if (!page.parentPath) continue; // Parse parent path segments (e.g., "start.real-estate" -> ["start", "real-estate"]) const pathSegments = page.parentPath.split('.'); // Build path to root, creating nodes as needed let currentMap = rootNodes; let currentFullPath = ''; for (let i = 0; i < pathSegments.length; i++) { const segment = pathSegments[i]; currentFullPath = currentFullPath ? `${currentFullPath}.${segment}` : segment; // Get or create node for this segment if (!currentMap.has(segment)) { // Check if there's a page for this path segment const segmentPage = allPageData.find( p => p.path === currentFullPath && !p.hide ); const node: NavigationNode = { id: segmentPage?.id || currentFullPath, pathSegment: segment, fullPath: currentFullPath, name: '', // Will be resolved later icon: undefined, // Will be resolved later order: 0, // Will be resolved later page: segmentPage, children: new Map(), pages: [] }; currentMap.set(segment, node); } const node = currentMap.get(segment)!; // If this is the last segment, add the page as a child page if (i === pathSegments.length - 1) { node.pages.push(page); } // Move to next level currentMap = node.children; } } // Resolve names, icons, and orders for all nodes const resolveNode = (node: NavigationNode): void => { // Resolve children first (bottom-up) for (const childNode of node.children.values()) { resolveNode(childNode); } // Resolve this node node.name = resolveNodeName(node.pathSegment, node.fullPath, node.page); node.icon = resolveNodeIcon(node.pathSegment, node.fullPath, node.page); // Collect all child pages (from direct pages and nested children) const allChildPages = [...node.pages]; for (const childNode of node.children.values()) { if (childNode.page) { allChildPages.push(childNode.page); } allChildPages.push(...childNode.pages); } node.order = resolveNodeOrder(node.pathSegment, node.fullPath, node.page, allChildPages); }; // Resolve all root nodes for (const node of rootNodes.values()) { resolveNode(node); } return rootNodes; }; // Convert navigation tree node to sidebar submenu item (recursive) const nodeToSubmenuItem = async (node: NavigationNode, depth: number = 0): Promise => { // Filter child pages by RBAC and privilegeChecker const accessiblePages: GenericPageData[] = []; for (const page of node.pages) { try { const hasRBACAccess = await canView('UI', page.path); if (!hasRBACAccess) continue; if (page.privilegeChecker) { try { const hasPrivilege = await page.privilegeChecker(); if (!hasPrivilege) continue; } catch (error) { console.error(`Error checking privilegeChecker for page ${page.path}:`, error); continue; } } accessiblePages.push(page); } catch (error) { console.error(`Error checking RBAC access for page ${page.path}:`, error); } } // Process child nodes recursively (increment depth) const accessibleChildren: SidebarSubmenuItemData[] = []; for (const childNode of node.children.values()) { const childItem = await nodeToSubmenuItem(childNode, depth + 1); if (childItem) { accessibleChildren.push(childItem); } } // Combine pages and child nodes, assigning depth const allChildren: SidebarSubmenuItemData[] = [ ...accessiblePages.map(page => ({ id: page.id, name: resolveLanguageText(page.name, t), link: `/${page.path}`, icon: page.icon, depth: depth + 1 // Child pages are one level deeper })), ...accessibleChildren ]; // If no accessible children, don't create this node if (allChildren.length === 0) { return null; } // If this node has a page itself, it shouldn't be a navigation node // But according to requirements: if it has subpages, it is NOT a page itself // So we create a navigation node without a link return { id: node.id, name: node.name, link: undefined, // Navigation node - not a clickable page icon: node.icon, submenu: allChildren.length > 0 ? allChildren : undefined, depth: depth // Current depth level }; }; // Convert navigation tree to sidebar items const treeToSidebarItems = async (tree: Map): Promise => { const items: SidebarItem[] = []; // Process each root node (depth 0 for top-level items) for (const node of tree.values()) { const submenuItem = await nodeToSubmenuItem(node, 0); if (submenuItem && submenuItem.submenu && submenuItem.submenu.length > 0) { items.push({ id: node.id, name: node.name, link: undefined, // Navigation node - not a clickable page icon: node.icon, moduleEnabled: true, order: node.order, submenu: submenuItem.submenu, depth: 0 // Top-level items have depth 0 }); } } return items; }; // Get sidebar items from page data const getSidebarItems = async (): Promise => { const items: SidebarItem[] = []; // Build navigation tree const navigationTree = buildNavigationTree(); // Convert tree to sidebar items const treeItems = await treeToSidebarItems(navigationTree); items.push(...treeItems); // Get main pages (no parent path) const mainPages = allPageData .filter(page => !page.parentPath && !page.hide && page.showInSidebar !== false) .sort((a, b) => (a.order || 0) - (b.order || 0)); // Process each main page for (const pageData of mainPages) { // Check RBAC permissions try { const hasRBACAccess = await canView('UI', pageData.path); if (!hasRBACAccess) { continue; } // Check client-side privilegeChecker if provided if (pageData.privilegeChecker) { try { const hasPrivilege = await pageData.privilegeChecker(); if (!hasPrivilege) { continue; } } catch (error) { console.error(`Error checking privilegeChecker for ${pageData.path}:`, error); continue; } } } catch (error) { console.error(`Error checking RBAC access for ${pageData.path}:`, error); continue; } // Check if this page has subpages (legacy support) if (pageData.hasSubpages) { // Find all subpages for this parent const allSubpages = allPageData.filter(p => p.parentPath === pageData.path && !p.hide && p.showInSidebar !== false ); // Filter subpages by RBAC access const accessibleSubpages: GenericPageData[] = []; for (const subpage of allSubpages) { try { const hasSubpageRBACAccess = await canView('UI', subpage.path); if (!hasSubpageRBACAccess) { continue; } if (subpage.privilegeChecker) { try { const hasPrivilege = await subpage.privilegeChecker(); if (!hasPrivilege) { continue; } } catch (error) { console.error(`Error checking privilegeChecker for subpage ${subpage.path}:`, error); continue; } } accessibleSubpages.push(subpage); } catch (error) { console.error(`Error checking RBAC access for subpage ${subpage.path}:`, error); } } if (accessibleSubpages.length > 0) { // Create item with submenu (no link since it has subpages) items.push({ id: pageData.id, name: resolveLanguageText(pageData.name, t), link: undefined, // No link - has subpages, so it's a navigation node icon: pageData.icon, moduleEnabled: pageData.moduleEnabled ?? true, order: pageData.order || 0, depth: 0, // Top-level items have depth 0 submenu: accessibleSubpages.map(subpage => ({ id: subpage.id, name: resolveLanguageText(subpage.name, t), link: `/${subpage.path}`, icon: subpage.icon, depth: 1 // First level of submenu })) }); } else { // No accessible subpages, show as regular item items.push({ id: pageData.id, name: resolveLanguageText(pageData.name, t), link: `/${pageData.path}`, icon: pageData.icon, moduleEnabled: pageData.moduleEnabled ?? true, order: pageData.order || 0, depth: 0 // Top-level items have depth 0 }); } } else { // Regular items without subpages items.push({ id: pageData.id, name: resolveLanguageText(pageData.name, t), link: `/${pageData.path}`, icon: pageData.icon, moduleEnabled: pageData.moduleEnabled ?? true, order: pageData.order || 0, depth: 0 // Top-level items have depth 0 }); } } // Sort all items by order const sortedItems = items.sort((a, b) => (a.order || 0) - (b.order || 0)); return sortedItems; }; // Refresh sidebar items const refreshSidebar = async () => { console.log('🔄 SidebarProvider: Refreshing sidebar items...'); setLoading(true); setError(null); try { // Preload all UI permissions in a single API call // This caches all permissions before iterating through pages await preloadUiPermissions(); const items = await getSidebarItems(); console.log('✅ SidebarProvider: Setting sidebar items:', { count: items.length, items: items.map(item => ({ id: item.id, link: item.link, name: item.name })) }); setSidebarItems(items); } catch (err) { console.error('❌ SidebarProvider: Error refreshing sidebar:', err); setError(err instanceof Error ? err.message : 'Failed to load sidebar items'); } finally { setLoading(false); } }; // Load sidebar items on mount and when language changes useEffect(() => { refreshSidebar(); }, [t]); const contextValue: SidebarContextType = { sidebarItems, loading, error, refreshSidebar }; return ( {children} ); }; export default SidebarProvider;