import React, { useState } from 'react'; import { NavLink, Outlet, useNavigate, Link } from 'react-router-dom'; import { LayoutDashboard, Building2, FolderKanban, Coins, ShieldAlert, Settings, LogOut, ChevronRight, ChevronDown, ShieldCheck, Database, Home, User, Users, GitPullRequest, MapPin, PanelLeftClose, PanelLeftOpen, ChevronsLeft, ChevronsRight, } from 'lucide-react'; import { useAuth } from '../../auth/AuthProvider'; import { BrandLogo } from '../ui/BrandLogo'; import { HelpTooltip } from '../ui/HelpTooltip'; import { ErrorBoundary } from '../ui/ErrorBoundary'; import { useQuery } from '@tanstack/react-query'; import { getUsers, getTenant } from '../../services/api'; import { getUserEmail, registerUserEmail } from '../../lib/formatters'; import { useRbac, RoleClaim } from '../../auth/rbac'; interface SubNavItem { name: string; path: string; icon: React.ElementType; help?: string; claim?: RoleClaim; } interface NavItem { name: string; path: string; icon: React.ElementType; help: string; claim?: RoleClaim; subItems?: SubNavItem[]; } const navItems: NavItem[] = [ { name: 'Executive Dashboard', path: '/dashboard', icon: LayoutDashboard, help: 'Strategic macro metrics, portfolio value, disbursed funding, ISO 31000 risk heatmap, and projects requiring immediate intervention.', claim: 'dashboard', }, { name: 'Governance & Structure', path: '/governance', icon: Building2, help: 'Master records for legal entities, holding structures, investment portfolios, and thematic programmes funded under statutory mandates.', claim: 'governance', }, { name: 'Projects & Milestones', path: '/projects', icon: FolderKanban, help: 'Project Work Breakdown Structure (WBS), contractual deliverables, scheduled milestones, evidence verification, and execution tracking.', claim: 'projects_milestones', subItems: [ { name: 'Beneficiaries', path: '/projects/beneficiaries', icon: Users, help: 'Manage target beneficiaries, demographics, and statutory compliance items satisfying beneficiaries.', claim: 'beneficiaries', }, { name: 'Change Management', path: '/projects/change-management', icon: GitPullRequest, help: 'Track project issues, contractual change requests, and resolve changes in-place.', claim: 'change_management', }, { name: 'Site Visits', path: '/projects/site-visits', icon: MapPin, help: 'Log physical site visits, field observations, inspector findings, and project feedback.', claim: 'site_visits', }, { name: 'Compliance', path: '/compliance', icon: ShieldCheck, help: 'Portfolio & programme compliance dashboard, statutory requirements, beneficiary outcome fulfillment, and risk monitoring.', claim: 'compliance', }, ], }, { name: 'Grant & Financials', path: '/grants', icon: Coins, help: 'Financial management including statutory awards, multi-year budget line allocations, tranches, and scheduled payment disbursements.', claim: 'grant_financials', }, { name: 'M&E, Evidence & Risks', path: '/monitoring', icon: ShieldAlert, help: 'Monitoring & Evaluation (M&E) framework, indicator tracking, ISO 31000 risk registers, mitigation plans, and compliance audits.', claim: 'evidence_risks', }, { name: 'Tenant Management', path: '/tenants', icon: Database, help: 'Multi-tenant partition directory, organizational onboarding, status lifecycle controls, and system-wide isolation boundaries.', claim: 'tenant_management', }, { name: 'Settings & Communications', path: '/settings', icon: Settings, help: 'User account provisioning, statutory sync, tenant environment parameters, and communication invite templates.', }, ]; export const AppLayout: React.FC = () => { const { user, claims, sub, tenantId, hashedTenantId, tenantName: authTenantName, canManageTenants, logout, roles } = useAuth(); const userRole = user?.role || (Array.isArray(roles) && roles.length > 0 ? roles[0] : (claims?.role as string) || 'User'); const { canView } = useRbac(); const [sidebarOpen, setSidebarOpen] = useState(true); const [userMenuOpen, setUserMenuOpen] = useState(false); const navigate = useNavigate(); const handleLogout = async () => { await logout(); }; // Filter navigation shelf strictly according to RBAC Role Claims const visibleNavItems = navItems .filter((item) => { if (item.path === '/tenants') { return canManageTenants || canView('tenant_management') || canView('tenant_self_service'); } if (item.path === '/settings') { return ( canView('settings_users') || canView('settings_invites') || canView('settings_notifications') || canView('settings_auditlog') || canView('tenant_self_service') ); } if (item.claim) { return canView(item.claim); } return true; }) .map((item) => { if (!item.subItems) return item; return { ...item, subItems: item.subItems.filter((subItem) => { if (subItem.claim) { return canView(subItem.claim); } return true; }), }; }); // Pre-populate user directory cache so all components and modals resolve user emails instantly useQuery({ queryKey: ['users-directory'], queryFn: () => getUsers(1, 200), staleTime: 5 * 60 * 1000, }); // Query tenant endpoint to resolve tenant name if not provided in claims or profile const { data: remoteTenant } = useQuery({ queryKey: ['active-tenant-header', tenantId], queryFn: () => getTenant(tenantId!), enabled: !!tenantId && !claims?.tenant && !user?.tenant && !authTenantName, staleTime: 5 * 60 * 1000, }); // Ensure current logged-in user is registered in the email cache const effectiveEmail = ((user?.email || claims?.email || claims?.['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] || claims?.upn) as string); if (sub && effectiveEmail) { registerUserEmail(sub, effectiveEmail); } if (user?.id && effectiveEmail) { registerUserEmail(user.id, effectiveEmail); } // Extract name and email directly from validated token claims const tokenUserName = user?.name || (claims.name as string) || (claims.preferred_username as string) || (claims.given_name ? `${claims.given_name} ${claims.family_name || ''}`.trim() : '') || 'Authorized User'; const tokenUserEmail = user?.email || (claims.email as string) || (claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] as string) || (claims.upn as string) || (sub ? getUserEmail(sub) : ''); // Extract tenant name from user claim 'tenant', auth context, remote tenant endpoint, or organization name const tenantName = (claims?.tenant as string) || (claims?.['tenant'] as string) || authTenantName || user?.tenant || remoteTenant?.name || user?.organisationName || (claims?.organisation as string) || (tenantId ? `Tenant #${tenantId}` : 'Department of Science, Innovation & Technology (DSI)'); return (
{/* Sidebar */} {/* Main Content Area */}
{/* Top Header */}
{/* Home Navigation */} Home
Tenant: {canManageTenants || canView('tenant_management') || canView('tenant_self_service') ? ( {tenantName} {hashedTenantId && ( {hashedTenantId} )} ) : ( {tenantName} {hashedTenantId && ( {hashedTenantId} )} )}
Authenticated Session
{/* User Profile and Controls */}

{tokenUserName}

{userRole}
{/* Dropdown Menu */} {userMenuOpen && (
setUserMenuOpen(false)} >

{tokenUserName}

{userRole}

{tokenUserEmail || (sub ? getUserEmail(sub) : '')}

Role: {userRole}

setUserMenuOpen(false)} className="flex items-center gap-2 px-3 py-2 text-xs text-slate-300 hover:text-white hover:bg-slate-800/60 rounded-xl transition" > Go to Home Page
)}
{/* Main Content Area */}
{/* Ambient Glows & Dot Grid - Isolated inside overflow-hidden to prevent phantom page scrollbars */}
{/* Bottom Status Footer */}
); };