Merge pull request 'Finishline' (#146) from finishline into main

Reviewed-on: #146
This commit was merged in pull request #146.
This commit is contained in:
2026-10-22 06:24:18 +02:00
12 changed files with 439 additions and 210 deletions
+163 -41
View File
@@ -955,64 +955,186 @@ PostFundManagement.Infrastructure PostFundManagement.Domain
└── Migrations/ └── Abstractions/ # IEntity, IAggregateRoot, IService └── Migrations/ └── Abstractions/ # IEntity, IAggregateRoot, IService
``` ```
### 16.2 C# 13 & .NET 10 Coding Conventions ### 16.2 Architectural Balance: Clean Architecture, Vertical Slices, SOLID & DRY
1. **Primary Constructors & Modern Types**: The PFM backend strikes a deliberate, pragmatic balance between **Clean Architecture**, **Vertical Slice Architecture**, **SOLID principles**, and the **DRY (Don't Repeat Yourself)** principle:
* Services and endpoints use primary constructors:
```csharp 1. **Clean Architecture with Vertical Slices**:
public sealed class ProjectService( * **Domain Layer**: Completely isolated aggregate roots, entities, taxonomies (`Enums`), domain errors (`IError`), and abstractions. It has no dependencies on database technologies or presentation frameworks.
IDbContextFactory<ApplicationDbContext> contextFactory, * **Infrastructure Layer**: Encapsulates EF Core configurations, PostgreSQL mappings, and data protection.
HashService hashService, * **Application & Api Layers by Vertical Slice**: Features are organized cleanly into domain feature slices (`Projects`, `Grants`, `Execution`, `Governance`, `Evidence`, `Communications`, `Users`). Each slice groups its commands/queries, records (`Records.cs`), core service (`[Slice]Service`), hybrid cache decorator (`[Slice]CacheService`), and Minimal API endpoints (`[Action]Endpoint.cs`), preserving strong cohesion without code bloat.
ITenantContextService tenantContext) : IProjectService
``` 2. **Strict Adherence to SOLID Design Principles**:
* Extensive use of expression-bodied members (`=>`) for concise method definitions. * **Single Responsibility Principle (SRP)**:
* Nullable reference types (`<Nullable>enable</Nullable>`) strictly enforced. * `[Slice]Service`: Implements pure business logic, database transactions, and entity mapping.
2. **Functional Result Pattern (`FluentResults`)**: * `[Slice]CacheService`: Implements read caching and mutation eviction via `HybridCache`.
* Domain and Application services **never throw exceptions for predictable business validation failures**. * `LoggingInterceptor`: Exclusively manages structured logging upon failure.
* Methods return `Task<Result>` or `Task<Result<T>>`. * `AuditInterceptor`: Exclusively manages regulatory audit logging upon success (`BR-007`).
* Standard error objects in `PostFundManagement.Domain.Errors`: * `[Action]Endpoint`: Exclusively handles route binding, OpenAPI documentation, and HTTP mapping.
* `ValidationError`: Mandatory field missing, format invalid, or constraint violation. * **Open/Closed Principle (OCP)**: New cross-cutting behaviors (auditing, metrics, tracing) and endpoints are added via decorators, interceptors, and `IEndpoint` implementations without altering existing service logic.
* `NotFoundError`: Entity ID not found in database. * **Liskov Substitution Principle (LSP)**: `[Slice]CacheService` transparently decorates and substitutes `[Slice]Service` across all consumers via `I[Slice]Service`.
* `UpdateError`: Mutation failed or produced zero affected rows. * **Interface Segregation Principle (ISP)**: Lean, dedicated service interfaces per domain slice rather than bloated monolithic service contracts.
* `CreateError`: Entity creation failed during persistence. * **Dependency Inversion Principle (DIP)**: Endpoints and services depend exclusively on abstractions (`IDbContextFactory`, `ITenantContextService`, `HashService`, service interfaces) rather than concrete implementations.
* `IdDecodeError`: HashID token corrupted, invalid salt, or tampering detected.
3. **Unified Exception-to-Result Wrapping**: 3. **DRY Principle via AOP & Functional Utilities**:
* Service methods wrap their implementation inside `Result.Try`: * **Zero Cross-Cutting Boilerplate**: Developers never write repetitive logging or auditing code in service methods. Attributes `[Log]` and `[Audit]` handle these concerns declaratively via Castle DynamicProxy.
* **Unified Error & Problem Details**: Domain error records (`ValidationError`, `NotFoundError`, `UpdateError`, `CreateError`, `IdDecodeError`) and extension methods (`ToProblems()`, `ToEndpointName()`) prevent repeated error payload formatting.
* **Shared HashID & Tenant Context**: Centralized `HashService` and `ITenantContextService` guarantee single-point security and tenant isolation enforcement.
---
### 16.3 C# 13 & .NET 10 Coding Style: Functional Results, Formatting & Syntax Rules
> [!IMPORTANT]
> **Authoritative Coding Style Rules (Never Regress or Undo This Approach)**:
> Antigravity / Gemini must strictly adhere to the established C# functional idioms, layout, and syntax conventions across all projects. When authoring, refactoring, or extending backend code:
> - **DO NOT introduce imperative `try-catch` blocks** for control flow or routine domain logic.
> - **DO NOT introduce nested `if-else` return branches**.
> - **DO NOT use braces `{}` on single-line `if` statements** — braces are reserved exclusively for blocks with more than one line of code.
> - **Prefer FluentResults over ternary operators** (`Result.FailIf(...).ToResult(...)` over `condition ? Result.Fail(...) : Result.Ok(...)`).
> - **Prefer expression-bodied members (`=>`) and switch expressions** over statement bodies and traditional switch statements.
> - **Never split method parameters one-per-line** in method or constructor definitions.
> - **Group related variable declarations and function calls** without empty blank lines between them.
> - **Never leave unused `using` namespaces** in `.cs` files.
> - **Pragmatically avoid magic strings** using constants, enums, or domain errors without obsessive over-engineering.
#### 1. Extensive Avoidance of `try-catch` Blocks
* Imperative `try-catch` blocks are strictly avoided across all application services, endpoints, and domain logic.
* Methods wrap their entire execution functionally inside `Result.Try`:
```csharp ```csharp
public async Task<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Project>>>)(async () => await Result.Try((Func<Task<Result<Project>>>)(async () =>
{ {
// Execution logic... // Execution logic without any try-catch...
})); }));
``` ```
* Catches unhandled exceptions and converts them safely into `Result.Fail` without crashing the thread or leaking raw stack traces. * Unexpected runtime exceptions are automatically caught by `Result.Try` and converted into a `Result.Fail` with exception metadata, allowing `LoggingInterceptor` to record errors without leaking raw exceptions or crashing threads.
4. **Pattern-Matching Endpoint Handlers**:
* Minimal API endpoints map `Result<T>` directly to HTTP status codes via C# switch expressions: #### 2. Minimal Branching, Guard Clauses & If-Statement Brace Discipline
* **No Braces for Single-Line `if`**: Braces `{}` are strictly omitted when the `if` body consists of a single statement/line:
```csharp ```csharp
await service.UpdateProjectStatusAsync(...) switch if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken))
return Result.Fail(NotFoundError.New);
if (decodeResult.IsFailed)
return Result.Fail<Contract>(IdDecodeError.Create().CausedBy(decodeResult.Errors));
```
* **Braces Only for Multi-Line Blocks**: Braces `{}` are used **only** when the `if` block encompasses more than one line of code:
```csharp
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetActivitiesAsync(projectIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
```
#### 3. FluentResults Monadic Chaining over Ternary Operators
* Whenever processing or transforming a response, **always choose FluentResults methods over ternary operators (`? :`)**:
```csharp
// Preferred (Fluent monadic style):
return Result.FailIf(project is null, NotFoundError.Create("Project not found"))
.ToResult(project?.Map()!);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled."));
// Avoid (Ternary branching):
// return project is null ? Result.Fail<Project>(...) : Result.Ok(project.Map());
```
#### 4. Expression-Bodied Members & Switch Expressions
* Methods, mappers, getters, and endpoint registrations strongly prefer expression bodies (`=>`):
```csharp
public static Activity Map(this Entities.Activity entity) => new()
{
Id = entity.Id,
Title = entity.Title,
// ...
};
```
* Minimal API endpoints map `Result<T>` directly to HTTP status codes via C# pattern-matching switch expressions:
```csharp
await service.UpdateProjectStatusAsync(request, context.GetUserId(), cancellationToken) switch
{ {
{ IsSuccess: true } => Results.Ok(), { IsSuccess: true } => Results.Ok(),
var r when r.HasError<IdDecodeError>() => Results.Forbid(), var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()), var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()), var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
} }
``` ```
5. **High-Concurrency DbContext Management**:
* Services inject `IDbContextFactory<ApplicationDbContext>` rather than scoped `ApplicationDbContext`. #### 5. Compact Method & Constructor Definitions
* Creates short-lived contexts using `await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);`. * Method signatures are kept compact and readable on one line or cleanly wrapped; **parameters are never split into one line per variable**:
* Avoids thread concurrency issues in parallel mediator requests and background Quartz jobs. ```csharp
6. **Query & Mutation Optimizations**: // Preferred:
* Queries use `.AsNoTracking()` to eliminate EF Core tracking overhead. public async Task<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) =>
* Targeted updates leverage `.ExecuteUpdateAsync(...)` for in-database bulk updates without loading full entities into memory.
7. **Tiered Hybrid Caching**: public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
* Implements `Microsoft.Extensions.Caching.Hybrid` alongside StackExchange Redis. ITenantContextService tenantContext) : IProjectService
* Local L1 memory cache: 30-minute expiration.
* Distributed L2 Redis cache: 2-minute expiration (`InstanceName = "PFM-API_"`). // Avoid:
// public async Task<Result> UpdateProjectStatusAsync(
// UpdateProjectStatusRequest request,
// Guid userId,
// CancellationToken cancellationToken = default)
```
#### 6. Bracket Placement (Allman Style)
* Opening braces `{` are placed on a new line for type declarations, multi-line blocks, lambda bodies in `Result.Try`, and object initializers:
```csharp
public sealed class ProjectService(...) : IProjectService
{
public async Task<Result> MethodAsync(...) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
// ...
}));
}
```
#### 7. Dense Grouping of Related Declarations & Calls
* Related variable declarations are grouped together without blank lines between them.
* Related sequential invocations are kept adjacent without extraneous empty lines:
```csharp
var name = request.Name.Humanize(LetterCasing.Title).Trim();
var description = request.Description.Humanize(LetterCasing.Sentence).Trim();
var cacheKey = HashService.ToMd5Hash($"project-{id}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
```
#### 8. Clean Namespaces & Pragmatic String Handling
* **Zero Unused Namespaces**: Every `.cs` file maintains clean, minimal `using` directives without stale or unused imports. File-scoped namespaces (`namespace PostFundManagement...;`) are standard.
* **Pragmatic Magic String Avoidance**: Avoid magic strings for business-critical values (use constants in `Constants.cs`, `EndpointTags`, domain enums, or typed domain errors), but remain pragmatic (e.g. human-readable error messages and route patterns need not be over-abstracted into single-use constants).
#### 9. Functional Programming Orientation
* While C# is multi-paradigm, the codebase leans intentionally into functional programming concepts:
* Immutability via positional records (`public sealed record UpdateProjectStatusRequest(...)`).
* Pure extension mapping functions (`entity.Map()`).
* Declarative LINQ queries and pipeline data transformations.
* Monadic error handling (`Result<T>`, `Result.Try`, `CausedBy`).
#### 10. High-Concurrency DbContext Management
* Services inject `IDbContextFactory<ApplicationDbContext>` rather than scoped `ApplicationDbContext`.
* Creates short-lived contexts using `await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);`.
* Avoids thread concurrency issues in parallel mediator requests and background Quartz jobs.
#### 11. Query & Mutation Optimizations
* Queries use `.AsNoTracking()` to eliminate EF Core tracking overhead.
* Targeted updates leverage `.ExecuteUpdateAsync(...)` for in-database bulk updates without loading full entities into memory.
#### 12. Tiered Hybrid Caching
* Implements `Microsoft.Extensions.Caching.Hybrid` alongside StackExchange Redis.
* Local L1 memory cache: 30-minute expiration.
* Distributed L2 Redis cache: 2-minute expiration (`InstanceName = "PFM-API_"`).
--- ---
### 16.3 Logging Strategy Using `LoggingInterceptor` & `[Log]` ### 16.4 Logging Strategy Using `LoggingInterceptor` & `[Log]`
The application uses Aspect-Oriented Programming (AOP) via **Castle DynamicProxy** and `Castle.Core.AsyncInterceptor` to decouple logging from core domain logic. The application uses Aspect-Oriented Programming (AOP) via **Castle DynamicProxy** and `Castle.Core.AsyncInterceptor` to decouple logging from core domain logic.
@@ -1044,7 +1166,7 @@ The application uses Aspect-Oriented Programming (AOP) via **Castle DynamicProxy
--- ---
### 16.4 Audit Strategy Using `AuditInterceptor`, `[Audit]` & `AuditService` ### 16.5 Audit Strategy Using `AuditInterceptor`, `[Audit]` & `AuditService`
The audit trail is a strict regulatory requirement (`BR-007`, `NFR-008`, `AT-12`). Rather than writing manual audit code in every handler, the platform automates audit logging via `AuditInterceptor`. The audit trail is a strict regulatory requirement (`BR-007`, `NFR-008`, `AT-12`). Rather than writing manual audit code in every handler, the platform automates audit logging via `AuditInterceptor`.
@@ -1074,7 +1196,7 @@ The audit trail is a strict regulatory requirement (`BR-007`, `NFR-008`, `AT-12`
--- ---
### 16.5 AOP Service Registration Pipeline ### 16.6 AOP Service Registration Pipeline
All application services are registered through a unified decorator-and-proxy composition in [`PostFundManagement.Application/Extensions/ServiceExtensionTools.cs`](file:///c:/Users/Admin/source/repos/PostFundManagement/PostFundManagement.Application/Extensions/ServiceExtensionTools.cs) and [`Services.cs`](file:///c:/Users/Admin/source/repos/PostFundManagement/PostFundManagement.Application/Extensions/Services.cs): All application services are registered through a unified decorator-and-proxy composition in [`PostFundManagement.Application/Extensions/ServiceExtensionTools.cs`](file:///c:/Users/Admin/source/repos/PostFundManagement/PostFundManagement.Application/Extensions/ServiceExtensionTools.cs) and [`Services.cs`](file:///c:/Users/Admin/source/repos/PostFundManagement/PostFundManagement.Application/Extensions/Services.cs):
@@ -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"
+19 -2
View File
@@ -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>
); );
}; };