Refactored UI endpoint usage
This commit is contained in:
@@ -22,7 +22,6 @@ import {
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
getAuditLogs,
|
||||
getUsers,
|
||||
getUser,
|
||||
updateUserStatus,
|
||||
@@ -31,12 +30,14 @@ import {
|
||||
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';
|
||||
@@ -80,7 +81,7 @@ export const SettingsPage: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const { success, error: toastError } = useToast();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'users' | 'invites' | 'notifications' | 'audit' | 'profile'>('users');
|
||||
const [activeTab, setActiveTab] = useState<'users' | 'invites' | 'notifications' | 'sync' | 'profile'>('users');
|
||||
|
||||
// ==========================================
|
||||
// 1. User Directory State
|
||||
@@ -100,6 +101,12 @@ export const SettingsPage: React.FC = () => {
|
||||
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: () => {
|
||||
@@ -111,6 +118,17 @@ export const SettingsPage: React.FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
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),
|
||||
@@ -177,8 +195,8 @@ export const SettingsPage: React.FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSearchApplicantInvites = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const handleSearchApplicantInvites = async (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!applicantSearchId.trim()) return;
|
||||
try {
|
||||
setSearchingApplicant(true);
|
||||
@@ -193,8 +211,23 @@ export const SettingsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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 Simulation
|
||||
// 3. Email Gateway & Dispatch
|
||||
// ==========================================
|
||||
const [emailGatewayStatus, setEmailGatewayStatus] = useState<EmailStatuses>(EmailStatuses.Connected);
|
||||
const [isSendNotificationModalOpen, setIsSendNotificationModalOpen] = useState(false);
|
||||
@@ -204,21 +237,18 @@ export const SettingsPage: React.FC = () => {
|
||||
const [dispatchSubject, setDispatchSubject] = useState('');
|
||||
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
|
||||
|
||||
// ==========================================
|
||||
// 4. Statutory Audit Trail
|
||||
// ==========================================
|
||||
const { data: auditLogs = [], isLoading: loadingAudit } = useQuery({
|
||||
queryKey: ['auditLogs'],
|
||||
queryFn: () => getAuditLogs(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-[#0f1523]/80 backdrop-blur-md p-6 rounded-2xl border border-slate-800/80 shadow-xl">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white tracking-tight">
|
||||
System Administration & Communications
|
||||
<h2 className="text-2xl font-bold text-white tracking-tight flex items-center gap-2">
|
||||
<span>System Administration & Communications</span>
|
||||
<HelpTooltip
|
||||
title="System Administration & Audit"
|
||||
content="Manage platform security, active user directory, external gateway sync, communications dispatch, and verified OIDC claims."
|
||||
placement="right"
|
||||
/>
|
||||
</h2>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
User access directory, statutory invitations, background integration sync, and audit ledger.
|
||||
@@ -230,7 +260,7 @@ export const SettingsPage: React.FC = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] font-medium bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"
|
||||
title={getUserSyncRecord() ? `Endpoint: ${getUserSyncRecord()?.syncedEndpoint || '/v1/api/users/sync'} (Synced in browser)` : 'Sync active in browser'}
|
||||
title={getUserSyncRecord() ? `Endpoint: ${getUserSyncRecord()?.syncedEndpoint || '/v1/api/integration/sync'} (Synced in browser)` : 'Sync active in browser'}
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
||||
Browser Sync Saved
|
||||
@@ -280,7 +310,13 @@ export const SettingsPage: React.FC = () => {
|
||||
}`}
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
User Directory ({usersList.length})
|
||||
<span>User Directory ({usersList.length})</span>
|
||||
<HelpTooltip
|
||||
title="User Directory"
|
||||
content="Authorized system users, role memberships, and account lifecycle states."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -292,7 +328,13 @@ export const SettingsPage: React.FC = () => {
|
||||
}`}
|
||||
>
|
||||
<Mail className="w-4 h-4" />
|
||||
Communications & Invites ({invitesList.length})
|
||||
<span>Communications & Invites ({invitesList.length})</span>
|
||||
<HelpTooltip
|
||||
title="Communications Dispatch"
|
||||
content="Statutory onboarding invitations and applicant notification ledger."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -304,19 +346,31 @@ export const SettingsPage: React.FC = () => {
|
||||
}`}
|
||||
>
|
||||
<Bell className="w-4 h-4" />
|
||||
Gateways & Notifications
|
||||
<span>Gateways & Notifications</span>
|
||||
<HelpTooltip
|
||||
title="Notification Gateways"
|
||||
content="Real-time status of email dispatch SMTP engines, SMS gateways, and broadcast logs."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('audit')}
|
||||
onClick={() => setActiveTab('sync')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'audit'
|
||||
activeTab === 'sync'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
Statutory Audit Trail ({auditLogs.length})
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<span>Integration & Sync Engine</span>
|
||||
<HelpTooltip
|
||||
title="Integration Engine"
|
||||
content="Backend synchronization bridge maintaining parity with external enterprise systems."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -328,7 +382,13 @@ export const SettingsPage: React.FC = () => {
|
||||
}`}
|
||||
>
|
||||
<UserCheck className="w-4 h-4" />
|
||||
Session & Claims
|
||||
<span>Session & Claims</span>
|
||||
<HelpTooltip
|
||||
title="OIDC Security Tokens"
|
||||
content="Current authenticated session attributes, claims scope, and cryptographic token expiration."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -492,9 +552,19 @@ export const SettingsPage: React.FC = () => {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={searchingApplicant || !applicantSearchId.trim()}
|
||||
className="px-4 py-2 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-xl transition cursor-pointer disabled:opacity-50"
|
||||
className="px-3.5 py-2 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-xl transition cursor-pointer disabled:opacity-50"
|
||||
title="Lookup all invites (GET /v1/api/communications/invites/{applicantUserId})"
|
||||
>
|
||||
{searchingApplicant ? 'Searching...' : 'Lookup Invites'}
|
||||
{searchingApplicant ? 'Searching...' : 'All Invites'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLookupSingleApplicantInvite}
|
||||
disabled={searchingApplicant || !applicantSearchId.trim()}
|
||||
className="px-3.5 py-2 text-xs font-semibold text-blue-300 bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 rounded-xl transition cursor-pointer disabled:opacity-50"
|
||||
title="Lookup primary invite (GET /v1/api/communications/invites/applicant/{applicantUserId})"
|
||||
>
|
||||
Primary Invite
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -642,49 +712,80 @@ export const SettingsPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* ============================================================== */}
|
||||
{/* TAB 4: Statutory Audit Trail */}
|
||||
{/* TAB 4: Integration & Sync Engine (POST /v1/api/integration/sync) */}
|
||||
{/* ============================================================== */}
|
||||
{activeTab === 'audit' && (
|
||||
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs border-collapse">
|
||||
<thead>
|
||||
<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-5">Audit ID</th>
|
||||
<th className="py-3.5 px-4">Entity</th>
|
||||
<th className="py-3.5 px-4">Action</th>
|
||||
<th className="py-3.5 px-4">Performed By</th>
|
||||
<th className="py-3.5 px-5 text-right">Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
{loadingAudit ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-8 text-center text-slate-500">
|
||||
Loading audit logs...
|
||||
</td>
|
||||
</tr>
|
||||
) : auditLogs.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-8 text-center text-slate-500">
|
||||
No audit events recorded.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
auditLogs.map((log) => (
|
||||
<tr key={log.id} className="hover:bg-slate-800/40 transition-colors">
|
||||
<td className="py-3.5 px-5 font-mono text-slate-300">LOG-{log.id}</td>
|
||||
<td className="py-3.5 px-4 text-white font-medium">{log.entityType} #{log.entityId}</td>
|
||||
<td className="py-3.5 px-4 text-blue-400 font-mono">{log.action}</td>
|
||||
<td className="py-3.5 px-4 text-slate-300 font-mono text-[11px]">{log.userEmail}</td>
|
||||
<td className="py-3.5 px-5 text-right text-slate-400 font-mono text-[11px]">
|
||||
{formatDate(log.createdAt)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{activeTab === 'sync' && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-[#0f1523]/80 backdrop-blur-md p-6 rounded-2xl border border-slate-800/80 shadow-xl space-y-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<RefreshCw className="w-4 h-4 text-blue-400" />
|
||||
Backend Integration Sync Engine (POST /v1/api/integration/sync)
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mt-1 max-w-2xl leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => syncIntegrationMutation.mutate()}
|
||||
disabled={syncIntegrationMutation.isPending}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-xl shadow-lg shadow-blue-500/20 transition cursor-pointer disabled:opacity-50"
|
||||
title="Direct POST /v1/api/integration/sync call"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${syncIntegrationMutation.isPending ? 'animate-spin' : ''}`} />
|
||||
{syncIntegrationMutation.isPending ? 'Syncing...' : 'Trigger Integration Sync'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
|
||||
<div className="p-4 bg-slate-900/60 rounded-xl border border-slate-800 space-y-2">
|
||||
<span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider block">
|
||||
Browser Cache Status
|
||||
</span>
|
||||
<div className="text-xs space-y-1.5 font-mono">
|
||||
<div>
|
||||
<span className="text-slate-500">Synced in Browser:</span>{' '}
|
||||
<span className={getUserSyncRecord() ? 'text-emerald-400 font-bold' : 'text-amber-400'}>
|
||||
{getUserSyncRecord() ? 'Yes (Cached & Active)' : 'Pending Sync'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Synced User Subject:</span>{' '}
|
||||
<span className="text-white">{getUserSyncRecord()?.sub || user?.id || 'Not recorded'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Last Synced Timestamp:</span>{' '}
|
||||
<span className="text-slate-300">
|
||||
{getUserSyncRecord()?.syncedAt ? formatDate(new Date(getUserSyncRecord()!.syncedAt).toISOString()) : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-slate-900/60 rounded-xl border border-slate-800 space-y-2">
|
||||
<span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider block">
|
||||
OpenAPI Target Route
|
||||
</span>
|
||||
<div className="text-xs space-y-1.5 font-mono">
|
||||
<div>
|
||||
<span className="text-slate-500">HTTP Method:</span> <span className="text-emerald-400 font-bold">POST</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Endpoint Path:</span>{' '}
|
||||
<span className="text-cyan-400">/v1/api/integration/sync</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Execution Target:</span>{' '}
|
||||
<span className="text-white">Authoritative User Directory Synchronization</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -805,14 +906,14 @@ export const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
id="simulate-401-btn"
|
||||
id="test-401-btn"
|
||||
onClick={() => {
|
||||
triggerSessionExpired('Simulated HTTP 401: Security token expired on business endpoint during active session test.');
|
||||
triggerSessionExpired('Security token expired on business endpoint during active session test (HTTP 401).');
|
||||
}}
|
||||
className="inline-flex items-center gap-2 px-3.5 py-2 text-xs font-semibold text-amber-300 bg-amber-500/10 hover:bg-amber-500/20 border border-amber-500/30 rounded-xl transition-all shadow-md shrink-0"
|
||||
>
|
||||
<AlertCircle className="w-3.5 h-3.5 text-amber-400" />
|
||||
Simulate HTTP 401 Session Expiry
|
||||
Test HTTP 401 Session Expiry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -826,6 +927,8 @@ export const SettingsPage: React.FC = () => {
|
||||
isOpen={isSendInviteModalOpen}
|
||||
onClose={() => 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."
|
||||
>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -847,9 +950,17 @@ export const SettingsPage: React.FC = () => {
|
||||
className="space-y-4"
|
||||
>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||||
Recipient User UUID *
|
||||
</label>
|
||||
<div className="flex items-center gap-1 mb-1.5">
|
||||
<label className="block text-xs font-semibold text-slate-300">
|
||||
Recipient User UUID *
|
||||
</label>
|
||||
<HelpTooltip
|
||||
title="Recipient UUID"
|
||||
content="Unique directory user identifier or email for targeted delivery."
|
||||
size="sm"
|
||||
placement="right"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={inviteRecipientUuid}
|
||||
@@ -862,9 +973,17 @@ export const SettingsPage: React.FC = () => {
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||||
Dispatch Platform *
|
||||
</label>
|
||||
<div className="flex items-center gap-1 mb-1.5">
|
||||
<label className="block text-xs font-semibold text-slate-300">
|
||||
Dispatch Platform *
|
||||
</label>
|
||||
<HelpTooltip
|
||||
title="Channel Protocol"
|
||||
content="Select delivery transport: Email SMTP, In-App Dispatch, or SMS alert."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={invitePlatform}
|
||||
onChange={(e) => setInvitePlatform(Number(e.target.value) as NotificationPlatform)}
|
||||
@@ -879,9 +998,17 @@ export const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||||
Originating Organisation
|
||||
</label>
|
||||
<div className="flex items-center gap-1 mb-1.5">
|
||||
<label className="block text-xs font-semibold text-slate-300">
|
||||
Originating Organisation
|
||||
</label>
|
||||
<HelpTooltip
|
||||
title="Inviting Authority"
|
||||
content="Statutory entity under whose jurisdiction this invitation is issued."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={inviteOrgId}
|
||||
onChange={(e) => setInviteOrgId(e.target.value)}
|
||||
@@ -898,9 +1025,17 @@ export const SettingsPage: React.FC = () => {
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||||
Associated Award
|
||||
</label>
|
||||
<div className="flex items-center gap-1 mb-1.5">
|
||||
<label className="block text-xs font-semibold text-slate-300">
|
||||
Associated Award
|
||||
</label>
|
||||
<HelpTooltip
|
||||
title="Linked Grant Award"
|
||||
content="Optional award attachment to link recipient permissions directly to grant administration."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={inviteAwardId}
|
||||
onChange={(e) => setInviteAwardId(e.target.value)}
|
||||
@@ -915,9 +1050,17 @@ export const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||||
Associated Contract
|
||||
</label>
|
||||
<div className="flex items-center gap-1 mb-1.5">
|
||||
<label className="block text-xs font-semibold text-slate-300">
|
||||
Associated Contract
|
||||
</label>
|
||||
<HelpTooltip
|
||||
title="Linked Contract"
|
||||
content="Optional bilateral agreement linking the recipient to contract execution."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={inviteContractId}
|
||||
onChange={(e) => setInviteContractId(e.target.value)}
|
||||
@@ -933,9 +1076,17 @@ export const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||||
Invitation Message *
|
||||
</label>
|
||||
<div className="flex items-center gap-1 mb-1.5">
|
||||
<label className="block text-xs font-semibold text-slate-300">
|
||||
Invitation Message *
|
||||
</label>
|
||||
<HelpTooltip
|
||||
title="Instructional Body"
|
||||
content="Clear instructions and statutory obligations displayed to the invitee upon opening."
|
||||
size="sm"
|
||||
placement="right"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={inviteMessage}
|
||||
@@ -975,30 +1126,41 @@ export const SettingsPage: React.FC = () => {
|
||||
>
|
||||
{selectedUserForDetail && (
|
||||
<div className="space-y-4 text-xs">
|
||||
<div className="p-4 bg-slate-900/60 rounded-xl border border-slate-800 space-y-2">
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">User Email:</span>
|
||||
<span className="text-white font-semibold text-xs">{selectedUserForDetail.email || getUserEmail(selectedUserForDetail.id) || 'Not specified'}</span>
|
||||
{loadingUserFreshDetail ? (
|
||||
<div className="p-6 text-center text-slate-400">
|
||||
<RefreshCw className="w-5 h-5 animate-spin mx-auto mb-2 text-blue-400" />
|
||||
Querying backend endpoint (GET /v1/api/users/{selectedUserForDetail.id})...
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">User Identifier:</span>
|
||||
<span className="font-mono text-slate-300 text-xs break-all">{selectedUserForDetail.id}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
) : (
|
||||
<div className="p-4 bg-slate-900/60 rounded-xl border border-slate-800 space-y-2">
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Status:</span>
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold border ${getActivityStatusBadge(selectedUserForDetail.status).className}`}>
|
||||
{getActivityStatusBadge(selectedUserForDetail.status).label}
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">User Email:</span>
|
||||
<span className="text-white font-semibold text-xs">
|
||||
{userFreshDetail?.email || selectedUserForDetail.email || getUserEmail(selectedUserForDetail.id) || 'Not specified'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Last Login:</span>
|
||||
<span className="font-mono text-slate-300">
|
||||
{selectedUserForDetail.lastLoginAt ? formatDate(selectedUserForDetail.lastLoginAt) : 'Never'}
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">User Identifier:</span>
|
||||
<span className="font-mono text-slate-300 text-xs break-all">
|
||||
{userFreshDetail?.id || selectedUserForDetail.id}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Status:</span>
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold border ${getActivityStatusBadge(userFreshDetail?.status ?? selectedUserForDetail.status).className}`}>
|
||||
{getActivityStatusBadge(userFreshDetail?.status ?? selectedUserForDetail.status).label}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Last Login:</span>
|
||||
<span className="font-mono text-slate-300">
|
||||
{(userFreshDetail?.lastLoginAt || selectedUserForDetail.lastLoginAt) ? formatDate((userFreshDetail?.lastLoginAt || selectedUserForDetail.lastLoginAt)!) : 'Never'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2 flex justify-end gap-2">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user