Added, beneficiaries, site visits and change management

This commit is contained in:
2026-09-27 11:40:28 +02:00
parent fb27cc8c92
commit 36e5d3d7c8
16 changed files with 3132 additions and 212 deletions
@@ -59,6 +59,7 @@ import { HelpTooltip } from '../components/ui/HelpTooltip';
import { useToast } from '../components/ui/Toast';
import { ExportButton } from '../components/ui/ExportButton';
import { ExportColumn } from '../utils/exportUtils';
import { useTableSort, SortableTh } from '../utils/tableSort';
import { useAuth } from '../auth/AuthProvider';
import {
EmailStatuses,
@@ -139,7 +140,7 @@ export const SettingsPage: React.FC = () => {
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.');
success('User Sync Complete', 'Authenticated user enabled and directory synced.');
queryClient.invalidateQueries({ queryKey: ['users'] });
},
onError: (err: any) => {
@@ -320,6 +321,13 @@ export const SettingsPage: React.FC = () => {
);
});
const {
sortedData: sortedNotifications,
sortColumn: notifSortCol,
sortDirection: notifSortDir,
handleSort: handleNotifSort,
} = useTableSort(filteredNotifications, 'createdAt', 'desc');
// ==========================================
// 4. Audit Log Ledger State
// ==========================================
@@ -417,6 +425,27 @@ export const SettingsPage: React.FC = () => {
);
});
const {
sortedData: sortedAuditLogs,
sortColumn: auditSortCol,
sortDirection: auditSortDir,
handleSort: handleAuditSort,
} = useTableSort(filteredAuditLogs, 'timestamp', 'desc');
const {
sortedData: sortedUsers,
sortColumn: userSortCol,
sortDirection: userSortDir,
handleSort: handleUserSort,
} = useTableSort(usersList, 'email', 'asc');
const {
sortedData: sortedInvites,
sortColumn: inviteSortCol,
sortDirection: inviteSortDir,
handleSort: handleInviteSort,
} = useTableSort(invitesList, 'createdAt', 'desc');
const getActionBadge = (action?: string | null) => {
const act = (action || 'GENERAL_EVENT').toUpperCase();
if (act.includes('APPROV') || act.includes('RELEASE') || act.includes('AUTH') || act.includes('VERIF')) {
@@ -488,51 +517,16 @@ export const SettingsPage: React.FC = () => {
)}
{activeTab === 'notifications' && (
<div className="flex items-center gap-2">
<button
onClick={() => {
refetchNotifications();
success('Notifications Refreshed', 'Latest communication transmissions loaded from endpoint.');
}}
disabled={isFetchingNotifications}
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-slate-800 hover:bg-slate-700 border border-slate-700/80 rounded-xl shadow-md transition cursor-pointer disabled:opacity-50"
title="Force refresh notifications from endpoint"
>
<RefreshCw className={`w-3.5 h-3.5 text-blue-400 ${isFetchingNotifications ? 'animate-spin' : ''}`} />
<span>Refresh Notifications</span>
</button>
<ExportButton
title="Communications & Notifications Ledger"
subtitle="Transmission telemetry report via /v1/api/communications/norifications"
filename={`pfm-notifications-report-${new Date().toISOString().split('T')[0]}`}
columns={notificationExportColumns}
data={filteredNotifications}
/>
</div>
)}
{activeTab === 'audit' && (
<div className="flex items-center gap-2">
<button
onClick={() => {
refetchAuditLogs();
success('Audit Ledger Refreshed', 'Latest audit log entries loaded from backend.');
}}
disabled={isFetchingAuditLogs}
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-slate-800 hover:bg-slate-700 border border-slate-700/80 rounded-xl shadow-md transition cursor-pointer disabled:opacity-50"
title="Force refresh audit logs from API"
>
<RefreshCw className={`w-3.5 h-3.5 text-blue-400 ${isFetchingAuditLogs ? 'animate-spin' : ''}`} />
<span>Refresh Audit Logs</span>
</button>
<ExportButton
title="System Audit & Security Log"
subtitle="Complete audit trail of user access, statutory approvals, and financial transactions"
filename={`pfm-audit-trail-${new Date().toISOString().split('T')[0]}`}
columns={auditExportColumns}
data={filteredAuditLogs}
/>
</div>
<button
onClick={() => {
setInviteOrgId(effectiveNotificationOrgId);
setIsSendInviteModalOpen(true);
}}
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition cursor-pointer"
>
<Plus className="w-4 h-4" />
Add Notification
</button>
)}
</div>
</div>
@@ -659,10 +653,18 @@ export const SettingsPage: React.FC = () => {
<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">User Email</th>
<th className="py-3.5 px-4">User Identifier</th>
<th className="py-3.5 px-4">Status</th>
<th className="py-3.5 px-4">Last Login</th>
<SortableTh column="email" currentColumn={userSortCol} direction={userSortDir} onSort={handleUserSort} className="py-3.5 px-5">
User Email
</SortableTh>
<SortableTh column="id" currentColumn={userSortCol} direction={userSortDir} onSort={handleUserSort} className="py-3.5 px-4">
User Identifier
</SortableTh>
<SortableTh column="status" currentColumn={userSortCol} direction={userSortDir} onSort={handleUserSort} className="py-3.5 px-4">
Status
</SortableTh>
<SortableTh column="lastLoginAt" currentColumn={userSortCol} direction={userSortDir} onSort={handleUserSort} className="py-3.5 px-4">
Last Login
</SortableTh>
<th className="py-3.5 px-5 text-right">Actions</th>
</tr>
</thead>
@@ -673,14 +675,14 @@ export const SettingsPage: React.FC = () => {
Loading user directory...
</td>
</tr>
) : usersList.length === 0 ? (
) : sortedUsers.length === 0 ? (
<tr>
<td colSpan={5} className="py-8 text-center text-slate-500">
No users found matching current filters.
</td>
</tr>
) : (
usersList.map((u) => {
sortedUsers.map((u) => {
const badge = getActivityStatusBadge(u.status);
return (
<tr key={u.id} className="hover:bg-slate-800/40 transition-colors">
@@ -831,11 +833,21 @@ export const SettingsPage: React.FC = () => {
<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">Recipient (UUID)</th>
<th className="py-3.5 px-4">Channel</th>
<th className="py-3.5 px-4">Message</th>
<th className="py-3.5 px-4">Target Org / Grant</th>
<th className="py-3.5 px-5 text-right">Created Date</th>
<SortableTh column="recipient" currentColumn={inviteSortCol} direction={inviteSortDir} onSort={handleInviteSort} className="py-3.5 px-5">
Recipient (UUID)
</SortableTh>
<SortableTh column="platform" currentColumn={inviteSortCol} direction={inviteSortDir} onSort={handleInviteSort} className="py-3.5 px-4">
Channel
</SortableTh>
<SortableTh column="message" currentColumn={inviteSortCol} direction={inviteSortDir} onSort={handleInviteSort} className="py-3.5 px-4">
Message
</SortableTh>
<SortableTh column="invitedOrganisationId" currentColumn={inviteSortCol} direction={inviteSortDir} onSort={handleInviteSort} className="py-3.5 px-4">
Target Org / Grant
</SortableTh>
<SortableTh column="createdAt" currentColumn={inviteSortCol} direction={inviteSortDir} onSort={handleInviteSort} align="right" className="py-3.5 px-5 text-right">
Created Date
</SortableTh>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
@@ -845,14 +857,14 @@ export const SettingsPage: React.FC = () => {
Loading invitations ledger...
</td>
</tr>
) : invitesList.length === 0 ? (
) : sortedInvites.length === 0 ? (
<tr>
<td colSpan={5} className="py-8 text-center text-slate-500">
No invitations recorded yet. Use &ldquo;Dispatch Invitation&rdquo; to send one.
</td>
</tr>
) : (
invitesList.map((inv, idx) => (
sortedInvites.map((inv, idx) => (
<tr key={idx} className="hover:bg-slate-800/40 transition-colors">
<td className="py-3.5 px-5 font-mono text-slate-300 text-[11px]">
{inv.recipient}
@@ -919,7 +931,7 @@ export const SettingsPage: React.FC = () => {
Total Notifications
</span>
<div className="text-xl font-bold font-mono text-white">{fetchedNotifications.length}</div>
<span className="text-[10px] text-slate-500">Returned by endpoint</span>
<span className="text-[10px] text-slate-500">System records</span>
</div>
<div className="bg-[#0f1523]/80 backdrop-blur-md p-4 rounded-2xl border border-slate-800/80 shadow-xl space-y-1">
@@ -966,19 +978,19 @@ export const SettingsPage: React.FC = () => {
</div>
{/* Filter & Search Toolbar */}
<div className="bg-[#0f1523]/80 backdrop-blur-md p-4 rounded-2xl border border-slate-800/80 shadow-xl flex flex-col lg:flex-row items-stretch lg:items-center justify-between gap-3">
<div className="relative flex-1 max-w-md">
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-3" />
<input
type="text"
value={notificationSearchTerm}
onChange={(e) => setNotificationSearchTerm(e.target.value)}
placeholder="Search notifications by subject, message, recipient, destination..."
className="w-full pl-9 pr-3 py-2 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div className="bg-[#0d121f]/90 p-4 rounded-2xl border border-slate-800/80 backdrop-blur-sm shadow-xl flex flex-col lg:flex-row lg:items-center justify-between gap-3 relative z-20">
<div className="flex flex-col sm:flex-row sm:items-center gap-3 flex-1 flex-wrap">
<div className="relative flex-1 min-w-[240px] max-w-md">
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-3" />
<input
type="text"
value={notificationSearchTerm}
onChange={(e) => setNotificationSearchTerm(e.target.value)}
placeholder="Search notifications by subject, message, recipient, destination..."
className="w-full pl-9 pr-3 py-2 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1.5">
<span className="text-xs font-semibold text-slate-400 whitespace-nowrap">Target Org:</span>
<Building2 className="w-3.5 h-3.5 text-blue-400" />
@@ -1045,7 +1057,9 @@ export const SettingsPage: React.FC = () => {
Reset
</button>
)}
</div>
<div className="flex items-center gap-2.5 self-end sm:self-center shrink-0 flex-wrap">
<button
type="button"
onClick={() => {
@@ -1080,13 +1094,13 @@ export const SettingsPage: React.FC = () => {
</h3>
<HelpTooltip
title="Communications Telemetry"
content="Real-time notification reports returned via /v1/api/communications/norifications/{organisationId}. Displays live delivery status across Email, SMS, WhatsApp, Webhooks, and In-App channels."
content="Real-time notification reports. Displays live delivery status across Email, SMS, WhatsApp, Webhooks, and In-App channels."
size="sm"
placement="right"
/>
</div>
<span className="text-[11px] text-slate-400 font-mono">
GET /v1/api/communications/norifications/{effectiveNotificationOrgId}
<span className="text-xs text-slate-400 font-mono">
Org #{effectiveNotificationOrgId}
</span>
</div>
@@ -1094,14 +1108,28 @@ export const SettingsPage: React.FC = () => {
<table className="w-full text-left border-collapse text-xs">
<thead>
<tr className="border-b border-slate-800 bg-slate-900/40 text-slate-400 font-medium">
<th className="py-3.5 px-4">Notification ID</th>
<th className="py-3.5 px-4">Channel / Platform</th>
<th className="py-3.5 px-4">Status</th>
<th className="py-3.5 px-4">Subject &amp; Message Payload</th>
<th className="py-3.5 px-4">Recipient &amp; Destination</th>
<th className="py-3.5 px-4">Target Org</th>
<th className="py-3.5 px-4">Timestamp</th>
<th className="py-3.5 px-4 text-right">Actions</th>
<SortableTh column="id" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Notification ID
</SortableTh>
<SortableTh column="platform" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Channel / Platform
</SortableTh>
<SortableTh column="status" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Status
</SortableTh>
<SortableTh column="subject" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Subject &amp; Message Payload
</SortableTh>
<SortableTh column="recipient" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Recipient &amp; Destination
</SortableTh>
<SortableTh column="organisationId" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Target Org
</SortableTh>
<SortableTh column="createdAt" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Timestamp
</SortableTh>
<th className="py-3.5 px-4 text-right font-medium">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
@@ -1109,10 +1137,10 @@ export const SettingsPage: React.FC = () => {
<tr>
<td colSpan={8} className="py-12 text-center text-slate-500">
<RefreshCw className="w-6 h-6 animate-spin mx-auto mb-2 text-blue-400" />
Querying communication microservice endpoints for Organisation #{effectiveNotificationOrgId}...
Loading communication records for Organisation #{effectiveNotificationOrgId}...
</td>
</tr>
) : filteredNotifications.length === 0 ? (
) : sortedNotifications.length === 0 ? (
<tr>
<td colSpan={8} className="py-12 text-center text-slate-500">
<Bell className="w-8 h-8 text-slate-600 mx-auto mb-2" />
@@ -1120,7 +1148,7 @@ export const SettingsPage: React.FC = () => {
</td>
</tr>
) : (
filteredNotifications.map((item) => {
sortedNotifications.map((item) => {
const statusBadge = getNotificationStatusBadge(item.status);
return (
<tr key={item.id} className="hover:bg-slate-800/40 transition-colors">
@@ -1332,13 +1360,13 @@ export const SettingsPage: React.FC = () => {
</h3>
<HelpTooltip
title="Audit Trail"
content="Authoritative record of state changes retrieved via POST /v1/api/governance/auditlog."
content="Authoritative record of state changes and system activities."
size="sm"
placement="right"
/>
</div>
<span className="text-[11px] text-slate-400 font-mono">
POST /v1/api/governance/auditlog
<span className="text-xs text-slate-400 font-mono">
{filteredAuditLogs.length} entries
</span>
</div>
@@ -1347,13 +1375,25 @@ export const SettingsPage: React.FC = () => {
<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-3 w-10 text-center"></th>
<th className="py-3.5 px-4">Timestamp</th>
<th className="py-3.5 px-4">Event ID</th>
<th className="py-3.5 px-4">Action</th>
<th className="py-3.5 px-4">Entity</th>
<th className="py-3.5 px-4">Performed By</th>
<th className="py-3.5 px-4">Changes</th>
<th className="py-3.5 px-5 text-right">Inspect</th>
<SortableTh column="timestamp" currentColumn={auditSortCol} direction={auditSortDir} onSort={handleAuditSort} className="py-3.5 px-4">
Timestamp
</SortableTh>
<SortableTh column="id" currentColumn={auditSortCol} direction={auditSortDir} onSort={handleAuditSort} className="py-3.5 px-4">
Event ID
</SortableTh>
<SortableTh column="action" currentColumn={auditSortCol} direction={auditSortDir} onSort={handleAuditSort} className="py-3.5 px-4">
Action
</SortableTh>
<SortableTh column="entityName" currentColumn={auditSortCol} direction={auditSortDir} onSort={handleAuditSort} className="py-3.5 px-4">
Entity
</SortableTh>
<SortableTh column="performedBy" currentColumn={auditSortCol} direction={auditSortDir} onSort={handleAuditSort} className="py-3.5 px-4">
Performed By
</SortableTh>
<SortableTh column="changes" currentColumn={auditSortCol} direction={auditSortDir} onSort={handleAuditSort} className="py-3.5 px-4">
Changes
</SortableTh>
<th className="py-3.5 px-5 text-right font-medium">Inspect</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
@@ -1361,20 +1401,20 @@ export const SettingsPage: React.FC = () => {
<tr>
<td colSpan={8} className="py-12 text-center text-slate-500">
<RefreshCw className="w-6 h-6 animate-spin mx-auto mb-2 text-blue-400" />
Querying governance audit log endpoints...
Loading governance audit records...
</td>
</tr>
) : filteredAuditLogs.length === 0 ? (
) : sortedAuditLogs.length === 0 ? (
<tr>
<td colSpan={8} className="py-12 text-center text-slate-500">
<History className="w-8 h-8 text-slate-600 mx-auto mb-2" />
{auditSearchTerm || auditEntityFilter !== 'all' || auditActionFilter !== 'all'
? 'No audit log entries match your filter criteria.'
: 'No audit records returned from endpoint.'}
: 'No audit records found.'}
</td>
</tr>
) : (
filteredAuditLogs.map((log) => {
sortedAuditLogs.map((log) => {
const badge = getActionBadge(log.action);
const isExpanded = !!expandedAuditLogIds[log.id];
const changesCount = Array.isArray(log.changes) ? log.changes.length : 0;
@@ -1574,7 +1614,7 @@ export const SettingsPage: React.FC = () => {
setCopiedAuditPayload(false);
}}
title={`Audit Event: #${selectedAuditLog?.id || ''}`}
subtitle="Authoritative event payload from /v1/api/governance/auditlog"
subtitle="Authoritative event payload and activity ledger"
>
{selectedAuditLog && (
<div className="space-y-4 text-xs">
@@ -2111,7 +2151,7 @@ export const SettingsPage: React.FC = () => {
{/* Read-only Engine Notice */}
<div className="p-2.5 bg-blue-500/5 border border-blue-500/20 rounded-xl text-[11px] text-slate-400 leading-relaxed">
<span className="font-semibold text-blue-400">Communication System Report: </span>
Data returned via <span className="font-mono text-slate-300">GET /v1/api/communications/norifications/{selectedNotificationForDetail.organisationId}</span>. Notification status transitions and retry schedules are managed autonomously by gateway microservices.
Notification status transitions and retry schedules are managed autonomously by communication gateway services.
</div>
</div>