Applied UI rendering fixes
This commit is contained in:
@@ -64,7 +64,7 @@ interface AuthContextType {
|
|||||||
authError: string | null;
|
authError: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const LOCAL_STORAGE_TOKEN_KEY = 'pfm_access_token';
|
export const LOCAL_STORAGE_TOKEN_KEY = 'pfm_access_token';
|
||||||
export const LOCAL_STORAGE_USER_KEY = 'pfm_auth_user';
|
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);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
if (urlParams.has('code') && urlParams.has('state')) {
|
if (urlParams.has('code') && urlParams.has('state')) {
|
||||||
const mgr = getUserManager();
|
const mgr = getUserManager();
|
||||||
const oidcUser: OidcUser = await mgr.signinCallback();
|
const oidcUser: OidcUser | undefined = await mgr.signinCallback();
|
||||||
|
|
||||||
if (oidcUser && oidcUser.access_token) {
|
if (oidcUser && oidcUser.access_token) {
|
||||||
const token = oidcUser.access_token;
|
const token = oidcUser.access_token;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
@@ -113,16 +114,16 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
|
|||||||
navigate('/');
|
navigate('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const modalContent = (
|
||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="activation-modal-title"
|
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 */}
|
{/* Non-dismissable Card Container: No X button, no escape dismiss, no backdrop click */}
|
||||||
<div
|
<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()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Amber accent top bar */}
|
{/* Amber accent top bar */}
|
||||||
@@ -278,4 +279,8 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return typeof document !== 'undefined'
|
||||||
|
? createPortal(modalContent, document.body)
|
||||||
|
: modalContent;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
@@ -54,15 +55,15 @@ export const PartnerAccessDeniedModal: React.FC<PartnerAccessDeniedModalProps> =
|
|||||||
navigate('/', { replace: true });
|
navigate('/', { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const modalContent = (
|
||||||
<div
|
<div
|
||||||
role="alertdialog"
|
role="alertdialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="partner-denied-title"
|
aria-labelledby="partner-denied-title"
|
||||||
aria-describedby="partner-denied-description"
|
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 */}
|
{/* Glow Header Accent */}
|
||||||
<div className="h-2 w-full bg-linear-to-r from-rose-600 via-red-500 to-amber-500" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return typeof document !== 'undefined'
|
||||||
|
? createPortal(modalContent, document.body)
|
||||||
|
: modalContent;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { ShieldAlert, Clock, ArrowRight, X, AlertTriangle, LogOut } from 'lucide-react';
|
import { ShieldAlert, Clock, ArrowRight, X, AlertTriangle, LogOut } from 'lucide-react';
|
||||||
import { clearSessionAndRedirectToLanding, dismissSubordinateModals } from '../../services/sessionExpiration';
|
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 secondsRemaining = Math.max(0, Math.ceil(timeLeftMs / 1000));
|
||||||
const progressPercent = Math.max(0, Math.min(100, (timeLeftMs / totalDurationMs) * 100));
|
const progressPercent = Math.max(0, Math.min(100, (timeLeftMs / totalDurationMs) * 100));
|
||||||
|
|
||||||
return (
|
const modalContent = (
|
||||||
<div
|
<div
|
||||||
id="session-expired-modal-backdrop"
|
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)}
|
onMouseEnter={() => setIsPaused(true)}
|
||||||
onMouseLeave={() => setIsPaused(false)}
|
onMouseLeave={() => setIsPaused(false)}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
@@ -95,7 +96,7 @@ export const SessionExpiredModal: React.FC<SessionExpiredModalProps> = ({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="session-expired-modal-content"
|
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 */}
|
{/* 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" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return typeof document !== 'undefined'
|
||||||
|
? createPortal(modalContent, document.body)
|
||||||
|
: modalContent;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -581,18 +581,21 @@ export const AppLayout: React.FC = () => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<main className="flex-1 overflow-y-auto p-6 md:p-8 bg-[#090d16] relative text-slate-100">
|
<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
|
<div
|
||||||
className="absolute inset-0 pointer-events-none opacity-15"
|
className="absolute inset-0 opacity-15"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(255,255,255,0.15) 1px, transparent 0)',
|
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(255,255,255,0.15) 1px, transparent 0)',
|
||||||
backgroundSize: '32px 32px',
|
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-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 pointer-events-none" />
|
<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>
|
<ErrorBoundary>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</ErrorBoundary>
|
</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 { AlertTriangle, RefreshCw, Home } from 'lucide-react';
|
||||||
|
import { AuthContext } from '../../auth/AuthProvider';
|
||||||
|
import { evaluateUserPermission } from '../../auth/rbac';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -11,6 +14,70 @@ interface State {
|
|||||||
error: Error | null;
|
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> {
|
export class ErrorBoundary extends React.Component<Props, State> {
|
||||||
declare props: Props;
|
declare props: Props;
|
||||||
declare state: State;
|
declare state: State;
|
||||||
@@ -38,36 +105,11 @@ export class ErrorBoundary extends React.Component<Props, State> {
|
|||||||
public render() {
|
public render() {
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[400px] flex items-center justify-center p-6">
|
<ErrorBoundaryFallbackContent
|
||||||
<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">
|
fallbackTitle={this.props.fallbackTitle}
|
||||||
<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">
|
error={this.state.error}
|
||||||
<AlertTriangle className="w-6 h-6" />
|
onReset={this.handleReset}
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { ReactNode, useEffect } from 'react';
|
import React, { ReactNode, useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { HelpTooltip } from './HelpTooltip';
|
import { HelpTooltip } from './HelpTooltip';
|
||||||
|
|
||||||
@@ -60,13 +61,18 @@ export const Modal: React.FC<ModalProps> = ({
|
|||||||
};
|
};
|
||||||
const maxWidthClass = maxWidthClassMap[effectiveSize] || 'max-w-lg';
|
const maxWidthClass = maxWidthClassMap[effectiveSize] || 'max-w-lg';
|
||||||
|
|
||||||
return (
|
const modalContent = (
|
||||||
<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">
|
|
||||||
<div
|
<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="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] my-auto text-slate-200`}
|
||||||
onClick={(e) => e.stopPropagation()}
|
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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="text-lg font-semibold text-white tracking-tight">{title}</h3>
|
<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>}
|
{subtitle && <p className="text-xs text-slate-400 mt-0.5">{subtitle}</p>}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="rounded-xl p-1.5 text-slate-400 hover:text-white hover:bg-slate-800/60 transition cursor-pointer"
|
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" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
</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),
|
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 assignableClaims = React.useMemo(() => {
|
||||||
const claimMap = new Map<string, { claim: string; label: string }>();
|
const claimMap = new Map<string, { claim: string; label: string }>();
|
||||||
if (Array.isArray(systemClaims)) {
|
if (Array.isArray(systemClaims)) {
|
||||||
@@ -255,6 +237,30 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
|||||||
return Array.from(claimMap.values());
|
return Array.from(claimMap.values());
|
||||||
}, [systemClaims, hasTenantSelfHelpAccess]);
|
}, [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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@@ -286,7 +292,7 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Shield className="w-3.5 h-3.5" />
|
<Shield className="w-3.5 h-3.5" />
|
||||||
<span>Roles ({userRoles.length})</span>
|
<span>Roles ({(Array.isArray(userRoles) ? userRoles : []).length})</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -298,7 +304,7 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Key className="w-3.5 h-3.5" />
|
<Key className="w-3.5 h-3.5" />
|
||||||
<span>Claims ({userClaims.length})</span>
|
<span>Claims ({(Array.isArray(userClaims) ? userClaims : []).length})</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -358,14 +364,14 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold border ${
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{effectiveUser.status === ActivityStatus.Active ? (
|
{effectiveUser?.status === ActivityStatus.Active ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => statusMutation.mutate(ActivityStatus.Inactive)}
|
onClick={() => statusMutation.mutate(ActivityStatus.Inactive)}
|
||||||
@@ -404,11 +410,15 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
|||||||
<option value="">
|
<option value="">
|
||||||
{availableRoles.length === 0 ? 'All system roles assigned' : 'Select role to assign...'}
|
{availableRoles.length === 0 ? 'All system roles assigned' : 'Select role to assign...'}
|
||||||
</option>
|
</option>
|
||||||
{availableRoles.map((r) => (
|
{availableRoles.map((r: any) => {
|
||||||
<option key={r.id} value={r.id}>
|
const roleOptId = r?.id || r?.Id || String(r);
|
||||||
{r.name}
|
const roleOptName = r?.name || r?.Name || String(r);
|
||||||
|
return (
|
||||||
|
<option key={roleOptId} value={roleOptId}>
|
||||||
|
{roleOptName}
|
||||||
</option>
|
</option>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -33,13 +33,18 @@
|
|||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom dark scrollbar styling */
|
/* Custom dark scrollbar styling - auto-appearing with transparent tracks */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #262626 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #0a0a0a;
|
background: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #262626;
|
background: #262626;
|
||||||
@@ -48,6 +53,18 @@
|
|||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #3f3f46;
|
background: #3f3f46;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Table responsive containment and smooth auto-scrolling */
|
||||||
|
.overflow-x-auto {
|
||||||
|
overflow-x: auto !important;
|
||||||
|
max-width: 100%;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure tables preserve minimum readable width without squashing action buttons */
|
||||||
|
.overflow-x-auto > table {
|
||||||
|
min-width: 780px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* High Contrast Dark Theme Adaptations for existing utility classes */
|
/* High Contrast Dark Theme Adaptations for existing utility classes */
|
||||||
|
|||||||
@@ -533,7 +533,7 @@ export const SettingsPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-7xl mx-auto">
|
<div className="space-y-6 max-w-7xl mx-auto min-w-0 w-full">
|
||||||
{/* Header Banner */}
|
{/* Header Banner */}
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-gradient-to-r from-[#0d1322] via-[#0f172a] to-[#0d1322] p-6 sm:p-8 rounded-3xl border border-slate-800/80 shadow-2xl relative overflow-hidden">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-gradient-to-r from-[#0d1322] via-[#0f172a] to-[#0d1322] p-6 sm:p-8 rounded-3xl border border-slate-800/80 shadow-2xl relative overflow-hidden">
|
||||||
<div className="absolute -right-10 -bottom-10 w-60 h-60 bg-blue-600/10 rounded-full blur-3xl pointer-events-none" />
|
<div className="absolute -right-10 -bottom-10 w-60 h-60 bg-blue-600/10 rounded-full blur-3xl pointer-events-none" />
|
||||||
@@ -819,8 +819,8 @@ export const SettingsPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto min-w-0 w-full">
|
||||||
<table className="w-full text-left text-xs border-collapse">
|
<table className="w-full text-left text-xs border-collapse min-w-[850px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
|
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
|
||||||
<SortableTh column="email" currentColumn={userSortCol} direction={userSortDir} onSort={handleUserSort} className="py-3.5 px-5">
|
<SortableTh column="email" currentColumn={userSortCol} direction={userSortDir} onSort={handleUserSort} className="py-3.5 px-5">
|
||||||
@@ -1096,8 +1096,8 @@ export const SettingsPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto min-w-0 w-full">
|
||||||
<table className="w-full text-left text-xs border-collapse">
|
<table className="w-full text-left text-xs border-collapse min-w-[850px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
|
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
|
||||||
<SortableTh column="recipient" currentColumn={inviteSortCol} direction={inviteSortDir} onSort={handleInviteSort} className="py-3.5 px-5">
|
<SortableTh column="recipient" currentColumn={inviteSortCol} direction={inviteSortDir} onSort={handleInviteSort} className="py-3.5 px-5">
|
||||||
@@ -1372,8 +1372,8 @@ export const SettingsPage: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto min-w-0 w-full">
|
||||||
<table className="w-full text-left border-collapse text-xs">
|
<table className="w-full text-left border-collapse text-xs min-w-[900px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-slate-800 bg-slate-900/40 text-slate-400 font-medium">
|
<tr className="border-b border-slate-800 bg-slate-900/40 text-slate-400 font-medium">
|
||||||
<SortableTh column="id" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
|
<SortableTh column="id" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
|
||||||
@@ -1639,8 +1639,8 @@ export const SettingsPage: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto min-w-0 w-full">
|
||||||
<table className="w-full text-left text-xs border-collapse">
|
<table className="w-full text-left text-xs border-collapse min-w-[850px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
|
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
|
||||||
<th className="py-3.5 px-3 w-10 text-center"></th>
|
<th className="py-3.5 px-3 w-10 text-center"></th>
|
||||||
@@ -1800,28 +1800,28 @@ export const SettingsPage: React.FC = () => {
|
|||||||
<tr className="bg-[#080c14] border-b border-slate-800/80">
|
<tr className="bg-[#080c14] border-b border-slate-800/80">
|
||||||
<td colSpan={8} className="p-4 sm:p-5 bg-gradient-to-r from-[#0d121f] via-[#090d16] to-[#0d121f]">
|
<td colSpan={8} className="p-4 sm:p-5 bg-gradient-to-r from-[#0d121f] via-[#090d16] to-[#0d121f]">
|
||||||
<div className="rounded-xl border border-slate-800/90 bg-[#070a12]/90 p-4 space-y-3 shadow-inner">
|
<div className="rounded-xl border border-slate-800/90 bg-[#070a12]/90 p-4 space-y-3 shadow-inner">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-slate-800/80 pb-3">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-slate-800/80 pb-3 flex-wrap">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Layers className="w-4 h-4 text-blue-400" />
|
<Layers className="w-4 h-4 text-blue-400 shrink-0" />
|
||||||
<h4 className="text-xs font-bold text-white uppercase tracking-wider">
|
<h4 className="text-xs font-bold text-white uppercase tracking-wider">
|
||||||
Recorded Changes Content (Event #{log.id})
|
Recorded Changes Content (Event #{log.id})
|
||||||
</h4>
|
</h4>
|
||||||
<span className="text-[10px] font-mono px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-400 border border-blue-500/20 font-semibold">
|
<span className="text-[10px] font-mono px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-400 border border-blue-500/20 font-semibold shrink-0">
|
||||||
{changesCount} {changesCount === 1 ? 'Entry' : 'Entries'}
|
{changesCount} {changesCount === 1 ? 'Entry' : 'Entries'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 text-xs text-slate-400">
|
<div className="flex items-center gap-2 sm:gap-3 text-xs text-slate-400 flex-wrap min-w-0">
|
||||||
<span>
|
<span className="truncate max-w-[260px] sm:max-w-none">
|
||||||
Entity: <strong className="text-white">{log.entityName || 'General'}</strong> {log.entityId ? `(#${log.entityId})` : ''}
|
Entity: <strong className="text-white">{log.entityName || 'General'}</strong> {log.entityId ? `(#${log.entityId})` : ''}
|
||||||
</span>
|
</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1 min-w-0">
|
||||||
Performer: <strong className="text-slate-200 font-mono text-[11px]">{displayEmail}</strong>
|
Performer: <strong className="text-slate-200 font-mono text-[11px] truncate max-w-[220px] sm:max-w-none">{displayEmail}</strong>
|
||||||
{log.performedBy && (
|
{log.performedBy && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => handleCopyUserId(log.performedBy, e)}
|
onClick={(e) => handleCopyUserId(log.performedBy, e)}
|
||||||
className="p-1 rounded hover:bg-slate-800 text-slate-400 hover:text-white transition cursor-pointer"
|
className="p-1 rounded hover:bg-slate-800 text-slate-400 hover:text-white transition cursor-pointer shrink-0"
|
||||||
title={`Copy User ID: ${log.performedBy}`}
|
title={`Copy User ID: ${log.performedBy}`}
|
||||||
>
|
>
|
||||||
{copiedUserId === log.performedBy ? (
|
{copiedUserId === log.performedBy ? (
|
||||||
@@ -1884,10 +1884,11 @@ export const SettingsPage: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
title={`Audit Event: #${selectedAuditLog?.id || ''}`}
|
title={`Audit Event: #${selectedAuditLog?.id || ''}`}
|
||||||
subtitle="Authoritative event payload and activity ledger"
|
subtitle="Authoritative event payload and activity ledger"
|
||||||
|
maxWidth="xl"
|
||||||
>
|
>
|
||||||
{selectedAuditLog && (
|
{selectedAuditLog && (
|
||||||
<div className="space-y-4 text-xs">
|
<div className="space-y-4 text-xs min-w-0">
|
||||||
<div className="p-4 bg-[#0d121f] rounded-xl border border-slate-800 space-y-3">
|
<div className="p-4 bg-[#0d121f] rounded-xl border border-slate-800 space-y-3 min-w-0">
|
||||||
<div className="flex justify-between items-center border-b border-slate-800/80 pb-2">
|
<div className="flex justify-between items-center border-b border-slate-800/80 pb-2">
|
||||||
<span className="text-slate-400 font-semibold uppercase text-[11px]">Action Type:</span>
|
<span className="text-slate-400 font-semibold uppercase text-[11px]">Action Type:</span>
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold border ${getActionBadge(selectedAuditLog.action).className}`}>
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold border ${getActionBadge(selectedAuditLog.action).className}`}>
|
||||||
@@ -1895,22 +1896,22 @@ export const SettingsPage: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3 min-w-0">
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Entity Name:</span>
|
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Entity Name:</span>
|
||||||
<span className="text-white font-semibold">{selectedAuditLog.entityName || '—'}</span>
|
<span className="text-white font-semibold truncate block">{selectedAuditLog.entityName || '—'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Entity ID:</span>
|
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Entity ID:</span>
|
||||||
<span className="font-mono text-slate-300">{selectedAuditLog.entityId || '—'}</span>
|
<span className="font-mono text-slate-300 break-all block">{selectedAuditLog.entityId || '—'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3 border-t border-slate-800/80 pt-2">
|
<div className="grid grid-cols-2 gap-3 border-t border-slate-800/80 pt-2 min-w-0">
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Performed By:</span>
|
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Performed By:</span>
|
||||||
<div className="flex items-center gap-1.5 mt-0.5">
|
<div className="flex items-center gap-1.5 mt-0.5 min-w-0">
|
||||||
<span className="font-mono text-slate-200 text-[11px] truncate">
|
<span className="font-mono text-slate-200 text-[11px] truncate block" title={selectedAuditLog.performedBy || 'System'}>
|
||||||
{getUserEmail(selectedAuditLog.performedBy) !== '—'
|
{getUserEmail(selectedAuditLog.performedBy) !== '—'
|
||||||
? getUserEmail(selectedAuditLog.performedBy)
|
? getUserEmail(selectedAuditLog.performedBy)
|
||||||
: (selectedAuditLog.performedBy || 'System')}
|
: (selectedAuditLog.performedBy || 'System')}
|
||||||
@@ -1931,32 +1932,34 @@ export const SettingsPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{selectedAuditLog.performedBy && (
|
{selectedAuditLog.performedBy && (
|
||||||
<span className="text-[10px] text-slate-500 font-mono block mt-0.5 truncate">
|
<span className="text-[10px] text-slate-500 font-mono block mt-0.5 truncate" title={`ID: ${selectedAuditLog.performedBy}`}>
|
||||||
ID: {selectedAuditLog.performedBy}
|
ID: {selectedAuditLog.performedBy}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Logged Timestamp:</span>
|
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Logged Timestamp:</span>
|
||||||
<span className="font-mono text-slate-300 block mt-0.5">{formatDate(selectedAuditLog.timestamp)}</span>
|
<span className="font-mono text-slate-300 block mt-0.5">{formatDate(selectedAuditLog.timestamp)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedAuditLog.tenantId && (
|
{selectedAuditLog.tenantId && (
|
||||||
<div className="border-t border-slate-800/80 pt-2">
|
<div className="border-t border-slate-800/80 pt-2 min-w-0">
|
||||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Tenant Scope:</span>
|
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Tenant Scope:</span>
|
||||||
<span className="font-mono text-slate-300">Tenant #{selectedAuditLog.tenantId}</span>
|
<span className="font-mono text-slate-300">Tenant #{selectedAuditLog.tenantId}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="border-t border-slate-800/80 pt-2 space-y-1">
|
<div className="border-t border-slate-800/80 pt-2 space-y-1 min-w-0">
|
||||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase mb-1">State Changes / Delta:</span>
|
<span className="text-slate-400 font-semibold block text-[11px] uppercase mb-1">State Changes / Delta:</span>
|
||||||
{Array.isArray(selectedAuditLog.changes) && selectedAuditLog.changes.length > 0 ? (
|
{Array.isArray(selectedAuditLog.changes) && selectedAuditLog.changes.length > 0 ? (
|
||||||
<ul className="space-y-1 p-2 rounded-lg bg-slate-900 border border-slate-800">
|
<ul className="space-y-1.5 p-2.5 rounded-lg bg-slate-900 border border-slate-800 min-w-0 max-h-56 overflow-y-auto">
|
||||||
{selectedAuditLog.changes.map((ch, idx) => (
|
{selectedAuditLog.changes.map((ch, idx) => (
|
||||||
<li key={idx} className="font-mono text-emerald-400/90 text-[11px] flex items-center gap-1.5">
|
<li key={idx} className="font-mono text-emerald-400/90 text-[11px] flex items-start gap-2 min-w-0">
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 shrink-0 mt-1.5" />
|
||||||
|
<span className="break-all whitespace-pre-wrap min-w-0 flex-1 leading-relaxed">
|
||||||
{ch}
|
{ch}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -1968,7 +1971,7 @@ export const SettingsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Raw JSON Data Viewer */}
|
{/* Raw JSON Data Viewer */}
|
||||||
<div className="border-t border-slate-800/80 pt-2.5 space-y-1.5">
|
<div className="border-t border-slate-800/80 pt-2.5 space-y-1.5 min-w-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-slate-400 font-semibold text-[11px] uppercase">
|
<span className="text-slate-400 font-semibold text-[11px] uppercase">
|
||||||
Raw Audit Record (JSON):
|
Raw Audit Record (JSON):
|
||||||
@@ -1994,7 +1997,7 @@ export const SettingsPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<pre className="p-3 rounded-lg bg-[#070a12] border border-slate-800/80 text-emerald-300/90 font-mono text-[10.5px] overflow-x-auto max-h-48">
|
<pre className="p-3 rounded-lg bg-[#070a12] border border-slate-800/80 text-emerald-300/90 font-mono text-[10.5px] overflow-x-auto whitespace-pre-wrap break-all max-h-48 min-w-0">
|
||||||
{JSON.stringify(selectedAuditLog, null, 2)}
|
{JSON.stringify(selectedAuditLog, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -2216,6 +2219,7 @@ export const SettingsPage: React.FC = () => {
|
|||||||
{/* ============================================================== */}
|
{/* ============================================================== */}
|
||||||
{/* Modal: User Administration & Role/Claim/Session Control */}
|
{/* Modal: User Administration & Role/Claim/Session Control */}
|
||||||
{/* ============================================================== */}
|
{/* ============================================================== */}
|
||||||
|
{isUserDetailModalOpen && selectedUserForDetail && (
|
||||||
<UserSecurityModal
|
<UserSecurityModal
|
||||||
isOpen={isUserDetailModalOpen}
|
isOpen={isUserDetailModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
@@ -2224,6 +2228,7 @@ export const SettingsPage: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
user={selectedUserForDetail}
|
user={selectedUserForDetail}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ============================================================== */}
|
{/* ============================================================== */}
|
||||||
{/* Modal: Provision Tenant User */}
|
{/* Modal: Provision Tenant User */}
|
||||||
|
|||||||
@@ -309,8 +309,8 @@ const TenantUsersSubList: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto rounded-lg border border-slate-800/80 bg-slate-900/40">
|
<div className="overflow-x-auto rounded-lg border border-slate-800/80 bg-slate-900/40 min-w-0 w-full">
|
||||||
<table className="w-full text-left text-xs border-collapse">
|
<table className="w-full text-left text-xs border-collapse min-w-[700px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-slate-900/80 border-b border-slate-800/80 text-slate-400 text-[11px] uppercase tracking-wider font-semibold">
|
<tr className="bg-slate-900/80 border-b border-slate-800/80 text-slate-400 text-[11px] uppercase tracking-wider font-semibold">
|
||||||
<th className="py-2 px-3">Email Address</th>
|
<th className="py-2 px-3">Email Address</th>
|
||||||
@@ -351,13 +351,14 @@ const TenantUsersSubList: React.FC<{
|
|||||||
<td className="py-2 px-3 text-right">
|
<td className="py-2 px-3 text-right">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const userToManage: User = u || {
|
const userToManage: User = {
|
||||||
id: tu.userId,
|
...(u || {}),
|
||||||
email: null,
|
id: u?.id || tu.userId,
|
||||||
role: null,
|
status: u?.status ?? tu.status,
|
||||||
idNumber: null,
|
email: u?.email || null,
|
||||||
status: tu.status,
|
role: u?.role || null,
|
||||||
lastLoginAt: null,
|
idNumber: u?.idNumber || null,
|
||||||
|
lastLoginAt: u?.lastLoginAt || null,
|
||||||
};
|
};
|
||||||
onManageUser(userToManage);
|
onManageUser(userToManage);
|
||||||
}}
|
}}
|
||||||
@@ -782,9 +783,9 @@ export const TenantManagementPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tenants Table */}
|
{/* Tenants Table */}
|
||||||
<div className="bg-[#0f1523]/80 rounded-2xl border border-slate-800/80 overflow-hidden shadow-xl backdrop-blur-md">
|
<div className="bg-[#0f1523]/80 rounded-2xl border border-slate-800/80 overflow-hidden shadow-xl backdrop-blur-md min-w-0 w-full">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto min-w-0 w-full">
|
||||||
<table className="w-full text-left text-xs border-collapse">
|
<table className="w-full text-left text-xs border-collapse min-w-[800px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 font-semibold uppercase tracking-wider">
|
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 font-semibold uppercase tracking-wider">
|
||||||
<th className="py-3.5 px-3 w-10 text-center">
|
<th className="py-3.5 px-3 w-10 text-center">
|
||||||
@@ -1198,6 +1199,7 @@ export const TenantManagementPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* User Security & Administration Modal */}
|
{/* User Security & Administration Modal */}
|
||||||
|
{isSecurityModalOpen && securityUser && (
|
||||||
<UserSecurityModal
|
<UserSecurityModal
|
||||||
isOpen={isSecurityModalOpen}
|
isOpen={isSecurityModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
@@ -1206,6 +1208,7 @@ export const TenantManagementPage: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
user={securityUser}
|
user={securityUser}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user