diff --git a/PostFundManagement.ConsoleUI/src/components/notifications/UserSearchPicker.tsx b/PostFundManagement.ConsoleUI/src/components/notifications/UserSearchPicker.tsx index 3054bb8..d5b77d2 100644 --- a/PostFundManagement.ConsoleUI/src/components/notifications/UserSearchPicker.tsx +++ b/PostFundManagement.ConsoleUI/src/components/notifications/UserSearchPicker.tsx @@ -69,7 +69,7 @@ export const UserSearchPicker: React.FC = ({ const filtered = activeUsers.filter(u => { if (!term) return true; const email = (u.email || '').toLowerCase(); - const role = (u.role || '').toLowerCase(); + const role = String(u.role ?? '').toLowerCase(); return email.includes(term) || role.includes(term); }); @@ -80,7 +80,7 @@ export const UserSearchPicker: React.FC = ({ const otherUsers: User[] = []; filtered.forEach(u => { - const r = (u.role || '').toLowerCase(); + const r = String(u.role ?? '').toLowerCase(); if (r.includes('admin') || r.includes('executive') || r.includes('director')) { adminUsers.push(u); } else if (r.includes('programme') || r.includes('project') || r.includes('manager')) { @@ -178,7 +178,7 @@ export const UserSearchPicker: React.FC = ({ id: u.id, email: u.email, name: formattedName, - role: u.role || 'User', + role: String(u.role ?? 'User'), }); setIsDropdownOpen(false); } else { @@ -186,7 +186,7 @@ export const UserSearchPicker: React.FC = ({ id: u.id, email: u.email, name: formattedName, - role: u.role || 'User', + role: String(u.role ?? 'User'), }); } } diff --git a/PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx b/PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx index 5760f5d..326070f 100644 --- a/PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx +++ b/PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx @@ -214,6 +214,11 @@ export const ExecutiveDashboard: React.FC = () => { return dashboard?.disbursed || 0; }, [dashboard, disbursements, filteredProjects, selectedProgramme, selectedSector]); + const disbursedPercentage = useMemo(() => { + if (!portfolioValue || portfolioValue <= 0) return 0; + return Math.min(100, Math.round((disbursed / portfolioValue) * 1000) / 10); + }, [disbursed, portfolioValue]); + const onTrackPercentage = useMemo(() => { const targetProjects = filteredProjects.length > 0 ? filteredProjects : projects; if (targetProjects.length === 0) return 0; @@ -341,7 +346,7 @@ export const ExecutiveDashboard: React.FC = () => { }, [disbursements]); // Projects requiring management attention based on risk profile - const attentionProjects = useMemo(() => { + const attentionProjects = useMemo(() => { const targetProjects = filteredProjects.length > 0 ? filteredProjects : projects; if (targetProjects.length === 0) return []; @@ -356,7 +361,7 @@ export const ExecutiveDashboard: React.FC = () => { }); return targetProjects - .map((p) => { + .map((p): AttentionProject => { const maxScore = projectMaxRisk[String(p.id)] || 0; const isRejected = p.status === ApprovalStatus.Rejected; const isPending = p.status === ApprovalStatus.Pending || p.status === ApprovalStatus.Review; @@ -393,7 +398,7 @@ export const ExecutiveDashboard: React.FC = () => { return { id: p.id, - name: p.name, + name: p.name || 'Unnamed Project', programme: p.programmeName || `Programme ${p.programmeId}`, status: statusLabel, risk: riskLevel, @@ -568,187 +573,328 @@ export const ExecutiveDashboard: React.FC = () => { - {/* 8 Headline Portfolio KPI Cards with Compliance & Budget At Risk */} -
- {/* Card 1: Portfolio Value */} -
-
-
- Portfolio Value - -
-
- + {/* Executive Portfolio KPIs: Structured 2-Tier Balanced Grid */} +
+ {/* Tier 1: Capital Commitments & Deployment Scale */} +
+
+
+ + Capital Deployment & Scale + + + Portfolio commitments, disbursements & capital exposure
+ + Financial Governance +
-
-

{formatCompactZAR(portfolioValue)}

-

{formatZAR(portfolioValue)}

+ +
+ {/* Card 1: Portfolio Value */} +
+
+
+ + Portfolio Value + + +
+
+ +
+
+
+

+ {formatCompactZAR(portfolioValue)} +

+
+ + {formatZAR(portfolioValue)} + + + Statutory Total + +
+
+
+ + {/* Card 2: Disbursed */} +
+
+
+ + Disbursed + + +
+
+ +
+
+
+
+

+ {formatCompactZAR(disbursed)} +

+ + {disbursedPercentage}% + +
+
+
+
+
+ Tranches released + {disbursedPercentage}% deployed +
+
+
+ + {/* Card 3: Active Awards */} +
+
+
+ + Active Awards + + +
+
+ +
+
+
+

+ {activeAwards} +

+
+ Under implementation + + {portfolios.length} Portfolio{portfolios.length === 1 ? '' : 's'} + +
+
+
+ + {/* Card 4: Budget At Risk */} +
+
+
+ + Budget At Risk + + +
+
+ +
+
+
+

+ {formatCompactZAR(totalBudgetAtRisk)} +

+
+ + {formatZAR(totalBudgetAtRisk)} + + 0 + ? 'text-rose-400 bg-rose-500/10 border-rose-500/20' + : 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20' + }`}> + {totalBudgetAtRisk > 0 ? 'Exposed' : 'Clear'} + +
+
+
- {/* Card 2: Active Awards */} -
-
-
- Active Awards - -
-
- + {/* Tier 2: Operational Health, Risk & Beneficiary Impact */} +
+
+
+ + Operational Performance & Impact + + + Milestone pace, compliance gate health & direct beneficiaries
+ + Delivery Health +
-
-

{activeAwards}

-

- Across {portfolios.length} portfolio{portfolios.length === 1 ? '' : 's'} -

-
-
- {/* Card 3: Disbursed */} -
-
-
- Disbursed - +
+ {/* Card 5: On Track */} +
+
+
+ + On Track + + +
+
+ +
+
+
+
+

+ {onTrackPercentage}% +

+ + {onTrackPercentage >= 80 ? 'Optimal' : onTrackPercentage >= 60 ? 'Moderate' : 'Critical'} + +
+
+
+
+
+ Milestones verified + Pace index +
+
-
- -
-
-
-

{formatCompactZAR(disbursed)}

-

- {portfolioValue > 0 ? `${((disbursed / portfolioValue) * 100).toFixed(1)}% of commitments` : '0.0% of commitments'} -

-
-
- {/* Card 4: On Track */} -
-
-
- On Track - + {/* Card 6: Compliance Health */} +
+
+
+ + Compliance + + +
+
+ +
+
+
+
+

+ {complianceRate}% +

+ + {complianceRate >= 90 ? 'Passed' : 'Gated'} + +
+
+
+
+
+ Statutory gate check + 100% requirement +
+
-
- -
-
-
-

{onTrackPercentage}%

-

Milestones verified

-
-
- {/* Card 5: At Risk */} -
-
-
- At Risk - + {/* Card 7: At Risk */} +
+
+
+ + At Risk + + +
+
+ +
+
+
+

+ {atRiskCount} +

+
+ {atRiskCount === 1 ? 'Project flagged' : 'Projects flagged'} + 0 + ? 'text-amber-400 bg-amber-500/10 border-amber-500/20' + : 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20' + }`}> + {atRiskCount > 0 ? 'Requires PM Action' : 'Nominal'} + +
+
-
- -
-
-
-

{atRiskCount}

-

Requires PM action

-
-
- {/* Card 6: Beneficiaries Reached */} -
-
-
- Beneficiaries - + {/* Card 8: Beneficiaries Reached */} +
+
+
+ + Beneficiaries + + +
+
+ +
+
+
+

+ {beneficiariesReached >= 1000 ? `${(beneficiariesReached / 1000).toFixed(1)}k` : beneficiariesReached} +

+
+ Directly impacted + + M&E Verified + +
+
-
- -
-
-
-

- {beneficiariesReached >= 1000 ? `${(beneficiariesReached / 1000).toFixed(0)}k` : beneficiariesReached} -

-

Directly impacted

-
-
- - {/* Card 7: Budget At Risk */} -
-
-
- Budget At Risk - -
-
- -
-
-
-

{formatCompactZAR(totalBudgetAtRisk)}

-

{formatZAR(totalBudgetAtRisk)}

-
-
- - {/* Card 8: Compliance Health */} -
-
-
- Compliance - -
-
- -
-
-
-

{complianceRate}%

-

100% statutory gate