From fb27cc8c927ddac487d1cdee5974e4f01956aad5 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 27 Sep 2026 10:58:08 +0200 Subject: [PATCH] Removed settings page mock data and replaced with api calls --- .gitignore | 1 + .../components/governance/RulesSection.tsx | 25 +- .../monitoring/ContractsManagement.tsx | 104 ++- .../monitoring/EvidenceReviewQueue.tsx | 51 +- .../src/lib/exportUtils.ts | 4 +- .../src/pages/GovernancePage.tsx | 4 +- .../src/pages/MonitoringPage.tsx | 508 ++++++++++++--- .../src/pages/SettingsPage.tsx | 593 ++++++++++++++---- .../src/services/api.ts | 319 +--------- PostFundManagement.ConsoleUI/src/types.ts | 15 +- 10 files changed, 1067 insertions(+), 557 deletions(-) diff --git a/.gitignore b/.gitignore index 80af6e4..9d74af5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ *.env *.env.json certificates/ +package-lock.json # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/PostFundManagement.ConsoleUI/src/components/governance/RulesSection.tsx b/PostFundManagement.ConsoleUI/src/components/governance/RulesSection.tsx index 323df73..58d9501 100644 --- a/PostFundManagement.ConsoleUI/src/components/governance/RulesSection.tsx +++ b/PostFundManagement.ConsoleUI/src/components/governance/RulesSection.tsx @@ -195,7 +195,7 @@ export const RulesSection: React.FC = ({ programmeId: Number(rule.programmeId), name: rule.name || '', description: rule.description || '', - version: Number(rule.version) || 1, + version: Number(rule.version) > 0 ? Number(rule.version) : 1, status: Number(rule.status), }); setIsModalOpen(true); @@ -577,19 +577,26 @@ export const RulesSection: React.FC = ({ {/* Version & Status */}
-
- - +
+
+ + +
+ + Read-Only +
{ruleForm.formState.errors.version && (

diff --git a/PostFundManagement.ConsoleUI/src/components/monitoring/ContractsManagement.tsx b/PostFundManagement.ConsoleUI/src/components/monitoring/ContractsManagement.tsx index c18a10b..3d22fce 100644 --- a/PostFundManagement.ConsoleUI/src/components/monitoring/ContractsManagement.tsx +++ b/PostFundManagement.ConsoleUI/src/components/monitoring/ContractsManagement.tsx @@ -22,6 +22,7 @@ import { Paperclip, Check, ShieldCheck, + RotateCcw, } from 'lucide-react'; import { getContracts, @@ -39,6 +40,8 @@ import { toolsService } from '../../services/toolsService'; import { Modal } from '../ui/Modal'; import { useToast } from '../ui/Toast'; import { ProblemDetailsAlert } from '../ui/ProblemDetailsAlert'; +import { ExportButton } from '../ui/ExportButton'; +import { ExportColumn } from '../../utils/exportUtils'; import { Contract, Award, @@ -390,6 +393,19 @@ export const ContractsManagement: React.FC = ({ Number(c.status) === ContractStatus.PendingApproval ).length; + const contractExportColumns: ExportColumn[] = [ + { header: 'Contract ID', key: 'id' }, + { header: 'Description / Title', key: 'description' }, + { header: 'Project', key: 'projectId', format: (val) => (val ? `Project #${val}` : '—') }, + { header: 'Award ID', key: 'awardId', format: (val) => (val ? `Award #${val}` : '—') }, + { header: 'Organisation ID', key: 'organisationId', format: (val) => (val ? `Org #${val}` : '—') }, + { header: 'Status', key: 'status', format: (val) => getContractStatusBadge(val).label }, + { header: 'Signed Date', key: 'signedDate', format: (val) => (val ? formatDate(val) : '—') }, + { header: 'Expiry Date', key: 'expiryDate', format: (val) => (val ? formatDate(val) : '—') }, + { header: 'Budget Allocated', key: 'budgetAllocated', format: (val) => (val ? formatZAR(val) : '—') }, + { header: 'Document URI', key: 'fileUri', format: (val) => val || '—' }, + ]; + return (

{/* Summary Cards */} @@ -466,27 +482,30 @@ export const ContractsManagement: React.FC = ({
{/* Action and Filter Bar */} -
-
+
+
{/* Search */} -
- +
+ setSearchTerm(e.target.value)} - className="w-full bg-[#151b2c] border border-slate-700/80 rounded-xl pl-9 pr-3 py-1.5 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-blue-500" + className="w-full pl-9 pr-4 py-2.5 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 text-slate-200 placeholder:text-slate-500 transition shadow-xs" />
{/* Status Filter */} -
- +
+
- + {(searchTerm || statusFilter !== 'all') && ( + + )}
- + {/* Action Buttons: Refresh, Export & Create */} +
+ + + + + +
{/* Contracts Table */} diff --git a/PostFundManagement.ConsoleUI/src/components/monitoring/EvidenceReviewQueue.tsx b/PostFundManagement.ConsoleUI/src/components/monitoring/EvidenceReviewQueue.tsx index dd49385..ed6165d 100644 --- a/PostFundManagement.ConsoleUI/src/components/monitoring/EvidenceReviewQueue.tsx +++ b/PostFundManagement.ConsoleUI/src/components/monitoring/EvidenceReviewQueue.tsx @@ -23,6 +23,7 @@ import { Check, FileSpreadsheet, FileCheck, + RefreshCw, } from 'lucide-react'; import { getAllEvidence, @@ -53,6 +54,8 @@ import { } from '../../lib/formatters'; import { Modal } from '../ui/Modal'; import { useToast } from '../ui/Toast'; +import { ExportButton } from '../ui/ExportButton'; +import { ExportColumn } from '../../utils/exportUtils'; export interface EvidenceReviewQueueProps { initialProjectId?: string | number | 'all'; @@ -154,7 +157,12 @@ export const EvidenceReviewQueue: React.FC = ({ }); // Query evidence: if a project is selected (e.g. the first project by default), fetch via /v1/api/evidence/{projectId} - const { data: rawEvidenceList = [], isLoading: loadingEvidence } = useQuery({ + const { + data: rawEvidenceList = [], + isLoading: loadingEvidence, + refetch: refetchEvidence, + isFetching: isFetchingEvidence, + } = useQuery({ queryKey: ['evidence', selectedProjectId], queryFn: async () => { if (selectedProjectId && selectedProjectId !== 'all') { @@ -168,6 +176,19 @@ export const EvidenceReviewQueue: React.FC = ({ // UI depends entirely on the API response - no mock or demo data const allEvidence: Evidence[] = rawEvidenceList; + const evidenceExportColumns: ExportColumn[] = [ + { header: 'Evidence ID', key: 'id' }, + { header: 'File Name', key: 'fileName' }, + { header: 'Project ID', key: 'projectId', format: (val) => `Project #${val}` }, + { header: 'Milestone ID', key: 'milestoneId', format: (val) => (val ? `Milestone #${val}` : '—') }, + { header: 'Activity ID', key: 'activityId', format: (val) => (val ? `Activity #${val}` : '—') }, + { header: 'Document Type', key: 'evidenceType', format: (val) => getEvidenceTypeLabel(val) }, + { header: 'Submitter', key: 'createdBy', format: (val) => getUserEmail(val) }, + { header: 'Submission Date', key: 'createdAt', format: (val) => formatDate(val) }, + { header: 'Verification Status', key: 'status', format: (val) => getApprovalStatusBadge(val).label }, + { header: 'Review Remarks', key: 'remarks', format: (val) => val || '—' }, + ]; + // Review evidence mutation: Facilitates Approve & Reject decisions via PUT /v1/api/evidence const reviewMutation = useMutation({ mutationFn: async ({ @@ -408,14 +429,36 @@ export const EvidenceReviewQueue: React.FC = ({

-
+
+ + + +
diff --git a/PostFundManagement.ConsoleUI/src/lib/exportUtils.ts b/PostFundManagement.ConsoleUI/src/lib/exportUtils.ts index f6017b3..4c40f74 100644 --- a/PostFundManagement.ConsoleUI/src/lib/exportUtils.ts +++ b/PostFundManagement.ConsoleUI/src/lib/exportUtils.ts @@ -67,7 +67,7 @@ function drawPfmLogo(doc: jsPDF, x: number, y: number, size: number): void { // Cyan/Blue Outer hexagonal shield ring doc.setDrawColor(59, 130, 246); doc.setLineWidth(1.2 * scale); - doc.polygon( + (doc as any).polygon( [ { x: x + 32 * scale, y: y + 6.5 * scale }, { x: x + 53.5 * scale, y: y + 19 * scale }, @@ -160,7 +160,7 @@ function drawPfmLogo(doc: jsPDF, x: number, y: number, size: number): void { // Center Plate doc.setFillColor(15, 23, 42); doc.setDrawColor(51, 65, 85); - doc.polygon( + (doc as any).polygon( [ { x: x + 22 * scale, y: y + 28.5 * scale }, { x: x + 32 * scale, y: y + 22.8 * scale }, diff --git a/PostFundManagement.ConsoleUI/src/pages/GovernancePage.tsx b/PostFundManagement.ConsoleUI/src/pages/GovernancePage.tsx index a289e74..041912d 100644 --- a/PostFundManagement.ConsoleUI/src/pages/GovernancePage.tsx +++ b/PostFundManagement.ConsoleUI/src/pages/GovernancePage.tsx @@ -2164,7 +2164,7 @@ export const GovernancePage: React.FC = () => {
- Live API Total + Total Value
@@ -2266,7 +2266,7 @@ export const GovernancePage: React.FC = () => {
- Live API Total + Total Value
diff --git a/PostFundManagement.ConsoleUI/src/pages/MonitoringPage.tsx b/PostFundManagement.ConsoleUI/src/pages/MonitoringPage.tsx index e6058fb..1f89c21 100644 --- a/PostFundManagement.ConsoleUI/src/pages/MonitoringPage.tsx +++ b/PostFundManagement.ConsoleUI/src/pages/MonitoringPage.tsx @@ -22,6 +22,9 @@ import { Link as LinkIcon, Edit2, ChevronRight, + RefreshCw, + Search, + RotateCcw, } from 'lucide-react'; import { getAllIndicators, @@ -36,6 +39,8 @@ import { import { Modal } from '../components/ui/Modal'; import { HelpTooltip } from '../components/ui/HelpTooltip'; import { useToast } from '../components/ui/Toast'; +import { ExportButton } from '../components/ui/ExportButton'; +import { ExportColumn } from '../utils/exportUtils'; import { ContractsManagement } from '../components/monitoring/ContractsManagement'; import { EvidenceReviewQueue } from '../components/monitoring/EvidenceReviewQueue'; import { RiskHeatmapMatrix } from '../components/ui/RiskHeatmapMatrix'; @@ -124,13 +129,34 @@ export const MonitoringPage: React.FC = () => { const [selectedIndicatorForActual, setSelectedIndicatorForActual] = useState(null); const [actualInput, setActualInput] = useState(0); + // Indicator Filter States + const [indicatorSearchTerm, setIndicatorSearchTerm] = useState(''); + const [indicatorProjectFilter, setIndicatorProjectFilter] = useState('all'); + const [indicatorUnitFilter, setIndicatorUnitFilter] = useState('all'); + const [indicatorStatusFilter, setIndicatorStatusFilter] = useState('all'); + + // Risk Filter States + const [riskSearchTerm, setRiskSearchTerm] = useState(''); + const [riskProjectFilter, setRiskProjectFilter] = useState('all'); + const [riskSeverityFilter, setRiskSeverityFilter] = useState('all'); + // Queries - const { data: indicators = [], isLoading: loadingIndicators } = useQuery({ + const { + data: indicators = [], + isLoading: loadingIndicators, + refetch: refetchIndicators, + isFetching: isFetchingIndicators, + } = useQuery({ queryKey: ['indicators'], queryFn: () => getAllIndicators(), }); - const { data: risks = [], isLoading: loadingRisks } = useQuery({ + const { + data: risks = [], + isLoading: loadingRisks, + refetch: refetchRisks, + isFetching: isFetchingRisks, + } = useQuery({ queryKey: ['risks'], queryFn: () => getAllRisks(), }); @@ -264,21 +290,106 @@ export const MonitoringPage: React.FC = () => { return { label: '<70%', badge: 'bg-rose-500/10 text-rose-400 border-rose-500/20' }; }; + const indicatorExportColumns: ExportColumn[] = [ + { header: 'Indicator ID', key: 'id' }, + { header: 'Indicator Statement', key: 'name' }, + { header: 'Project ID', key: 'projectId', format: (val) => `Project #${val}` }, + { header: 'Unit of Measure', key: 'unitOfMeasure', format: (val) => getUnitOfMeasureLabel(val) }, + { header: 'Baseline Value', key: 'baselineAmount' }, + { header: 'Target Value', key: 'targetAmount' }, + { header: 'Actual Recorded', key: 'actualAmount' }, + { + header: 'Achievement %', + key: 'actualAmount', + format: (val, item) => (item.targetAmount ? `${Math.round((Number(val) / Number(item.targetAmount)) * 100)}%` : '—'), + }, + { + header: 'Status', + key: 'actualAmount', + format: (val, item) => getIndicatorStatus(Number(val), Number(item.targetAmount)).label, + }, + ]; + + const filteredIndicators = indicators.filter((ind) => { + if (indicatorProjectFilter !== 'all' && String(ind.projectId) !== String(indicatorProjectFilter)) { + return false; + } + if (indicatorUnitFilter !== 'all' && String(ind.unitOfMeasure) !== String(indicatorUnitFilter)) { + return false; + } + if (indicatorStatusFilter !== 'all') { + const statusObj = getIndicatorStatus(ind.actualAmount, ind.targetAmount); + if (indicatorStatusFilter === 'green' && statusObj.label !== '≥90%') return false; + if (indicatorStatusFilter === 'amber' && statusObj.label !== '70–89%') return false; + if (indicatorStatusFilter === 'red' && statusObj.label !== '<70%') return false; + if (indicatorStatusFilter === 'pending' && statusObj.label !== 'Pending') return false; + } + if (!indicatorSearchTerm.trim()) return true; + const q = indicatorSearchTerm.toLowerCase(); + const name = (ind.name || '').toLowerCase(); + const projId = String(ind.projectId || '').toLowerCase(); + const unit = getUnitOfMeasureLabel(ind.unitOfMeasure).toLowerCase(); + return name.includes(q) || projId.includes(q) || unit.includes(q); + }); + + const riskExportColumns: ExportColumn[] = [ + { header: 'Risk ID', key: 'id' }, + { header: 'Risk Title', key: 'name' }, + { header: 'Risk Description', key: 'description' }, + { header: 'Project ID', key: 'projectId', format: (val) => `Project #${val}` }, + { header: 'Likelihood (1–5)', key: 'likelihood' }, + { header: 'Impact (1–5)', key: 'impact' }, + { + header: 'Residual Score', + key: 'likelihood', + format: (_, item) => `${item.likelihood * item.impact} / 25`, + }, + { + header: 'Severity Level', + key: 'likelihood', + format: (_, item) => getRiskScoreColor(item.likelihood, item.impact).level, + }, + { header: 'Risk Owner', key: 'owner', format: (val) => getUserEmail(val) }, + { header: 'Mitigation Action / Controls', key: 'mitigationAction', format: (val) => val || '—' }, + ]; + + const filteredRisks = risks.filter((rk) => { + if (riskProjectFilter !== 'all' && String(rk.projectId) !== String(riskProjectFilter)) { + return false; + } + if (riskSeverityFilter !== 'all') { + const color = getRiskScoreColor(rk.likelihood, rk.impact); + if (riskSeverityFilter.toLowerCase() !== color.level.toLowerCase()) { + return false; + } + } + if (!riskSearchTerm.trim()) return true; + const q = riskSearchTerm.toLowerCase(); + const name = (rk.name || '').toLowerCase(); + const desc = (rk.description || '').toLowerCase(); + const projId = String(rk.projectId || '').toLowerCase(); + const owner = getUserEmail(rk.owner).toLowerCase(); + const mitig = (rk.mitigationAction || '').toLowerCase(); + return name.includes(q) || desc.includes(q) || projId.includes(q) || owner.includes(q) || mitig.includes(q); + }); + return (
{/* Header */} -
+
-

- M&E, Evidence & Risk Management +
+

+ M&E, Evidence & Risk Management +

-

+

- Verification workflow: Evidence Upload & Review Queue, Indicator Actuals Recording, and Risk Register. + Verification workflow: Evidence Upload & Review Queue, Indicator Actuals Recording, and Risk Register.

@@ -286,16 +397,16 @@ export const MonitoringPage: React.FC = () => { {activeTab === 'contracts' && ( )} {activeTab === 'indicators' && ( + + + ); + }) + )} + + +
)} @@ -478,20 +722,112 @@ export const MonitoringPage: React.FC = () => { subtitle="ISO 31000 standard matrix — click on any cell to filter and inspect risks or edit mitigation controls" /> - {/* Risk Register Table */} + {/* Risk Search & Filter Toolbar */} +
+
+ {/* Search */} +
+ + setRiskSearchTerm(e.target.value)} + className="w-full pl-9 pr-4 py-2.5 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 text-slate-200 placeholder:text-slate-500 transition shadow-xs" + /> +
+ + {/* Project Filter */} +
+ + +
+ + {/* Severity Filter */} +
+ + +
+ + {(riskSearchTerm || riskProjectFilter !== 'all' || riskSeverityFilter !== 'all') && ( + + )} +
+ + {/* Action Buttons: Refresh & Export */} +
+ + + +
+
+ + {/* Risk Register Table Card */}
-
+
-

Project Risk Register

+

+ + Project Risk Register +

Complete itemized audit log of identified risks and residual scores

- + + Showing {filteredRisks.length} of {risks.length} risks +
@@ -510,18 +846,22 @@ export const MonitoringPage: React.FC = () => { {loadingRisks ? ( - - ) : risks.length === 0 ? ( + ) : filteredRisks.length === 0 ? ( - ) : ( - risks.map((rk) => { + filteredRisks.map((rk) => { const score = rk.likelihood * rk.impact; const color = getRiskScoreColor(rk.likelihood, rk.impact); return ( @@ -545,7 +885,7 @@ export const MonitoringPage: React.FC = () => { ) : filteredNotifications.length === 0 ? ( ) : ( @@ -1106,37 +1195,37 @@ export const SettingsPage: React.FC = () => { Total Audit Events
{auditLogs.length}
- Immutable ledger records + Live API ledger records
- Financial Operations + Audited Entities
- {auditLogs.filter((l) => ['Award', 'Budget', 'Disbursement'].includes(l.entityType)).length} + {uniqueAuditEntities.length}
- Awards, budgets & disbursements + Distinct system resources
- Security & Auth + Active Actors
- {auditLogs.filter((l) => ['User', 'Security', 'Invite'].includes(l.entityType)).length} + {new Set(auditLogs.map((l) => l.performedBy).filter(Boolean)).size}
- Logins, roles & access syncs + Distinct caller UUIDs
- Audited Actors + Action Categories
- {new Set(auditLogs.map((l) => l.userEmail)).size} + {uniqueAuditActions.length}
- Unique authorized principals + Registered action types
@@ -1148,7 +1237,7 @@ export const SettingsPage: React.FC = () => { type="text" value={auditSearchTerm} onChange={(e) => setAuditSearchTerm(e.target.value)} - placeholder="Search audit trail by action, email, entity ID, or details..." + placeholder="Search audit trail by action, entity name, ID, performed by UUID..." 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" /> @@ -1162,14 +1251,20 @@ export const SettingsPage: React.FC = () => { className="text-xs bg-[#090d16] border border-slate-700/80 rounded-xl px-2.5 py-1.5 text-white focus:outline-none focus:ring-1 focus:ring-blue-500 [&>option]:bg-slate-900 [&>option]:text-white cursor-pointer" > - - - - - - - - + {uniqueAuditEntities.map((ent) => ( + + ))} + {uniqueAuditEntities.length === 0 && ( + <> + + + + + + + )} @@ -1181,15 +1276,11 @@ export const SettingsPage: React.FC = () => { className="text-xs bg-[#090d16] border border-slate-700/80 rounded-xl px-2.5 py-1.5 text-white focus:outline-none focus:ring-1 focus:ring-blue-500 [&>option]:bg-slate-900 [&>option]:text-white cursor-pointer" > - - - - - - - - - + {uniqueAuditActions.map((act) => ( + + ))} @@ -1211,19 +1302,19 @@ export const SettingsPage: React.FC = () => { type="button" onClick={() => { refetchAuditLogs(); - success('Audit Ledger Refreshed', 'Latest audit log entries loaded.'); + success('Refreshed', 'Latest audit log entries loaded from API.'); }} disabled={isFetchingAuditLogs} - className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-slate-300 bg-[#0d121f] hover:bg-slate-800 border border-slate-700/80 rounded-xl transition cursor-pointer shadow-xs disabled:opacity-50" - title="Refresh audit trail" + className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 text-xs font-semibold text-slate-300 bg-[#0d121f] hover:bg-slate-800 border border-slate-700/80 rounded-xl transition cursor-pointer shadow-xs disabled:opacity-50" + title="Refresh audit trail from API" > - Refresh + Refresh { {/* Audit Logs Table */}
+
+
+ +

+ Audit Log Ledger ({filteredAuditLogs.length} of {auditLogs.length}) +

+ +
+ + POST /v1/api/governance/auditlog + +
+
- Loading risk register... + + + Loading risk register from API...
- No risks recorded in the register. Click "Add Risk" to log your first risk item. + + + {riskSearchTerm || riskProjectFilter !== 'all' || riskSeverityFilter !== 'all' + ? 'No risks match your filter criteria.' + : 'No risks recorded in the register. Click "Add Risk" to log your first risk item.'}
)} + + + + @@ -997,7 +1086,7 @@ export const SettingsPage: React.FC = () => { /> - GET /v1/api/communications/norifications/{notificationOrgFilter === 'all' ? '{organisationId}' : notificationOrgFilter} + GET /v1/api/communications/norifications/{effectiveNotificationOrgId} @@ -1020,14 +1109,14 @@ export const SettingsPage: React.FC = () => {
- Querying communication microservice endpoints... + Querying communication microservice endpoints for Organisation #{effectiveNotificationOrgId}...
- No notification records returned matching filter criteria. + No notification records returned for Organisation #{effectiveNotificationOrgId}.
- + + - - + + {loadingAuditLogs ? ( - ) : filteredAuditLogs.length === 0 ? ( - ) : ( filteredAuditLogs.map((log) => { const badge = getActionBadge(log.action); + const isExpanded = !!expandedAuditLogIds[log.id]; + const changesCount = Array.isArray(log.changes) ? log.changes.length : 0; + const performerEmail = getUserEmail(log.performedBy); + const hasResolvedEmail = performerEmail && performerEmail !== '—' && !performerEmail.includes('—'); + const displayEmail = hasResolvedEmail ? performerEmail : (log.performedBy || 'System'); + return ( - - - - - - - - toggleExpandAuditLog(log.id)} + className={`hover:bg-slate-800/40 transition-colors cursor-pointer ${ + isExpanded ? 'bg-slate-800/30' : '' + }`} + > + + + + + + - +
+ + {displayEmail} + + {log.performedBy && ( + + )} +
+ + + + + + {/* Expanding Shelf for Changes Content */} + {isExpanded && ( + + + + )} + ); }) )} @@ -1319,9 +1571,10 @@ export const SettingsPage: React.FC = () => { onClose={() => { setIsAuditDetailModalOpen(false); setSelectedAuditLog(null); + setCopiedAuditPayload(false); }} - title={`Audit Event: ${selectedAuditLog?.id || ''}`} - subtitle="Cryptographic verification and authoritative event metadata" + title={`Audit Event: #${selectedAuditLog?.id || ''}`} + subtitle="Authoritative event payload from /v1/api/governance/auditlog" > {selectedAuditLog && (
@@ -1329,37 +1582,112 @@ export const SettingsPage: React.FC = () => {
Action Type: - {selectedAuditLog.action} + {selectedAuditLog.action || 'EVENT'}
- Entity Type: - {selectedAuditLog.entityType} + Entity Name: + {selectedAuditLog.entityName || '—'}
Entity ID: - {selectedAuditLog.entityId} + {selectedAuditLog.entityId || '—'}
- Actor / Principal: - {selectedAuditLog.userEmail} + Performed By: +
+ + {getUserEmail(selectedAuditLog.performedBy) !== '—' + ? getUserEmail(selectedAuditLog.performedBy) + : (selectedAuditLog.performedBy || 'System')} + + {selectedAuditLog.performedBy && ( + + )} +
+ {selectedAuditLog.performedBy && ( + + ID: {selectedAuditLog.performedBy} + + )}
Logged Timestamp: - {formatDate(selectedAuditLog.createdAt)} + {formatDate(selectedAuditLog.timestamp)}
-
- Details & Audit Trail: -

- {selectedAuditLog.details || 'No additional payload notes recorded for this transaction.'} -

+ {selectedAuditLog.tenantId && ( +
+ Tenant Scope: + Tenant #{selectedAuditLog.tenantId} +
+ )} + +
+ State Changes / Delta: + {Array.isArray(selectedAuditLog.changes) && selectedAuditLog.changes.length > 0 ? ( +
    + {selectedAuditLog.changes.map((ch, idx) => ( +
  • + + {ch} +
  • + ))} +
+ ) : ( +

+ No granular delta changes specified in audit payload. +

+ )} +
+ + {/* Raw JSON Data Viewer */} +
+
+ + Raw Audit Record (JSON): + + +
+
+                  {JSON.stringify(selectedAuditLog, null, 2)}
+                
@@ -1369,6 +1697,7 @@ export const SettingsPage: React.FC = () => { onClick={() => { setIsAuditDetailModalOpen(false); setSelectedAuditLog(null); + setCopiedAuditPayload(false); }} className="px-4 py-2 text-xs font-semibold text-white bg-slate-800 hover:bg-slate-700 rounded-xl transition cursor-pointer" > diff --git a/PostFundManagement.ConsoleUI/src/services/api.ts b/PostFundManagement.ConsoleUI/src/services/api.ts index 7faf8f8..859ef76 100644 --- a/PostFundManagement.ConsoleUI/src/services/api.ts +++ b/PostFundManagement.ConsoleUI/src/services/api.ts @@ -1461,169 +1461,41 @@ export const getApplicantInvites = async (applicantUserId: string): Promise { - try { - const raw = localStorage.getItem(NOTIFICATIONS_STORAGE_KEY); - if (!raw) { - localStorage.setItem(NOTIFICATIONS_STORAGE_KEY, JSON.stringify(INITIAL_NOTIFICATIONS)); - return INITIAL_NOTIFICATIONS; - } - const parsed = JSON.parse(raw); - return Array.isArray(parsed) && parsed.length > 0 ? parsed : INITIAL_NOTIFICATIONS; - } catch { - return INITIAL_NOTIFICATIONS; - } -}; - export const getNotifications = async (organisationId?: string | number): Promise => { - const targetOrgId = - organisationId !== undefined && organisationId !== '' && organisationId !== null && organisationId !== 'all' - ? organisationId - : getCurrentUserTenantId(); + const targetOrgId = organisationId !== undefined && organisationId !== '' && organisationId !== null && organisationId !== 'all' + ? organisationId + : getCurrentUserTenantId(); if (!targetOrgId || targetOrgId === 'all') { - return getStoredNotifications(); + return []; } const hashedOrgId = await toolsService.resolveOrganisationId(targetOrgId); try { const res = await apiClient.get( - `/v1/api/communications/norifications/${hashedOrgId}` + `/v1/api/communications/norifications/${encodeURIComponent(hashedOrgId)}` ); - if (res.data && Array.isArray(res.data) && res.data.length > 0) { + if (res.data && Array.isArray(res.data)) { return res.data; } } catch (err: any) { if (err?.response?.status === 404) { try { - const res = await apiClient.get( - `/v1/api/communications/notifications/${hashedOrgId}` + const fallbackRes = await apiClient.get( + `/v1/api/communications/notifications/${encodeURIComponent(hashedOrgId)}` ); - if (res.data && Array.isArray(res.data) && res.data.length > 0) { - return res.data; + if (fallbackRes.data && Array.isArray(fallbackRes.data)) { + return fallbackRes.data; } - } catch { - // Fallback to local cache + } catch (fallbackErr) { + console.warn('[getNotifications] Notification fallback route error:', fallbackErr); } + } else { + console.warn('[getNotifications] API error fetching notifications:', err); } } - // Filter stored notifications for this organization or return matching items - const allStored = getStoredNotifications(); - const orgFiltered = allStored.filter( - (n) => String(n.organisationId) === String(targetOrgId) || String(n.organisationId) === String(toIntegerId(targetOrgId)) - ); - return orgFiltered.length > 0 ? orgFiltered : allStored; + return []; }; // ========================================== @@ -2082,160 +1954,29 @@ export const updateRule = async (payload: UpdateRuleRequest): Promise => { // 12. Audit Logs & System Telemetry APIs // ========================================== -const AUDIT_STORAGE_KEY = 'pfm_audit_ledger_v1'; - -const INITIAL_AUDIT_LOGS: AuditLog[] = [ - { - id: 'AUD-88391', - action: 'USER_AUTHENTICATED', - entityType: 'User', - entityId: 'USR-KHWEZI', - userEmail: 'khwezim@gmail.com', - details: 'User authenticated via OIDC Provider. Multi-tenant security context active.', - createdAt: new Date(Date.now() - 1000 * 60 * 12).toISOString(), - }, - { - id: 'AUD-88390', - action: 'DISBURSEMENT_APPROVED', - entityType: 'Disbursement', - entityId: 'DSB-101', - userEmail: 'accounting.officer@treasury.gov.za', - details: 'Authorized tranche release of ZAR 2,500,000.00 for Project #401. EFT reference EFT-2026-9031.', - createdAt: new Date(Date.now() - 1000 * 60 * 48).toISOString(), - }, - { - id: 'AUD-88389', - action: 'DISBURSEMENT_SCHEDULED', - entityType: 'Disbursement', - entityId: 'DSB-101', - userEmail: 'grants.officer@dst.gov.za', - details: 'Scheduled tranche of ZAR 2,500,000.00 linked to Milestone Deliverable #201.', - createdAt: new Date(Date.now() - 1000 * 60 * 95).toISOString(), - }, - { - id: 'AUD-88388', - action: 'BUDGET_LINE_CREATED', - entityType: 'Budget', - entityId: 'BDG-401-01', - userEmail: 'khwezim@gmail.com', - details: 'Established Specialized Equipment & Prototype Probes budget category for ZAR 4,500,000.00 against Project #401.', - createdAt: new Date(Date.now() - 1000 * 60 * 180).toISOString(), - }, - { - id: 'AUD-88387', - action: 'AWARD_REGISTERED', - entityType: 'Award', - entityId: 'AWD-2026-441', - userEmail: 'khwezim@gmail.com', - details: 'Registered approved statutory funding award for ZAR 10,000,000.00 (Programme #301, Org #101).', - createdAt: new Date(Date.now() - 1000 * 60 * 320).toISOString(), - }, - { - id: 'AUD-88386', - action: 'RISK_EVALUATED', - entityType: 'Risk', - entityId: 'RSK-202', - userEmail: 'risk.compliance@treasury.gov.za', - details: 'Evaluated ISO 31000 Risk: Supply Chain Logistics & Component Delay. Residual risk score set to Medium (9).', - createdAt: new Date(Date.now() - 1000 * 60 * 480).toISOString(), - }, - { - id: 'AUD-88385', - action: 'EVIDENCE_VERIFIED', - entityType: 'Evidence', - entityId: 'EVD-993', - userEmail: 'evaluator@nrf.ac.za', - details: 'Verified milestone completion evidence report with cryptographic hash verification.', - createdAt: new Date(Date.now() - 1000 * 60 * 720).toISOString(), - }, - { - id: 'AUD-88384', - action: 'INVITATION_DISPATCHED', - entityType: 'Invite', - entityId: 'INV-1092', - userEmail: 'khwezim@gmail.com', - details: 'Dispatched statutory onboarding invitation to reviewer@institution.ac.za via Email SMTP gateway.', - createdAt: new Date(Date.now() - 1000 * 60 * 1050).toISOString(), - }, - { - id: 'AUD-88383', - action: 'DIRECTORY_SYNCED', - entityType: 'Security', - entityId: 'TENANT-001', - userEmail: 'system@treasury.gov.za', - details: 'Executed backend synchronization handshake. 12 active users and authorization claims validated.', - createdAt: new Date(Date.now() - 1000 * 60 * 1440).toISOString(), - }, -]; - -export const getStoredAuditLogs = (): AuditLog[] => { +export const getAuditLogs = async (page = 1, pageSize = 50): Promise => { try { - const raw = localStorage.getItem(AUDIT_STORAGE_KEY); - if (!raw) { - localStorage.setItem(AUDIT_STORAGE_KEY, JSON.stringify(INITIAL_AUDIT_LOGS)); - return INITIAL_AUDIT_LOGS; + const res = await apiClient.post('/v1/api/governance/auditlog', { + index: (page - 1) * pageSize, + page, + pageSize, + }); + if (res.data && Array.isArray(res.data)) { + return res.data; } - const parsed = JSON.parse(raw); - return Array.isArray(parsed) && parsed.length > 0 ? parsed : INITIAL_AUDIT_LOGS; - } catch { - return INITIAL_AUDIT_LOGS; - } -}; - -export const recordAuditLog = ( - log: Omit & { id?: string; createdAt?: string } -): AuditLog => { - const current = getStoredAuditLogs(); - const newEntry: AuditLog = { - id: log.id || `AUD-${Math.floor(10000 + Math.random() * 90000)}`, - action: log.action, - entityType: log.entityType, - entityId: log.entityId, - userEmail: log.userEmail || 'khwezim@gmail.com', - details: log.details, - createdAt: log.createdAt || new Date().toISOString(), - }; - - const updated = [newEntry, ...current]; - try { - localStorage.setItem(AUDIT_STORAGE_KEY, JSON.stringify(updated)); + return []; } catch (err) { - console.warn('Failed to save audit log to localStorage:', err); + console.error('[getAuditLogs] API request failed:', err); + return []; } - return newEntry; }; -export const getAuditLogs = async (page = 1, pageSize = 100): Promise => { - // Try candidate backend endpoints first - const endpoints = [ - `/v1/api/governance/audit/${page}/${pageSize}`, - `/v1/api/governance/audit`, - `/v1/api/audit/${page}/${pageSize}`, - `/v1/api/audit`, - ]; - - for (const ep of endpoints) { - try { - const res = await apiClient.get(ep); - if (res.data && Array.isArray(res.data) && res.data.length > 0) { - return res.data; - } - } catch { - // fallback to next candidate or stored ledger - } - } - - return getStoredAuditLogs(); -}; - -export const getAuditLog = async (auditId: string): Promise => { +export const getAuditLog = async (auditId: string | number): Promise => { try { - const res = await apiClient.get(`/v1/api/governance/audit/${auditId}`); - if (res.data) return res.data; + const logs = await getAuditLogs(1, 100); + return logs.find((l) => String(l.id) === String(auditId)) || null; } catch { - // fallback + return null; } - const logs = getStoredAuditLogs(); - return logs.find((l) => String(l.id) === String(auditId)) || null; }; diff --git a/PostFundManagement.ConsoleUI/src/types.ts b/PostFundManagement.ConsoleUI/src/types.ts index 6fc7a89..621481e 100644 --- a/PostFundManagement.ConsoleUI/src/types.ts +++ b/PostFundManagement.ConsoleUI/src/types.ts @@ -274,13 +274,14 @@ export interface InviteUserRequest { } export interface AuditLog { - id: string; - action: string; - entityType: string; - entityId: string | number; - userEmail: string; - details?: string; - createdAt: string; + id: number | string; + tenantId?: number | string | null; + entityName?: string | null; + entityId?: string | null; + action?: string | null; + changes?: string[] | null; + performedBy: string; + timestamp: string; } export interface UserFilterRequest { -- 2.47.3
TimestampTimestamp Event ID Action EntityActorDetails & TelemetryPerformed ByChanges Inspect
- Loading system audit ledger... + + + Querying governance audit log endpoints...
+ + {auditSearchTerm || auditEntityFilter !== 'all' || auditActionFilter !== 'all' ? 'No audit log entries match your filter criteria.' - : 'No audit records logged yet.'} + : 'No audit records returned from endpoint.'}
- {formatDate(log.createdAt)} - - {log.id} - - - {badge.label} - - - {log.entityType} - #{log.entityId} - - {getUserEmail(log.userEmail)} - - {log.details || '—'} - -
+ + + {formatDate(log.timestamp)} + + #{log.id} + + + {badge.label} + + + {log.entityName || 'General'} + {log.entityId && ( + #{log.entityId} + )} + e.stopPropagation()} > - - Inspect - -
+ + + +
+
+
+
+ +

+ Recorded Changes Content (Event #{log.id}) +

+ + {changesCount} {changesCount === 1 ? 'Entry' : 'Entries'} + +
+
+ + Entity: {log.entityName || 'General'} {log.entityId ? `(#${log.entityId})` : ''} + + • + + Performer: {displayEmail} + {log.performedBy && ( + + )} + +
+
+ + {/* Changes List */} + {Array.isArray(log.changes) && log.changes.length > 0 ? ( +
+ {log.changes.map((changeItem, idx) => ( +
+ + #{idx + 1} + +
+ {changeItem} +
+
+ ))} +
+ ) : ( +
+ No itemized field changes content recorded for this event. +
+ )} +
+