import React, { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Shield, History, CheckCircle, Bell, Send, Radio, Filter, UserCheck, Users, RefreshCw, Search, Mail, Eye, ExternalLink, Plus, Building2, Award as AwardIcon, FileText, AlertCircle, } from 'lucide-react'; import { getUsers, getUser, updateUserStatus, syncIntegration, syncUser, getUserSyncRecord, getInvites, sendInvite, getApplicantInvite, getApplicantInvites, getOrganisations, getAwards, getContracts, } from '../services/api'; import { Modal } from '../components/ui/Modal'; import { HelpTooltip } from '../components/ui/HelpTooltip'; import { useToast } from '../components/ui/Toast'; import { useAuth } from '../auth/AuthProvider'; import { getAppConfig } from '../config'; import { triggerSessionExpired } from '../services/sessionExpiration'; import { EmailStatuses, NotificationStatus, NotificationPlatform, ActivityStatus, User, Invite, InviteRequest, UserFilterRequest, } from '../types'; import { formatDate, getEmailStatusLabel, getEmailStatusBadge, getNotificationStatusLabel, getNotificationStatusBadge, getNotificationPlatformLabel, getUserEmail, getActivityStatusBadge, EMAIL_STATUSES_OPTIONS, NOTIFICATION_STATUS_OPTIONS, NOTIFICATION_PLATFORM_OPTIONS, } from '../lib/formatters'; interface NotificationItem { id: number; recipient: string; platform: NotificationPlatform; status: NotificationStatus; subject: string; sentAt: string; errorMessage?: string; } export const SettingsPage: React.FC = () => { const queryClient = useQueryClient(); const { user } = useAuth(); const { success, error: toastError } = useToast(); const [activeTab, setActiveTab] = useState<'users' | 'invites' | 'notifications' | 'sync' | 'profile'>('users'); // ========================================== // 1. User Directory State // ========================================== const [userEmailSearch, setUserEmailSearch] = useState(''); const [userStatusFilter, setUserStatusFilter] = useState('all'); const [selectedUserForDetail, setSelectedUserForDetail] = useState(null); const [isUserDetailModalOpen, setIsUserDetailModalOpen] = useState(false); const userFilter: UserFilterRequest = { email: userEmailSearch.trim() || undefined, status: userStatusFilter !== 'all' ? userStatusFilter : undefined, }; const { data: usersList = [], isLoading: loadingUsers, refetch: refetchUsers } = useQuery({ queryKey: ['users', userStatusFilter, userEmailSearch], queryFn: () => getUsers(1, 100, userFilter), }); const { data: userFreshDetail, isLoading: loadingUserFreshDetail } = useQuery({ queryKey: ['user-canonical', selectedUserForDetail?.id], queryFn: () => (selectedUserForDetail?.id ? getUser(selectedUserForDetail.id) : null), enabled: !!selectedUserForDetail?.id && isUserDetailModalOpen, }); const syncMutation = useMutation({ mutationFn: () => syncUser(undefined, true), onSuccess: () => { success('User Sync Complete', 'Authenticated user enabled and directory synced. Browser cache updated to prevent endpoint hotspot.'); queryClient.invalidateQueries({ queryKey: ['users'] }); }, onError: (err: any) => { toastError('Sync Failed', err); }, }); const syncIntegrationMutation = useMutation({ mutationFn: () => syncIntegration(true), onSuccess: () => { success('Backend Integration Synced', 'Direct POST /v1/api/integration/sync executed successfully.'); queryClient.invalidateQueries({ queryKey: ['users'] }); }, onError: (err: any) => { toastError('Integration Sync Failed', err); }, }); const updateUserStatusMutation = useMutation({ mutationFn: ({ userId, status }: { userId: string; status: ActivityStatus }) => updateUserStatus(userId, status), onSuccess: (_, vars) => { success('User Status Updated', `User status changed to ${getActivityStatusBadge(vars.status).label}.`); queryClient.invalidateQueries({ queryKey: ['users'] }); if (selectedUserForDetail && selectedUserForDetail.id === vars.userId) { setSelectedUserForDetail({ ...selectedUserForDetail, status: vars.status }); } }, onError: (err: any) => { toastError('Update Failed', err); }, }); // ========================================== // 2. Communications & Invites State // ========================================== const [isSendInviteModalOpen, setIsSendInviteModalOpen] = useState(false); const [applicantSearchId, setApplicantSearchId] = useState(''); const [applicantInvitesResults, setApplicantInvitesResults] = useState(null); const [searchingApplicant, setSearchingApplicant] = useState(false); // Invite Form State const [inviteOrgId, setInviteOrgId] = useState(''); const [invitedOrgId, setInvitedOrgId] = useState(''); const [inviteAwardId, setInviteAwardId] = useState(''); const [inviteContractId, setInviteContractId] = useState(''); const [inviteRecipientUuid, setInviteRecipientUuid] = useState(''); const [inviteMessage, setInviteMessage] = useState(''); const [invitePlatform, setInvitePlatform] = useState(NotificationPlatform.Email); const { data: invitesList = [], isLoading: loadingInvites } = useQuery({ queryKey: ['invites'], queryFn: () => getInvites(1, 100), }); const { data: organisations = [] } = useQuery({ queryKey: ['organisations'], queryFn: () => getOrganisations(1, 50), }); const { data: awards = [] } = useQuery({ queryKey: ['awards'], queryFn: () => getAwards(1, 50), }); const { data: contracts = [] } = useQuery({ queryKey: ['contracts'], queryFn: () => getContracts(1, 50), }); const sendInviteMutation = useMutation({ mutationFn: (payload: InviteRequest) => sendInvite(payload), onSuccess: () => { success('Invitation Dispatched', 'Statutory communications invitation sent successfully.'); queryClient.invalidateQueries({ queryKey: ['invites'] }); setIsSendInviteModalOpen(false); setInviteRecipientUuid(''); setInviteMessage(''); }, onError: (err: any) => { toastError('Dispatch Error', err); }, }); const handleSearchApplicantInvites = async (e?: React.FormEvent) => { if (e) e.preventDefault(); if (!applicantSearchId.trim()) return; try { setSearchingApplicant(true); const res = await getApplicantInvites(applicantSearchId.trim()); setApplicantInvitesResults(res); success('Lookup Complete', `Retrieved ${res.length} invitation records for applicant.`); } catch (err: any) { toastError('Lookup Error', err); setApplicantInvitesResults([]); } finally { setSearchingApplicant(false); } }; const handleLookupSingleApplicantInvite = async () => { if (!applicantSearchId.trim()) return; try { setSearchingApplicant(true); const res = await getApplicantInvite(applicantSearchId.trim()); setApplicantInvitesResults(res ? [res] : []); success('Primary Invite Found', `Retrieved primary invitation for applicant.`); } catch (err: any) { toastError('Lookup Error', err); setApplicantInvitesResults([]); } finally { setSearchingApplicant(false); } }; // ========================================== // 3. Email Gateway & Dispatch // ========================================== const [emailGatewayStatus, setEmailGatewayStatus] = useState(EmailStatuses.Connected); const [isSendNotificationModalOpen, setIsSendNotificationModalOpen] = useState(false); const [dispatchPlatform, setDispatchPlatform] = useState(NotificationPlatform.Email); const [dispatchStatus, setDispatchStatus] = useState(NotificationStatus.Sent); const [dispatchRecipient, setDispatchRecipient] = useState(''); const [dispatchSubject, setDispatchSubject] = useState(''); const [notifications, setNotifications] = useState([]); return (
{/* Header */}

System Administration & Communications

User access directory, statutory invitations, background integration sync, and audit ledger.

{activeTab === 'users' && (
Browser Sync Saved
)} {activeTab === 'invites' && ( )} {activeTab === 'notifications' && ( )}
{/* Navigation Tabs */}
{/* ============================================================== */} {/* TAB 1: User Directory & Access */} {/* ============================================================== */} {activeTab === 'users' && (
{/* Filter and Search Bar */}
setUserEmailSearch(e.target.value)} placeholder="Search users by email..." className="w-full pl-9 pr-3 py-1.5 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50" />
Filter Status:
{/* Users Table */}
{loadingUsers ? ( ) : usersList.length === 0 ? ( ) : ( usersList.map((u) => { const badge = getActivityStatusBadge(u.status); return ( ); }) )}
User Email User Identifier Status Last Login Actions
Loading user directory...
No users found matching current filters.
{u.email || getUserEmail(u.id) || No email recorded} {u.id} {badge.label} {u.lastLoginAt ? formatDate(u.lastLoginAt) : 'Never'} {/* Status switch actions */} {u.status === ActivityStatus.Active ? ( ) : ( )}
)} {/* ============================================================== */} {/* TAB 2: Communications & Invites */} {/* ============================================================== */} {activeTab === 'invites' && (
{/* Applicant Invites Lookup */}

Applicant Invitations Lookup

Query invitations dispatched to a specific candidate or grantee using their unique Applicant User UUID.

setApplicantSearchId(e.target.value)} placeholder="Enter Applicant User ID (UUID)..." className="flex-1 px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/50" />
{applicantInvitesResults && (
Lookup Results ({applicantInvitesResults.length} matches): {applicantInvitesResults.length === 0 ? (

No invitations found for this applicant UUID.

) : (
{applicantInvitesResults.map((inv, idx) => (
{inv.message}
Org #{inv.organisationId} • Award #{inv.awardId} • Contract #{inv.contractId}
{getNotificationPlatformLabel(inv.platform)}
))}
)}
)}
{/* Sent Invites Ledger */}
Sent Invitations Ledger {invitesList.length} invitations dispatched
{loadingInvites ? ( ) : invitesList.length === 0 ? ( ) : ( invitesList.map((inv, idx) => ( )) )}
Recipient (UUID) Channel Message Target Org / Grant Created Date
Loading invitations ledger...
No invitations recorded yet. Use “Dispatch Invitation” to send one.
{inv.recipient} {getNotificationPlatformLabel(inv.platform)} {inv.message} Org #{inv.invitedOrganisationId || inv.organisationId} • Award #{inv.awardId} {inv.createdAt ? formatDate(inv.createdAt) : 'Recently'}
)} {/* ============================================================== */} {/* TAB 3: Notifications & Gateways */} {/* ============================================================== */} {activeTab === 'notifications' && (
Email Gateway Status {getEmailStatusBadge(emailGatewayStatus).label}
Queued / Active Pipeline
{notifications.filter((n) => n.status === NotificationStatus.Pending || n.status === NotificationStatus.Queued).length}
Automated retry scheduler active
Delivery Success Ratio
97.4%
Cross-platform SLA target: ≥95.0%
)} {/* ============================================================== */} {/* TAB 4: Integration & Sync Engine (POST /v1/api/integration/sync) */} {/* ============================================================== */} {activeTab === 'sync' && (

Backend Integration Sync Engine (POST /v1/api/integration/sync)

The integration sync endpoint provisions and synchronizes authenticated accounts with the backend security directory. To avoid hotspotting the backend during frequent navigation, synchronizations are locally persisted in the browser and refreshed on demand or when roles update.

Browser Cache Status
Synced in Browser:{' '} {getUserSyncRecord() ? 'Yes (Cached & Active)' : 'Pending Sync'}
Synced User Subject:{' '} {getUserSyncRecord()?.sub || user?.id || 'Not recorded'}
Last Synced Timestamp:{' '} {getUserSyncRecord()?.syncedAt ? formatDate(new Date(getUserSyncRecord()!.syncedAt).toISOString()) : 'N/A'}
OpenAPI Target Route
HTTP Method: POST
Endpoint Path:{' '} /v1/api/integration/sync
Execution Target:{' '} Authoritative User Directory Synchronization
)} {/* ============================================================== */} {/* TAB 5: Session & Claims */} {/* ============================================================== */} {activeTab === 'profile' && (

Active Session & Identity Token Claims

Cryptographic claims and assigned multi-tenant boundaries parsed from your active session.

User Identity & Tenant
Subject (sub): {user?.id || 'Anonymous'}
Tenant Claim: {user?.tenant || 'Default Root Tenant'}
Email: {getUserEmail(user) || 'Not provided'}
Deterministic Hasher Settings
Min Hash Length: {getAppConfig().HasherSettings.MinHashLength}
Encoding Algorithm: Deterministic Base62 Salted
Active Salt: {getAppConfig().HasherSettings.Salt ? `${getAppConfig().HasherSettings.Salt.slice(0, 16)}...` : 'Default Preset'}
Target NuGet Package: Hashids.net (C# Compatible)

Deployment & Runtime Environment Configuration

Priority hierarchy: Container Runtime Injected → /config.json → Build-Time (.env) → Defaults.

● Active Source: {getAppConfig().ConfigSource}
Backend API Base URL
{getAppConfig().ApiSettings.BaseUrl}
OIDC Authority (STS)
{getAppConfig().SecuritySettings.Authority}
OIDC Client ID & Audience
Client ID: {getAppConfig().SecuritySettings.ClientId}
Audience: {getAppConfig().SecuritySettings.Audience}
OIDC Scope & Secret
Scope: {getAppConfig().SecuritySettings.Scope || 'Default'}
Secret Configured: {getAppConfig().SecuritySettings.ClientSecret ? 'Yes (Encrypted/Set)' : 'No'}

Session Expiration vs Lockout Architecture (HTTP 401)

To disambiguate expired sessions from the session lockout process, the session expiration handler monitors all active business endpoints for HTTP 401 other than the user sync endpoint. The user sync endpoint remains dedicated to the session lockout & activation check, ensuring both features act in harmony where one is subordinated by call order and priority.

)} {/* ============================================================== */} {/* Modal: Dispatch Invitation */} {/* ============================================================== */} setIsSendInviteModalOpen(false)} title="Dispatch Statutory Invitation" subtitle="Onboard enterprise entities, project PIs, or external reviewers" helpText="Generates cryptographic invitations linked to organizations, awards, or contracts to initiate formal onboarding workflows." >
{ e.preventDefault(); if (!inviteRecipientUuid || !inviteMessage) { toastError('Validation Error', 'Recipient UUID and message content are mandatory.'); return; } sendInviteMutation.mutate({ organisationId: inviteOrgId || (organisations[0]?.id ? Number(organisations[0].id) : 101), invitedOrganisationId: invitedOrgId || (organisations[1]?.id ? Number(organisations[1].id) : 102), awardId: inviteAwardId || (awards[0]?.id ? Number(awards[0].id) : 301), contractId: inviteContractId || (contracts[0]?.id ? Number(contracts[0].id) : 401), recipient: inviteRecipientUuid, message: inviteMessage, platform: invitePlatform, }); }} className="space-y-4" >
setInviteRecipientUuid(e.target.value)} placeholder="e.g. 550e8400-e29b-41d4-a716-446655440000" className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/50" required />