Applied UI rendering fixes

This commit is contained in:
2026-10-22 06:23:22 +02:00
parent 87f597c091
commit c24e809b88
11 changed files with 261 additions and 154 deletions
@@ -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<AccountActivationModalProps> = ({
navigate('/');
};
return (
const modalContent = (
<div
role="dialog"
aria-modal="true"
aria-labelledby="activation-modal-title"
className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-[#060a12]/90 backdrop-blur-md animate-in fade-in duration-200 select-none"
className="fixed inset-0 z-[9999] flex items-center justify-center p-4 sm:p-6 bg-[#060a12]/90 backdrop-blur-md animate-in fade-in duration-200 select-none overflow-y-auto"
>
{/* Non-dismissable Card Container: No X button, no escape dismiss, no backdrop click */}
<div
className="relative w-full max-w-xl bg-[#0d1322] border border-amber-500/30 rounded-2xl shadow-2xl shadow-black/80 overflow-hidden flex flex-col text-slate-100"
className="relative w-full max-w-xl bg-[#0d1322] border border-amber-500/30 rounded-2xl shadow-2xl shadow-black/80 overflow-hidden flex flex-col text-slate-100 my-auto"
onClick={(e) => e.stopPropagation()}
>
{/* Amber accent top bar */}
@@ -278,4 +279,8 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
</div>
</div>
);
return typeof document !== 'undefined'
? createPortal(modalContent, document.body)
: modalContent;
};
@@ -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<PartnerAccessDeniedModalProps> =
navigate('/', { replace: true });
};
return (
const modalContent = (
<div
role="alertdialog"
aria-modal="true"
aria-labelledby="partner-denied-title"
aria-describedby="partner-denied-description"
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/85 backdrop-blur-md animate-in fade-in duration-200"
className="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-black/85 backdrop-blur-md animate-in fade-in duration-200 overflow-y-auto"
>
<div className="relative w-full max-w-lg overflow-hidden rounded-2xl bg-[#0e1424] border border-rose-900/60 shadow-2xl shadow-rose-950/50">
<div className="relative w-full max-w-lg overflow-hidden rounded-2xl bg-[#0e1424] border border-rose-900/60 shadow-2xl shadow-rose-950/50 my-auto">
{/* Glow Header Accent */}
<div className="h-2 w-full bg-linear-to-r from-rose-600 via-red-500 to-amber-500" />
@@ -171,4 +172,8 @@ export const PartnerAccessDeniedModal: React.FC<PartnerAccessDeniedModalProps> =
</div>
</div>
);
return typeof document !== 'undefined'
? createPortal(modalContent, document.body)
: modalContent;
};
@@ -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<SessionExpiredModalProps> = ({
const secondsRemaining = Math.max(0, Math.ceil(timeLeftMs / 1000));
const progressPercent = Math.max(0, Math.min(100, (timeLeftMs / totalDurationMs) * 100));
return (
const modalContent = (
<div
id="session-expired-modal-backdrop"
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/85 backdrop-blur-md p-4 animate-in fade-in duration-200"
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/85 backdrop-blur-md p-4 animate-in fade-in duration-200 overflow-y-auto"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
role="dialog"
@@ -95,7 +96,7 @@ export const SessionExpiredModal: React.FC<SessionExpiredModalProps> = ({
>
<div
id="session-expired-modal-content"
className="relative w-full max-w-md bg-[#0c121e] border border-amber-500/40 rounded-2xl shadow-2xl shadow-black/90 p-6 overflow-hidden text-slate-200 space-y-5"
className="relative w-full max-w-md bg-[#0c121e] border border-amber-500/40 rounded-2xl shadow-2xl shadow-black/90 p-6 overflow-hidden text-slate-200 space-y-5 my-auto"
>
{/* Subtle decorative top accent bar */}
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-amber-500 via-orange-500 to-rose-500" />
@@ -183,4 +184,8 @@ export const SessionExpiredModal: React.FC<SessionExpiredModalProps> = ({
</div>
</div>
);
return typeof document !== 'undefined'
? createPortal(modalContent, document.body)
: modalContent;
};
@@ -581,18 +581,21 @@ export const AppLayout: React.FC = () => {
</header>
{/* Main Content Area */}
<main className="flex-1 overflow-y-auto p-6 md:p-8 bg-[#090d16] relative text-slate-100">
<div
className="absolute inset-0 pointer-events-none opacity-15"
style={{
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(255,255,255,0.15) 1px, transparent 0)',
backgroundSize: '32px 32px',
}}
/>
<div className="absolute -top-24 -left-24 w-80 h-80 bg-blue-600/10 rounded-full blur-3xl pointer-events-none" />
<div className="absolute top-1/2 -right-24 w-80 h-80 bg-indigo-600/10 rounded-full blur-3xl pointer-events-none" />
<main className="flex-1 overflow-y-auto min-w-0 p-4 sm:p-6 md:p-8 bg-[#090d16] relative text-slate-100">
{/* Ambient Glows & Dot Grid - Isolated inside overflow-hidden to prevent phantom page scrollbars */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div
className="absolute inset-0 opacity-15"
style={{
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(255,255,255,0.15) 1px, transparent 0)',
backgroundSize: '32px 32px',
}}
/>
<div className="absolute -top-24 -left-24 w-80 h-80 bg-blue-600/10 rounded-full blur-3xl" />
<div className="absolute top-1/2 -right-24 w-80 h-80 bg-indigo-600/10 rounded-full blur-3xl" />
</div>
<div className="relative z-10">
<div className="relative z-10 min-w-0 w-full">
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
@@ -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<FallbackContentProps> = ({
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 (
<div className="min-h-[400px] flex items-center justify-center p-6">
<div className="max-w-md w-full bg-[#0f1523]/90 backdrop-blur-md rounded-2xl border border-rose-500/30 p-6 text-center shadow-2xl">
<div className="w-12 h-12 rounded-2xl bg-rose-500/10 border border-rose-500/20 text-rose-400 flex items-center justify-center mx-auto mb-4">
<AlertTriangle className="w-6 h-6" />
</div>
<h3 className="text-base font-bold text-white mb-2">
{fallbackTitle || 'Display Module Encountered an Issue'}
</h3>
<p className="text-xs text-slate-400 mb-4 leading-relaxed">
{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.'}
</p>
{error?.message && (
<div className="mb-4 p-2.5 bg-rose-950/40 rounded-xl border border-rose-800/40 text-left text-[11px] text-rose-300 font-mono break-all max-h-24 overflow-y-auto">
{error.message}
</div>
)}
<div className="flex items-center justify-center gap-3">
<button
type="button"
onClick={onReset}
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold bg-slate-800 hover:bg-slate-700 text-white border border-slate-700 transition cursor-pointer"
>
<RefreshCw className="w-3.5 h-3.5 text-blue-400" />
<span>Reload View</span>
</button>
{canAccessDashboard && (
<Link
to="/dashboard"
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold bg-blue-600 hover:bg-blue-500 text-white transition cursor-pointer shadow-md shadow-blue-500/20"
>
<Home className="w-3.5 h-3.5" />
<span>Dashboard</span>
</Link>
)}
</div>
</div>
</div>
);
};
export class ErrorBoundary extends React.Component<Props, State> {
declare props: Props;
declare state: State;
@@ -38,36 +105,11 @@ export class ErrorBoundary extends React.Component<Props, State> {
public render() {
if (this.state.hasError) {
return (
<div className="min-h-[400px] flex items-center justify-center p-6">
<div className="max-w-md w-full bg-[#0f1523]/90 backdrop-blur-md rounded-2xl border border-rose-500/30 p-6 text-center shadow-2xl">
<div className="w-12 h-12 rounded-2xl bg-rose-500/10 border border-rose-500/20 text-rose-400 flex items-center justify-center mx-auto mb-4">
<AlertTriangle className="w-6 h-6" />
</div>
<h3 className="text-base font-bold text-white mb-2">
{this.props.fallbackTitle || 'Display Module Encountered an Issue'}
</h3>
<p className="text-xs text-slate-400 mb-6 leading-relaxed">
An unexpected display error occurred while rendering this view. You can reload the view or return to the executive dashboard.
</p>
<div className="flex items-center justify-center gap-3">
<button
type="button"
onClick={this.handleReset}
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold bg-slate-800 hover:bg-slate-700 text-white border border-slate-700 transition cursor-pointer"
>
<RefreshCw className="w-3.5 h-3.5 text-blue-400" />
<span>Reload View</span>
</button>
<a
href="/dashboard"
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold bg-blue-600 hover:bg-blue-500 text-white transition cursor-pointer shadow-md shadow-blue-500/20"
>
<Home className="w-3.5 h-3.5" />
<span>Dashboard</span>
</a>
</div>
</div>
</div>
<ErrorBoundaryFallbackContent
fallbackTitle={this.props.fallbackTitle}
error={this.state.error}
onReset={this.handleReset}
/>
);
}
@@ -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<ModalProps> = ({
};
const maxWidthClass = maxWidthClassMap[effectiveSize] || 'max-w-lg';
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-150">
const modalContent = (
<div
className="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-black/80 backdrop-blur-md animate-in fade-in duration-150 overflow-y-auto"
onClick={onClose}
role="dialog"
aria-modal="true"
>
<div
className={`relative w-full ${maxWidthClass} bg-[#0f1523] rounded-2xl shadow-2xl border border-slate-800/90 overflow-hidden flex flex-col max-h-[90vh] text-slate-200`}
className={`relative w-full ${maxWidthClass} bg-[#0f1523] rounded-2xl shadow-2xl border border-slate-800/90 overflow-hidden flex flex-col max-h-[90vh] my-auto text-slate-200`}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-start justify-between p-5 border-b border-slate-800/80 bg-[#0d121f]/90">
<div className="flex items-start justify-between p-5 border-b border-slate-800/80 bg-[#0d121f]/90 shrink-0">
<div>
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-white tracking-tight">{title}</h3>
@@ -82,14 +88,20 @@ export const Modal: React.FC<ModalProps> = ({
{subtitle && <p className="text-xs text-slate-400 mt-0.5">{subtitle}</p>}
</div>
<button
type="button"
onClick={onClose}
className="rounded-xl p-1.5 text-slate-400 hover:text-white hover:bg-slate-800/60 transition cursor-pointer"
aria-label="Close dialog"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6 overflow-y-auto flex-1 bg-[#0f1523]">{children}</div>
<div className="p-6 overflow-y-auto flex-1 bg-[#0f1523] min-w-0">{children}</div>
</div>
</div>
);
return typeof document !== 'undefined'
? createPortal(modalContent, document.body)
: modalContent;
};
@@ -210,24 +210,6 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
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<string, { claim: string; label: string }>();
if (Array.isArray(systemClaims)) {
@@ -255,6 +237,30 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
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 (
<Modal
isOpen={isOpen}
@@ -286,7 +292,7 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
}`}
>
<Shield className="w-3.5 h-3.5" />
<span>Roles ({userRoles.length})</span>
<span>Roles ({(Array.isArray(userRoles) ? userRoles : []).length})</span>
</button>
<button
type="button"
@@ -298,7 +304,7 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
}`}
>
<Key className="w-3.5 h-3.5" />
<span>Claims ({userClaims.length})</span>
<span>Claims ({(Array.isArray(userClaims) ? userClaims : []).length})</span>
</button>
<button
type="button"
@@ -358,14 +364,14 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
</span>
<span
className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold border ${
(getActivityStatusBadge(effectiveUser.status) || {}).className || 'bg-slate-800 text-slate-300'
(getActivityStatusBadge(effectiveUser?.status) || {}).className || 'bg-slate-800 text-slate-300'
}`}
>
{(getActivityStatusBadge(effectiveUser.status) || {}).label || 'Active'}
{(getActivityStatusBadge(effectiveUser?.status) || {}).label || 'Active'}
</span>
</div>
<div className="flex items-center gap-2">
{effectiveUser.status === ActivityStatus.Active ? (
{effectiveUser?.status === ActivityStatus.Active ? (
<button
type="button"
onClick={() => statusMutation.mutate(ActivityStatus.Inactive)}
@@ -404,11 +410,15 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
<option value="">
{availableRoles.length === 0 ? 'All system roles assigned' : 'Select role to assign...'}
</option>
{availableRoles.map((r) => (
<option key={r.id} value={r.id}>
{r.name}
</option>
))}
{availableRoles.map((r: any) => {
const roleOptId = r?.id || r?.Id || String(r);
const roleOptName = r?.name || r?.Name || String(r);
return (
<option key={roleOptId} value={roleOptId}>
{roleOptName}
</option>
);
})}
</select>
<button
type="button"