+ Users assigned the Partner role by security are not permitted to access the PFM Backoffice Console.
+
+
+
+
+ {/* User & Identity Summary Card */}
+
+
+ Account Name
+ {displayName}
+
+
+ {displayEmail && (
+
+
+
+ Email
+
+ {displayEmail}
+
+ )}
+
+
+
+
+ Assigned Role
+
+
+ {role}
+
+
+
+ {tenant && (
+
+
+
+ Tenant Context
+
+ {tenant}
+
+ )}
+
+ {sub && (
+
+
+
+ Subject Key
+
+ {sub}
+
+ )}
+
+
+ {/* Security Policy Information */}
+
+
+
+ Administrative Boundary Notice
+
+
+ The PFM Console is reserved for statutory administrators, sponsors, programme managers, and governance officers. Partner credentials cannot access this environment. If you require backoffice administrative access, please request an updated role assignment from your system administrator.
+
+
+
+ {/* Actions */}
+
+
+
+
+
+ {/* Footer Brand watermark */}
+
+
+
+ Post Fund Management (PFM)
+
+ National Security Gateway
+
+
+
+ );
+};
diff --git a/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx b/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx
index 54fd012..d3458a3 100644
--- a/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx
+++ b/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx
@@ -25,7 +25,7 @@ import { BrandLogo } from '../ui/BrandLogo';
import { HelpTooltip } from '../ui/HelpTooltip';
import { ErrorBoundary } from '../ui/ErrorBoundary';
import { useQuery } from '@tanstack/react-query';
-import { getUsers } from '../../services/api';
+import { getUsers, getTenant } from '../../services/api';
import { getUserEmail, registerUserEmail } from '../../lib/formatters';
interface SubNavItem {
@@ -94,6 +94,12 @@ const navItems: NavItem[] = [
icon: ShieldAlert,
help: 'Monitoring & Evaluation (M&E) framework, indicator tracking, ISO 31000 risk registers, mitigation plans, and compliance audits.',
},
+ {
+ name: 'Tenant Management',
+ path: '/tenants',
+ icon: Database,
+ help: 'Multi-tenant partition directory, organizational onboarding, status lifecycle controls, and system-wide isolation boundaries.',
+ },
{
name: 'Settings & Communications',
path: '/settings',
@@ -103,7 +109,7 @@ const navItems: NavItem[] = [
];
export const AppLayout: React.FC = () => {
- const { user, claims, sub, tenantId, hashedTenantId, logout } = useAuth();
+ const { user, claims, sub, tenantId, hashedTenantId, tenantName: authTenantName, canManageTenants, logout } = useAuth();
const [sidebarOpen, setSidebarOpen] = useState(true);
const [userMenuOpen, setUserMenuOpen] = useState(false);
const navigate = useNavigate();
@@ -112,6 +118,14 @@ export const AppLayout: React.FC = () => {
await logout();
};
+ // Only show Tenant Management if user's tenant_id == 1 and their role list has Admin in it
+ const visibleNavItems = navItems.filter((item) => {
+ if (item.path === '/tenants') {
+ return canManageTenants;
+ }
+ return true;
+ });
+
// Pre-populate user directory cache so all components and modals resolve user emails instantly
useQuery({
queryKey: ['users-directory'],
@@ -119,6 +133,14 @@ export const AppLayout: React.FC = () => {
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) {
@@ -143,14 +165,16 @@ export const AppLayout: React.FC = () => {
(claims.upn as string) ||
(sub ? getUserEmail(sub) : '');
- // Extract tenant name directly from the user claim named 'tenant'
+ // Extract tenant name from user claim 'tenant', auth context, remote tenant endpoint, or organization name
const tenantName =
(claims?.tenant as string) ||
- user?.tenant ||
(claims?.['tenant'] as string) ||
+ authTenantName ||
+ user?.tenant ||
+ remoteTenant?.name ||
user?.organisationName ||
(claims?.organisation as string) ||
- 'Department of Science, Innovation & Technology (DSI)';
+ (tenantId ? `Tenant #${tenantId}` : 'Department of Science, Innovation & Technology (DSI)');
return (