From c24e809b88d7f14caf8ed844d5def552f92c6050 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 22 Oct 2026 06:23:22 +0200 Subject: [PATCH] Applied UI rendering fixes --- .../src/auth/AuthProvider.tsx | 4 +- .../auth/AccountActivationModal.tsx | 11 +- .../auth/PartnerAccessDeniedModal.tsx | 11 +- .../components/auth/SessionExpiredModal.tsx | 11 +- .../src/components/layout/AppLayout.tsx | 25 +++-- .../src/components/ui/ErrorBoundary.tsx | 104 ++++++++++++------ .../src/components/ui/Modal.tsx | 22 +++- .../components/users/UserSecurityModal.tsx | 66 ++++++----- PostFundManagement.ConsoleUI/src/index.css | 21 +++- .../src/pages/SettingsPage.tsx | 97 ++++++++-------- .../src/pages/TenantManagementPage.tsx | 43 ++++---- 11 files changed, 261 insertions(+), 154 deletions(-) diff --git a/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx b/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx index 7b77dda..91ebb38 100644 --- a/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx +++ b/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx @@ -64,7 +64,7 @@ interface AuthContextType { authError: string | null; } -const AuthContext = createContext(undefined); +export const AuthContext = createContext(undefined); export const LOCAL_STORAGE_TOKEN_KEY = 'pfm_access_token'; export const LOCAL_STORAGE_USER_KEY = 'pfm_auth_user'; @@ -415,7 +415,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('code') && urlParams.has('state')) { const mgr = getUserManager(); - const oidcUser: OidcUser = await mgr.signinCallback(); + const oidcUser: OidcUser | undefined = await mgr.signinCallback(); if (oidcUser && oidcUser.access_token) { const token = oidcUser.access_token; diff --git a/PostFundManagement.ConsoleUI/src/components/auth/AccountActivationModal.tsx b/PostFundManagement.ConsoleUI/src/components/auth/AccountActivationModal.tsx index d96813f..f91766b 100644 --- a/PostFundManagement.ConsoleUI/src/components/auth/AccountActivationModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/auth/AccountActivationModal.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { useNavigate } from 'react-router-dom'; import { ShieldAlert, @@ -113,16 +114,16 @@ export const AccountActivationModal: React.FC = ({ navigate('/'); }; - return ( + const modalContent = (
{/* Non-dismissable Card Container: No X button, no escape dismiss, no backdrop click */}
e.stopPropagation()} > {/* Amber accent top bar */} @@ -278,4 +279,8 @@ export const AccountActivationModal: React.FC = ({
); + + return typeof document !== 'undefined' + ? createPortal(modalContent, document.body) + : modalContent; }; diff --git a/PostFundManagement.ConsoleUI/src/components/auth/PartnerAccessDeniedModal.tsx b/PostFundManagement.ConsoleUI/src/components/auth/PartnerAccessDeniedModal.tsx index c7c04d8..ce7eb0a 100644 --- a/PostFundManagement.ConsoleUI/src/components/auth/PartnerAccessDeniedModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/auth/PartnerAccessDeniedModal.tsx @@ -1,4 +1,5 @@ import React, { useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { useNavigate } from 'react-router-dom'; import { ShieldAlert, @@ -54,15 +55,15 @@ export const PartnerAccessDeniedModal: React.FC = navigate('/', { replace: true }); }; - return ( + const modalContent = (
-
+
{/* Glow Header Accent */}
@@ -171,4 +172,8 @@ export const PartnerAccessDeniedModal: React.FC =
); + + return typeof document !== 'undefined' + ? createPortal(modalContent, document.body) + : modalContent; }; diff --git a/PostFundManagement.ConsoleUI/src/components/auth/SessionExpiredModal.tsx b/PostFundManagement.ConsoleUI/src/components/auth/SessionExpiredModal.tsx index 7523ca8..7681a5a 100644 --- a/PostFundManagement.ConsoleUI/src/components/auth/SessionExpiredModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/auth/SessionExpiredModal.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState, useRef } from 'react'; +import { createPortal } from 'react-dom'; import { ShieldAlert, Clock, ArrowRight, X, AlertTriangle, LogOut } from 'lucide-react'; import { clearSessionAndRedirectToLanding, dismissSubordinateModals } from '../../services/sessionExpiration'; @@ -83,10 +84,10 @@ export const SessionExpiredModal: React.FC = ({ const secondsRemaining = Math.max(0, Math.ceil(timeLeftMs / 1000)); const progressPercent = Math.max(0, Math.min(100, (timeLeftMs / totalDurationMs) * 100)); - return ( + const modalContent = (
setIsPaused(true)} onMouseLeave={() => setIsPaused(false)} role="dialog" @@ -95,7 +96,7 @@ export const SessionExpiredModal: React.FC = ({ >
{/* Subtle decorative top accent bar */}
@@ -183,4 +184,8 @@ export const SessionExpiredModal: React.FC = ({
); + + return typeof document !== 'undefined' + ? createPortal(modalContent, document.body) + : modalContent; }; diff --git a/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx b/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx index 80b0c2d..c34d60a 100644 --- a/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx +++ b/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx @@ -581,18 +581,21 @@ export const AppLayout: React.FC = () => { {/* Main Content Area */} -
-
-
-
+
+ {/* Ambient Glows & Dot Grid - Isolated inside overflow-hidden to prevent phantom page scrollbars */} +
+
+
+
+
-
+
diff --git a/PostFundManagement.ConsoleUI/src/components/ui/ErrorBoundary.tsx b/PostFundManagement.ConsoleUI/src/components/ui/ErrorBoundary.tsx index 03706f6..0a68b5e 100644 --- a/PostFundManagement.ConsoleUI/src/components/ui/ErrorBoundary.tsx +++ b/PostFundManagement.ConsoleUI/src/components/ui/ErrorBoundary.tsx @@ -1,5 +1,8 @@ -import React, { Component, ErrorInfo, ReactNode } from 'react'; +import React, { Component, ErrorInfo, ReactNode, useContext } from 'react'; +import { Link } from 'react-router-dom'; import { AlertTriangle, RefreshCw, Home } from 'lucide-react'; +import { AuthContext } from '../../auth/AuthProvider'; +import { evaluateUserPermission } from '../../auth/rbac'; interface Props { children: ReactNode; @@ -11,6 +14,70 @@ interface State { error: Error | null; } +interface FallbackContentProps { + fallbackTitle?: string; + error: Error | null; + onReset: () => void; +} + +const ErrorBoundaryFallbackContent: React.FC = ({ + fallbackTitle, + error, + onReset, +}) => { + const auth = useContext(AuthContext); + const roles = auth?.roles || []; + const claims = auth?.claims || {}; + const isUnrestricted = + roles.some((r) => r.toLowerCase().trim() === 'admin') || + claims?.unrestricted === true || + claims?.unrestricted === 'all'; + + const canAccessDashboard = isUnrestricted || evaluateUserPermission(roles, claims, 'dashboard', 'view'); + + return ( +
+
+
+ +
+

+ {fallbackTitle || 'Display Module Encountered an Issue'} +

+

+ {canAccessDashboard + ? 'An unexpected display error occurred while rendering this view. You can reload the view or return to the executive dashboard.' + : 'An unexpected display error occurred while rendering this view. Please try reloading the view.'} +

+ {error?.message && ( +
+ {error.message} +
+ )} +
+ + {canAccessDashboard && ( + + + Dashboard + + )} +
+
+
+ ); +}; + export class ErrorBoundary extends React.Component { declare props: Props; declare state: State; @@ -38,36 +105,11 @@ export class ErrorBoundary extends React.Component { public render() { if (this.state.hasError) { return ( -
-
-
- -
-

- {this.props.fallbackTitle || 'Display Module Encountered an Issue'} -

-

- An unexpected display error occurred while rendering this view. You can reload the view or return to the executive dashboard. -

-
- - - - Dashboard - -
-
-
+ ); } diff --git a/PostFundManagement.ConsoleUI/src/components/ui/Modal.tsx b/PostFundManagement.ConsoleUI/src/components/ui/Modal.tsx index fded33c..343d4a9 100644 --- a/PostFundManagement.ConsoleUI/src/components/ui/Modal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/ui/Modal.tsx @@ -1,4 +1,5 @@ import React, { ReactNode, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { X } from 'lucide-react'; import { HelpTooltip } from './HelpTooltip'; @@ -60,13 +61,18 @@ export const Modal: React.FC = ({ }; const maxWidthClass = maxWidthClassMap[effectiveSize] || 'max-w-lg'; - return ( -
+ const modalContent = ( +
e.stopPropagation()} > -
+

{title}

@@ -82,14 +88,20 @@ export const Modal: React.FC = ({ {subtitle &&

{subtitle}

}
-
{children}
+
{children}
); + + return typeof document !== 'undefined' + ? createPortal(modalContent, document.body) + : modalContent; }; diff --git a/PostFundManagement.ConsoleUI/src/components/users/UserSecurityModal.tsx b/PostFundManagement.ConsoleUI/src/components/users/UserSecurityModal.tsx index 2a1b48e..a55975b 100644 --- a/PostFundManagement.ConsoleUI/src/components/users/UserSecurityModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/users/UserSecurityModal.tsx @@ -210,24 +210,6 @@ export const UserSecurityModal: React.FC = ({ onError: (err: any) => toastError('Status Update Failed', err), }); - if (!user) return null; - - const effectiveUser = freshUser || user; - const decodedId = effectiveUser?.idNumber ? decodeIdNumberHex(effectiveUser.idNumber) : null; - const assignedRoleIds = new Set( - (Array.isArray(userRoles) ? userRoles : []).map((r) => r?.id || (r as any)?.Id).filter(Boolean) - ); - const availableRoles = (Array.isArray(systemRoles) ? systemRoles : []).filter((r) => { - if (!r) return false; - const rId = r.id || (r as any).Id; - const rName = r.name || (r as any).Name || ''; - if (rId && assignedRoleIds.has(rId)) return false; - if (hasTenantSelfHelpAccess && (rName.toLowerCase() === 'admin' || String(rId) === '1')) { - return false; - } - return true; - }); - const assignableClaims = React.useMemo(() => { const claimMap = new Map(); if (Array.isArray(systemClaims)) { @@ -255,6 +237,30 @@ export const UserSecurityModal: React.FC = ({ return Array.from(claimMap.values()); }, [systemClaims, hasTenantSelfHelpAccess]); + const assignedRoleIds = React.useMemo(() => { + return new Set( + (Array.isArray(userRoles) ? userRoles : []).map((r) => r?.id || (r as any)?.Id).filter(Boolean) + ); + }, [userRoles]); + + const availableRoles = React.useMemo(() => { + return (Array.isArray(systemRoles) ? systemRoles : []).filter((r) => { + if (!r) return false; + const rId = r.id || (r as any).Id; + const rName = r.name || (r as any).Name || ''; + if (rId && assignedRoleIds.has(rId)) return false; + if (hasTenantSelfHelpAccess && (rName.toLowerCase() === 'admin' || String(rId) === '1')) { + return false; + } + return true; + }); + }, [systemRoles, assignedRoleIds, hasTenantSelfHelpAccess]); + + if (!isOpen || !user) return null; + + const effectiveUser = freshUser || user; + const decodedId = effectiveUser?.idNumber ? decodeIdNumberHex(effectiveUser.idNumber) : null; + return ( = ({ }`} > - Roles ({userRoles.length}) + Roles ({(Array.isArray(userRoles) ? userRoles : []).length})
- {effectiveUser.status === ActivityStatus.Active ? ( + {effectiveUser?.status === ActivityStatus.Active ? (