Files
pfm/PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx
T

1014 lines
47 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useMemo } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import {
TrendingUp,
AlertTriangle,
Users,
Layers,
Coins,
CheckCircle,
ArrowUpRight,
Filter,
RefreshCw,
Clock,
ShieldAlert,
RotateCcw,
LayoutDashboard,
ShieldCheck,
} from 'lucide-react';
import {
getDashboard,
getProjects,
getAllPortfolios,
getAllProgrammes,
getAwards,
getBudgets,
getDisbursements,
getAllRisks,
getAllIndicators,
getProjectComplianceItems,
} from '../services/api';
import { formatCompactZAR, formatZAR, getIndustrySectorLabel } from '../lib/formatters';
import { ApprovalStatus, ComplianceStatus } from '../types';
import { RiskHeatmapMatrix } from '../components/ui/RiskHeatmapMatrix';
import { HelpTooltip } from '../components/ui/HelpTooltip';
import { useTableSort, SortableTh } from '../utils/tableSort';
interface AttentionProject {
id: number | string;
name: string;
programme: string;
status: string;
risk: string;
complianceStatus: string;
isCompliant: boolean;
unmetCount: number;
budgetAtRisk: number;
financialVariance: string;
varianceType: string;
maxScore: number;
}
export const ExecutiveDashboard: React.FC = () => {
const queryClient = useQueryClient();
const [selectedProgramme, setSelectedProgramme] = useState('All');
const [selectedSector, setSelectedSector] = useState('All');
const [selectedPeriod, setSelectedPeriod] = useState('FY 2026/27');
const isFiltered = selectedProgramme !== 'All' || selectedSector !== 'All' || selectedPeriod !== 'FY 2026/27';
const activeFilterCount =
(selectedProgramme !== 'All' ? 1 : 0) +
(selectedSector !== 'All' ? 1 : 0) +
(selectedPeriod !== 'FY 2026/27' ? 1 : 0);
const resetFilters = () => {
setSelectedProgramme('All');
setSelectedSector('All');
setSelectedPeriod('FY 2026/27');
};
// Data queries
const { data: dashboard, isLoading: loadingDashboard, refetch: refetchDashboard, isFetching } = useQuery({
queryKey: ['dashboard'],
queryFn: getDashboard,
});
const { data: projects = [] } = useQuery({
queryKey: ['projects'],
queryFn: () => getProjects(),
});
const { data: portfolios = [] } = useQuery({
queryKey: ['portfolios'],
queryFn: () => getAllPortfolios(),
});
const { data: programmes = [] } = useQuery({
queryKey: ['programmes'],
queryFn: () => getAllProgrammes(),
});
const { data: awards = [] } = useQuery({
queryKey: ['awards'],
queryFn: () => getAwards(),
});
const { data: budgets = [] } = useQuery({
queryKey: ['budgets'],
queryFn: () => getBudgets(),
});
const { data: disbursements = [] } = useQuery({
queryKey: ['disbursements'],
queryFn: () => getDisbursements(),
});
const { data: risks = [] } = useQuery({
queryKey: ['risks'],
queryFn: () => getAllRisks(),
});
const { data: indicators = [] } = useQuery({
queryKey: ['indicators'],
queryFn: () => getAllIndicators(),
});
// Filtered projects according to active Programme and Sector dropdown selections
const filteredProjects = useMemo(() => {
return projects.filter((p) => {
if (selectedProgramme !== 'All' && String(p.programmeId) !== selectedProgramme) {
return false;
}
if (selectedSector !== 'All') {
const sectorLabel = getIndustrySectorLabel(p.sector);
if (sectorLabel !== selectedSector) return false;
}
return true;
});
}, [projects, selectedProgramme, selectedSector]);
const { data: projectComplianceMap = new Map<string | number, any[]>() } = useQuery({
queryKey: ['projectComplianceMap', filteredProjects.map((p) => p.id)],
queryFn: async () => {
const map = new Map<string | number, any[]>();
await Promise.all(
filteredProjects.map(async (p) => {
try {
const items = await getProjectComplianceItems(p.id);
map.set(p.id, items);
} catch {
map.set(p.id, []);
}
})
);
return map;
},
enabled: filteredProjects.length > 0,
});
const refetchAll = () => {
refetchDashboard();
queryClient.invalidateQueries();
};
// Filtered risks associated with the filtered project set
const filteredRisks = useMemo(() => {
if (selectedProgramme === 'All' && selectedSector === 'All') {
return risks;
}
const projectIds = new Set(filteredProjects.map((p) => String(p.id)));
return risks.filter((r) => projectIds.has(String(r.projectId)));
}, [risks, filteredProjects, selectedProgramme, selectedSector]);
// Headline portfolio metrics calculation
const portfolioValue = useMemo(() => {
if (selectedProgramme !== 'All' || selectedSector !== 'All') {
const projectIds = new Set(filteredProjects.map((p) => String(p.id)));
const filteredBudgets = budgets.filter((b) => projectIds.has(String(b.projectId)));
const budgetTotal = filteredBudgets.reduce((sum, b) => sum + (Number(b.amount) || 0), 0);
if (budgetTotal > 0) return budgetTotal;
const filteredAwards = awards.filter((a) => projectIds.has(String(a.projectId)));
const awardsTotal = filteredAwards.reduce((sum, a) => sum + (Number(a.amount) || 0), 0);
return awardsTotal;
}
if (dashboard?.portfolioValue && dashboard.portfolioValue > 0) {
return dashboard.portfolioValue;
}
const budgetTotal = budgets.reduce((sum, b) => sum + (Number(b.amount) || 0), 0);
if (budgetTotal > 0) return budgetTotal;
const awardsTotal = awards.reduce((sum, a) => sum + (Number(a.amount) || 0), 0);
return awardsTotal;
}, [dashboard, budgets, awards, filteredProjects, selectedProgramme, selectedSector]);
const activeAwards = useMemo(() => {
if (selectedProgramme !== 'All' || selectedSector !== 'All') {
const projectIds = new Set(filteredProjects.map((p) => String(p.id)));
const matched = awards.filter((a) => projectIds.has(String(a.projectId)));
return matched.length || filteredProjects.length;
}
if (awards.length > 0) return awards.length;
return dashboard?.activeAwards ?? projects.length;
}, [dashboard, awards, filteredProjects, projects, selectedProgramme, selectedSector]);
const disbursed = useMemo(() => {
const relevantDisbursements = disbursements.filter((d) => {
if (selectedProgramme !== 'All' || selectedSector !== 'All') {
const projectIds = new Set(filteredProjects.map((p) => String(p.id)));
return projectIds.has(String(d.projectId));
}
return true;
});
const approvedTotal = relevantDisbursements
.filter((d) => d.status === ApprovalStatus.Approved || (d.status as number) === 3)
.reduce((sum, d) => sum + (Number(d.amount) || 0), 0);
if (approvedTotal > 0) return approvedTotal;
const allTotal = relevantDisbursements.reduce((sum, d) => sum + (Number(d.amount) || 0), 0);
if (allTotal > 0 && (!dashboard?.disbursed || dashboard.disbursed === 0)) {
return allTotal;
}
return dashboard?.disbursed || 0;
}, [dashboard, disbursements, filteredProjects, selectedProgramme, selectedSector]);
const onTrackPercentage = useMemo(() => {
const targetProjects = filteredProjects.length > 0 ? filteredProjects : projects;
if (targetProjects.length === 0) return 0;
const onTrack = targetProjects.filter(
(p) => p.status === ApprovalStatus.Approved || (p.status as number) === 3 || (p.status as unknown as string) === 'Active'
).length;
if (onTrack === 0 && dashboard?.onTrackPercentage && dashboard.onTrackPercentage > 0) {
return dashboard.onTrackPercentage;
}
return Math.round((onTrack / targetProjects.length) * 100);
}, [dashboard, filteredProjects, projects]);
// Calculate at-risk projects count based on risk scores
const atRiskCount = useMemo(() => {
const targetRisks = filteredRisks;
const highRisks = targetRisks.filter((r) => {
const score = (Number(r.likelihood) || 1) * (Number(r.impact) || 1);
return score >= 12 || ((Number(r.impact) || 1) >= 4 && (Number(r.likelihood) || 1) >= 3);
});
if (targetRisks.length > 0) {
return highRisks.length;
}
return dashboard?.atRiskCount ?? 0;
}, [dashboard, filteredRisks]);
// Compliance and Capital at Risk calculations
const totalBudgetAtRisk = useMemo(() => {
const targetProjects = filteredProjects.length > 0 ? filteredProjects : projects;
return targetProjects.reduce((sum, p) => {
const pComps = projectComplianceMap.get(p.id) || [];
const hasUnmet = pComps.some((c) => Number(c.status) === ComplianceStatus.NotMet || Number(c.status) === 1);
const isPendingOrRejected = p.status === ApprovalStatus.Rejected || p.status === ApprovalStatus.Pending;
if (hasUnmet || isPendingOrRejected) {
const pBudgets = budgets.filter((b) => String(b.projectId) === String(p.id));
const pApproved = pBudgets
.filter((b) => Number(b.status) === ApprovalStatus.Approved || b.status === undefined)
.reduce((bSum, b) => bSum + (Number(b.amount) || 0), 0);
return sum + pApproved;
}
return sum;
}, 0);
}, [filteredProjects, projects, projectComplianceMap, budgets]);
const complianceRate = useMemo(() => {
const targetProjects = filteredProjects.length > 0 ? filteredProjects : projects;
if (targetProjects.length === 0) return 100;
const compliantCount = targetProjects.filter((p) => {
const pComps = projectComplianceMap.get(p.id) || [];
return !pComps.some((c) => Number(c.status) === ComplianceStatus.NotMet || Number(c.status) === 1);
}).length;
return Math.round((compliantCount / targetProjects.length) * 100);
}, [filteredProjects, projects, projectComplianceMap]);
const beneficiariesReached = useMemo(() => {
if (dashboard?.beneficiariesReached && dashboard.beneficiariesReached > 0) {
return dashboard.beneficiariesReached;
}
const sumActual = indicators.reduce((sum, ind) => sum + (Number(ind.actualAmount) || 0), 0);
return sumActual > 0 ? sumActual : 0;
}, [dashboard, indicators]);
// Sector allocation breakdown
const sectorData = useMemo(() => {
const targetProjects = filteredProjects.length > 0 ? filteredProjects : projects;
if (targetProjects.length === 0) return [];
const counts: Record<string, number> = {};
targetProjects.forEach((p) => {
const label = getIndustrySectorLabel(p.sector);
counts[label] = (counts[label] || 0) + 1;
});
const colors = ['bg-emerald-500', 'bg-teal-500', 'bg-blue-500', 'bg-indigo-500', 'bg-purple-500', 'bg-amber-500'];
const totalProjects = targetProjects.length;
return Object.entries(counts).map(([name, count], index) => {
const percentage = Math.round((count / totalProjects) * 100);
const amount = portfolioValue > 0 ? Math.round((portfolioValue / totalProjects) * count) : 0;
return {
name,
count,
percentage,
amount,
color: colors[index % colors.length],
};
});
}, [projects, filteredProjects, portfolioValue]);
// Quarterly disbursements distribution
const quarterlyTrend = useMemo(() => {
if (!disbursements || disbursements.length === 0) {
return [
{ quarter: 'Q1', amount: 0, heightPct: 15 },
{ quarter: 'Q2', amount: 0, heightPct: 15 },
{ quarter: 'Q3', amount: 0, heightPct: 15 },
{ quarter: 'Q4', amount: 0, heightPct: 15 },
];
}
const qTotals: Record<string, number> = { Q1: 0, Q2: 0, Q3: 0, Q4: 0 };
disbursements.forEach((d) => {
const dateStr = d.createdAt || d.updatedAt;
if (dateStr) {
const month = new Date(dateStr).getMonth(); // 0-11
if (month <= 2) qTotals.Q1 += d.amount || 0;
else if (month <= 5) qTotals.Q2 += d.amount || 0;
else if (month <= 8) qTotals.Q3 += d.amount || 0;
else qTotals.Q4 += d.amount || 0;
} else {
qTotals.Q1 += (d.amount || 0) * 0.25;
qTotals.Q2 += (d.amount || 0) * 0.25;
qTotals.Q3 += (d.amount || 0) * 0.25;
qTotals.Q4 += (d.amount || 0) * 0.25;
}
});
const maxQ = Math.max(...Object.values(qTotals), 1);
return Object.entries(qTotals).map(([quarter, amount]) => ({
quarter,
amount,
heightPct: Math.max(15, Math.min(100, Math.round((amount / maxQ) * 100))),
}));
}, [disbursements]);
// Projects requiring management attention based on risk profile
const attentionProjects = useMemo(() => {
const targetProjects = filteredProjects.length > 0 ? filteredProjects : projects;
if (targetProjects.length === 0) return [];
// Map each project to its highest residual risk score
const projectMaxRisk: Record<string, number> = {};
risks.forEach((r) => {
const pid = String(r.projectId);
const score = (Number(r.likelihood) || 1) * (Number(r.impact) || 1);
if (!projectMaxRisk[pid] || score > projectMaxRisk[pid]) {
projectMaxRisk[pid] = score;
}
});
return targetProjects
.map((p) => {
const maxScore = projectMaxRisk[String(p.id)] || 0;
const isRejected = p.status === ApprovalStatus.Rejected;
const isPending = p.status === ApprovalStatus.Pending || p.status === ApprovalStatus.Review;
let riskLevel: 'RED' | 'AMBER' | 'GREEN' = 'GREEN';
let statusLabel = 'Active';
let variance = '0%';
let varianceType: 'negative' | 'positive' = 'positive';
if (isRejected || maxScore >= 15) {
riskLevel = 'RED';
statusLabel = 'At Risk';
variance = '-15%';
varianceType = 'negative';
} else if (isPending || maxScore >= 10) {
riskLevel = 'AMBER';
statusLabel = isPending ? 'Pending' : 'Review';
variance = '-5%';
varianceType = 'negative';
}
const pComps = projectComplianceMap.get(p.id) || [];
const unmetCount = pComps.filter(
(c) => Number(c.status) === ComplianceStatus.NotMet || Number(c.status) === 1
).length;
const isCompliant = unmetCount === 0;
const pBudgets = budgets.filter((b) => String(b.projectId) === String(p.id));
const pApprovedBudget = pBudgets
.filter((b) => Number(b.status) === ApprovalStatus.Approved || b.status === undefined)
.reduce((sum, b) => sum + (Number(b.amount) || 0), 0);
const projectBudgetAtRisk = !isCompliant || riskLevel === 'RED' || riskLevel === 'AMBER' ? pApprovedBudget : 0;
return {
id: p.id,
name: p.name,
programme: p.programmeName || `Programme ${p.programmeId}`,
status: statusLabel,
risk: riskLevel,
complianceStatus: isCompliant ? 'Compliant' : `Noncompliant (${unmetCount} Unmet)`,
isCompliant,
unmetCount,
budgetAtRisk: projectBudgetAtRisk,
financialVariance: variance,
varianceType,
maxScore,
};
})
.filter((p) => p.risk === 'RED' || p.risk === 'AMBER' || !p.isCompliant)
.sort((a, b) => b.budgetAtRisk - a.budgetAtRisk || b.maxScore - a.maxScore)
.slice(0, 8);
}, [projects, filteredProjects, risks, projectComplianceMap, budgets]);
const {
sortedData: sortedAttentionProjects,
sortColumn: projectSortCol,
sortDirection: projectSortDir,
handleSort: handleProjectSort,
} = useTableSort<AttentionProject>(attentionProjects, null, null, {
name: (p) => p.name,
programme: (p) => p.programme,
status: (p) => p.status,
risk: (p) => p.risk,
financialVariance: (p) => p.financialVariance,
});
return (
<div className="space-y-6 max-w-7xl mx-auto">
{/* Header Banner */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-gradient-to-r from-[#0d1322] via-[#0f172a] to-[#0d1322] p-6 sm:p-8 rounded-3xl border border-slate-800/80 shadow-2xl relative overflow-hidden">
<div className="absolute -right-10 -bottom-10 w-60 h-60 bg-blue-600/10 rounded-full blur-3xl pointer-events-none" />
<div className="space-y-2 relative z-10">
<div className="flex items-center gap-3">
<div className="h-12 w-12 rounded-2xl bg-blue-600/20 border border-blue-500/30 flex items-center justify-center text-blue-400 shadow-lg shadow-blue-500/10">
<LayoutDashboard className="w-6 h-6" />
</div>
<div>
<div className="flex items-center gap-2.5 flex-wrap">
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-white flex items-center gap-2.5">
Executive Portfolio Dashboard
</h1>
<HelpTooltip
title="Executive Dashboard Overview"
content="Macro-level command center showing aggregated national portfolio commitments, actual tranche disbursements, delivery pace, and critical project escalations."
size="sm"
placement="bottom"
/>
<span className="inline-flex items-center gap-1.5 text-[11px] font-semibold text-blue-400 bg-blue-500/10 px-2.5 py-0.5 rounded-full border border-blue-500/30">
<span className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse" />
Live Portfolio Feed
</span>
</div>
<p className="text-xs sm:text-sm text-slate-400 mt-1">
Real-time portfolio overview, disbursement tracking, risk heatmap, and projects requiring governance action.
</p>
</div>
</div>
</div>
<div className="flex items-center gap-2.5 shrink-0 relative z-10 self-start md:self-center">
{isFiltered && (
<button
type="button"
onClick={resetFilters}
className="inline-flex items-center gap-1.5 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 cursor-pointer shadow-xs"
title="Reset all filters to default"
>
<RotateCcw className="w-3.5 h-3.5" />
<span>Reset Filters</span>
</button>
)}
<button
type="button"
onClick={refetchAll}
disabled={isFetching}
className="inline-flex items-center gap-1.5 px-3.5 py-2 text-xs font-semibold text-slate-200 hover:text-white bg-slate-900/90 hover:bg-slate-800 border border-slate-700/80 rounded-xl transition cursor-pointer shadow-xs disabled:opacity-50"
title="Refresh dashboard metrics"
>
<RefreshCw className={`w-3.5 h-3.5 ${isFetching ? 'animate-spin text-blue-400' : 'text-slate-400'}`} />
<span>{isFetching ? 'Refreshing...' : 'Refresh'}</span>
</button>
</div>
</div>
{/* Filter Toolbar */}
<div className="bg-[#0f1523]/80 backdrop-blur-md p-4 rounded-2xl border border-slate-800/80 shadow-xl flex flex-wrap items-center gap-3">
<div className="flex items-center gap-2 text-xs font-semibold text-slate-300 pr-1">
<Filter className="w-3.5 h-3.5 text-blue-400" />
<span>Filters:</span>
{activeFilterCount > 0 && (
<span className="text-[10px] font-bold text-blue-300 bg-blue-500/20 border border-blue-500/40 px-2 py-0.5 rounded-full">
{activeFilterCount} Active
</span>
)}
</div>
{/* Programme Filter */}
<div className="flex items-center bg-[#090d16] border border-slate-700/80 hover:border-slate-600 rounded-xl px-3 py-1.5 text-xs transition shadow-inner">
<span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider mr-2 shrink-0">
Programme
</span>
<select
value={selectedProgramme}
onChange={(e) => setSelectedProgramme(e.target.value)}
className="bg-transparent text-xs font-medium text-slate-100 focus:outline-none cursor-pointer pr-1 [&>option]:bg-slate-900 [&>option]:text-white max-w-[200px] truncate"
>
<option value="All">All</option>
{programmes.map((pr) => (
<option key={pr.id} value={pr.id}>
{pr.name}
</option>
))}
</select>
<HelpTooltip
title="Programme Filter"
content="Isolates metrics and projects funded under a specific thematic programme mandate."
size="sm"
placement="bottom"
/>
</div>
{/* Sector Filter */}
<div className="flex items-center bg-[#090d16] border border-slate-700/80 hover:border-slate-600 rounded-xl px-3 py-1.5 text-xs transition shadow-inner">
<span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider mr-2 shrink-0">
Sector
</span>
<select
value={selectedSector}
onChange={(e) => setSelectedSector(e.target.value)}
className="bg-transparent text-xs font-medium text-slate-100 focus:outline-none cursor-pointer pr-1 [&>option]:bg-slate-900 [&>option]:text-white max-w-[170px] truncate"
>
<option value="All">All</option>
{sectorData.map((s) => (
<option key={s.name} value={s.name}>
{s.name}
</option>
))}
</select>
<HelpTooltip
title="Industry Sector Filter"
content="Filters analytics by designated economic industry sector (e.g. AgriTech, HealthTech, Renewable Energy)."
size="sm"
placement="bottom"
/>
</div>
{/* Financial Period Filter */}
<div className="flex items-center bg-[#090d16] border border-slate-700/80 hover:border-slate-600 rounded-xl px-3 py-1.5 text-xs transition shadow-inner">
<span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider mr-2 shrink-0">
Period
</span>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="bg-transparent text-xs font-medium text-slate-100 focus:outline-none cursor-pointer pr-1 [&>option]:bg-slate-900 [&>option]:text-white"
>
<option value="FY 2026/27">FY 2026/27</option>
<option value="FY 2025/26">FY 2025/26</option>
<option value="All-Time">All-Time Cumulative</option>
</select>
<HelpTooltip
title="Financial Period Filter"
content="Calculates disbursements and milestones according to the government fiscal year or cumulative life-of-programme."
size="sm"
placement="bottom"
/>
</div>
</div>
{/* 8 Headline Portfolio KPI Cards with Compliance & Budget At Risk */}
<div className="grid grid-cols-2 sm:grid-cols-4 xl:grid-cols-8 gap-3.5">
{/* Card 1: Portfolio Value */}
<div className="bg-[#0f1523]/80 p-5 rounded-2xl border border-slate-800/80 hover:border-blue-500/30 transition-all flex flex-col justify-between shadow-md group backdrop-blur-sm">
<div className="flex items-center justify-between text-slate-400">
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">Portfolio Value</span>
<HelpTooltip
title="Total Portfolio Value"
content="Aggregated statutory grant commitment amount approved across all active grant awards and contracts."
size="sm"
placement="top"
/>
</div>
<div className="p-1.5 rounded-lg bg-blue-500/10 text-blue-400 border border-blue-500/20 group-hover:scale-110 transition">
<Coins className="w-3.5 h-3.5" />
</div>
</div>
<div className="mt-3">
<p className="text-2xl font-bold text-white tracking-tight">{formatCompactZAR(portfolioValue)}</p>
<p className="text-[10px] text-slate-400 font-mono mt-1">{formatZAR(portfolioValue)}</p>
</div>
</div>
{/* Card 2: Active Awards */}
<div className="bg-[#0f1523]/80 p-5 rounded-2xl border border-slate-800/80 hover:border-blue-500/30 transition-all flex flex-col justify-between shadow-md group backdrop-blur-sm">
<div className="flex items-center justify-between text-slate-400">
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">Active Awards</span>
<HelpTooltip
title="Active Grant Awards"
content="Total legally executed statutory agreements currently undergoing active disbursement and implementation."
size="sm"
placement="top"
/>
</div>
<div className="p-1.5 rounded-lg bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 group-hover:scale-110 transition">
<Layers className="w-3.5 h-3.5" />
</div>
</div>
<div className="mt-3">
<p className="text-2xl font-bold text-white tracking-tight">{activeAwards}</p>
<p className="text-[10px] text-emerald-400 font-mono mt-1">
Across {portfolios.length} portfolio{portfolios.length === 1 ? '' : 's'}
</p>
</div>
</div>
{/* Card 3: Disbursed */}
<div className="bg-[#0f1523]/80 p-5 rounded-2xl border border-slate-800/80 hover:border-teal-500/30 transition-all flex flex-col justify-between shadow-md group backdrop-blur-sm">
<div className="flex items-center justify-between text-slate-400">
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">Disbursed</span>
<HelpTooltip
title="Disbursed Funds"
content="Actual funds paid out to implementing grantees following verified milestone proof-of-performance."
size="sm"
placement="top"
/>
</div>
<div className="p-1.5 rounded-lg bg-teal-500/10 text-teal-400 border border-teal-500/20 group-hover:scale-110 transition">
<TrendingUp className="w-3.5 h-3.5" />
</div>
</div>
<div className="mt-3">
<p className="text-2xl font-bold text-teal-400 tracking-tight">{formatCompactZAR(disbursed)}</p>
<p className="text-[10px] text-slate-400 font-mono mt-1">
{portfolioValue > 0 ? `${((disbursed / portfolioValue) * 100).toFixed(1)}% of commitments` : '0.0% of commitments'}
</p>
</div>
</div>
{/* Card 4: On Track */}
<div className="bg-[#0f1523]/80 p-5 rounded-2xl border border-slate-800/80 hover:border-emerald-500/30 transition-all flex flex-col justify-between shadow-md group backdrop-blur-sm">
<div className="flex items-center justify-between text-slate-400">
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">On Track</span>
<HelpTooltip
title="On-Track Progress Ratio"
content="Percentage of active projects meeting schedule deadlines and deliverable acceptance criteria without critical deviations."
size="sm"
placement="top"
/>
</div>
<div className="p-1.5 rounded-lg bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 group-hover:scale-110 transition">
<CheckCircle className="w-3.5 h-3.5" />
</div>
</div>
<div className="mt-3">
<p className="text-2xl font-bold text-emerald-400 tracking-tight">{onTrackPercentage}%</p>
<p className="text-[10px] text-slate-400 font-mono mt-1">Milestones verified</p>
</div>
</div>
{/* Card 5: At Risk */}
<div className="bg-[#0f1523]/80 p-5 rounded-2xl border border-slate-800/80 hover:border-amber-500/30 transition-all flex flex-col justify-between shadow-md group backdrop-blur-sm">
<div className="flex items-center justify-between text-slate-400">
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">At Risk</span>
<HelpTooltip
title="At-Risk Projects"
content="Count of projects with high residual risk scores (≥12), schedule slippage >30 days, or adverse financial variance."
size="sm"
placement="top"
/>
</div>
<div className="p-1.5 rounded-lg bg-amber-500/10 text-amber-400 border border-amber-500/20 group-hover:scale-110 transition">
<AlertTriangle className="w-3.5 h-3.5" />
</div>
</div>
<div className="mt-3">
<p className="text-2xl font-bold text-amber-400 tracking-tight">{atRiskCount}</p>
<p className="text-[10px] text-rose-400 font-mono mt-1">Requires PM action</p>
</div>
</div>
{/* Card 6: Beneficiaries Reached */}
<div className="bg-[#0f1523]/80 p-5 rounded-2xl border border-slate-800/80 hover:border-indigo-500/30 transition-all flex flex-col justify-between shadow-md group backdrop-blur-sm">
<div className="flex items-center justify-between text-slate-400">
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">Beneficiaries</span>
<HelpTooltip
title="Beneficiaries Impacted"
content="Cumulative count of direct beneficiaries reached through projects, verified against national M&E indicator targets."
size="sm"
placement="top"
/>
</div>
<div className="p-1.5 rounded-lg bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 group-hover:scale-110 transition">
<Users className="w-3.5 h-3.5" />
</div>
</div>
<div className="mt-3">
<p className="text-2xl font-bold text-white tracking-tight">
{beneficiariesReached >= 1000 ? `${(beneficiariesReached / 1000).toFixed(0)}k` : beneficiariesReached}
</p>
<p className="text-[10px] text-slate-400 font-mono mt-1">Directly impacted</p>
</div>
</div>
{/* Card 7: Budget At Risk */}
<div className="bg-[#0f1523]/80 p-5 rounded-2xl border border-slate-800/80 hover:border-rose-500/40 transition-all flex flex-col justify-between shadow-md group backdrop-blur-sm">
<div className="flex items-center justify-between text-slate-400">
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">Budget At Risk</span>
<HelpTooltip
title="Total Budgeted Capital At Risk"
content="Aggregated approved budget across all projects currently flagged as noncompliant or carrying unmitigated high risks."
size="sm"
placement="top"
/>
</div>
<div className="p-1.5 rounded-lg bg-rose-500/10 text-rose-400 border border-rose-500/20 group-hover:scale-110 transition">
<AlertTriangle className="w-3.5 h-3.5" />
</div>
</div>
<div className="mt-3">
<p className="text-2xl font-bold text-rose-400 tracking-tight">{formatCompactZAR(totalBudgetAtRisk)}</p>
<p className="text-[10px] text-slate-400 font-mono mt-1">{formatZAR(totalBudgetAtRisk)}</p>
</div>
</div>
{/* Card 8: Compliance Health */}
<div className="bg-[#0f1523]/80 p-5 rounded-2xl border border-slate-800/80 hover:border-emerald-500/40 transition-all flex flex-col justify-between shadow-md group backdrop-blur-sm">
<div className="flex items-center justify-between text-slate-400">
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">Compliance</span>
<HelpTooltip
title="Portfolio Compliance Health"
content="Percentage of active projects meeting 100% statutory compliance and beneficiary outcome criteria."
size="sm"
placement="top"
/>
</div>
<div className="p-1.5 rounded-lg bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 group-hover:scale-110 transition">
<ShieldCheck className="w-3.5 h-3.5" />
</div>
</div>
<div className="mt-3">
<p className="text-2xl font-bold text-emerald-400 tracking-tight">{complianceRate}%</p>
<p className="text-[10px] text-slate-400 font-mono mt-1">100% statutory gate</p>
</div>
</div>
</div>
{/* Visual Models Section (Sector Breakdown & Disbursements) */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Panel 1: Funding by Sector */}
<div className="bg-[#0f1523]/80 p-5 rounded-2xl border border-slate-800/80 flex flex-col justify-between shadow-lg backdrop-blur-sm">
<div>
<div className="flex items-center justify-between border-b border-slate-800/80 pb-3">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold text-white">Funding by Sector</h3>
<HelpTooltip
title="Sector Funding Distribution"
content="Breakdown of active grant commitments distributed across designated industrial and socio-economic sectors."
size="sm"
placement="bottom"
/>
</div>
<span className="text-[11px] font-mono text-blue-400 bg-blue-500/10 px-2 py-0.5 rounded-md border border-blue-500/30">
Live Portfolio
</span>
</div>
{/* Sector Donut Chart */}
<div className="py-6 flex items-center justify-center">
<div className="relative w-36 h-36 rounded-full border-8 border-slate-800 flex items-center justify-center">
<div className="w-24 h-24 rounded-full border-4 border-blue-500 flex flex-col items-center justify-center bg-[#090d16] shadow-inner">
<span className="text-xl font-bold text-white">{sectorData.length}</span>
<span className="text-[9px] uppercase tracking-wider text-slate-400">
{sectorData.length === 1 ? 'Sector' : 'Sectors'}
</span>
</div>
</div>
</div>
{/* Sector Progress Bars */}
<div className="space-y-2.5">
{sectorData.length === 0 ? (
<p className="text-xs text-center text-slate-500 py-3">No active project sectors assigned yet.</p>
) : (
sectorData.map((s) => (
<div key={s.name} className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-slate-300 font-medium truncate">{s.name} ({s.count})</span>
<span className="font-semibold text-white">{s.percentage}%</span>
</div>
<div className="w-full h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div className={`h-full ${s.color} rounded-full`} style={{ width: `${s.percentage}%` }}></div>
</div>
</div>
))
)}
</div>
</div>
<div className="pt-4 border-t border-slate-800/80 mt-4 text-[11px] text-slate-400 flex justify-between">
<span>Primary Focus: {sectorData[0]?.name || 'All Sectors'}</span>
<Link to="/governance?tab=portfolios" className="text-blue-400 font-medium hover:underline flex items-center gap-1">
View Portfolios <ArrowUpRight className="w-3 h-3" />
</Link>
</div>
</div>
{/* Panel 2: Disbursement Distribution */}
<div className="bg-[#0f1523]/80 p-5 rounded-2xl border border-slate-800/80 flex flex-col justify-between shadow-lg backdrop-blur-sm">
<div>
<div className="flex items-center justify-between border-b border-slate-800/80 pb-3">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold text-white">Disbursement Distribution</h3>
<HelpTooltip
title="Disbursement Timeline & Cadence"
content="Compares statutory multi-year grant tranche commitments against verified payouts released across calendar quarters."
size="sm"
placement="bottom"
/>
</div>
<span className="text-[11px] font-mono text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-md border border-emerald-500/30 font-semibold">
{disbursements.length} Tranche{disbursements.length === 1 ? '' : 's'}
</span>
</div>
<p className="text-[11px] text-slate-400 my-2">
Quarterly release distribution across active portfolios and milestone verifications.
</p>
{/* Quarterly Disbursement Trend Chart */}
<div className="h-44 flex items-end gap-3 pt-6 pb-2 px-2 border-b border-slate-800/80">
{quarterlyTrend.map((q) => (
<div key={q.quarter} className="flex-1 flex flex-col items-center gap-1.5 h-full justify-end">
<span className="text-[10px] font-mono text-slate-400">{q.quarter}</span>
<div
className="w-full bg-slate-800 rounded-t-md"
style={{ height: `${Math.max(10, q.heightPct * 0.7)}%` }}
title={`Commitment: ${formatCompactZAR(q.amount)}`}
></div>
<div
className="w-full bg-gradient-to-t from-blue-600 to-indigo-500 rounded-t-md shadow-xs shadow-blue-500/20"
style={{ height: `${q.heightPct}%` }}
title={`Disbursed: ${formatCompactZAR(q.amount)}`}
></div>
</div>
))}
</div>
<div className="flex items-center justify-between text-[11px] text-slate-400 pt-3">
<div className="flex items-center gap-2">
<span className="h-2 w-3 bg-slate-800 rounded-xs"></span>
<span>Commitment Allocation</span>
</div>
<div className="flex items-center gap-2">
<span className="h-2 w-3 bg-blue-500 rounded-xs"></span>
<span>Actual Disbursed</span>
</div>
</div>
</div>
<div className="pt-4 border-t border-slate-800/80 text-[11px] text-slate-400 flex justify-between items-center">
<span>Total Disbursed: {formatCompactZAR(disbursed)}</span>
<Link to="/grants" className="text-blue-400 font-medium hover:underline flex items-center gap-1">
Financials <ArrowUpRight className="w-3 h-3" />
</Link>
</div>
</div>
</div>
{/* Panel 3: Portfolio Risk Heatmap & Matrix */}
<RiskHeatmapMatrix
risks={filteredRisks}
projects={projects}
title="Portfolio Risk Heatmap & Matrix"
subtitle="Aggregated ISO 31000 distribution of project risks by Likelihood (1–5) and Impact (1–5)"
/>
{/* Projects Requiring Attention Table */}
<div className="bg-[#0f1523]/80 rounded-2xl border border-slate-800/80 overflow-hidden shadow-lg backdrop-blur-sm">
<div className="p-5 border-b border-slate-800/80 flex flex-col sm:flex-row sm:items-center justify-between gap-2 bg-[#0d121f]/90">
<div>
<div className="flex items-center gap-2">
<h3 className="text-base font-semibold text-white">Projects Flagged At Risk & Noncompliant</h3>
<HelpTooltip
title="Exception Escalations"
content="Identifies projects breaching risk thresholds, schedule deadlines, or financial variances (>10%) requiring executive steering committee action."
size="sm"
placement="bottom"
/>
</div>
<p className="text-xs text-slate-400 mt-0.5">
Prioritized by risk severity, delivery delay, or financial variance threshold breach.
</p>
</div>
<Link
to="/projects"
className="text-xs font-semibold text-blue-400 bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 px-3.5 py-1.5 rounded-xl transition self-start sm:self-auto shadow-xs"
>
Open Project Directory &rarr;
</Link>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="bg-[#0d121f]/95 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold text-[11px]">
<SortableTh column="name" currentColumn={projectSortCol} direction={projectSortDir} onSort={handleProjectSort} className="py-3.5 px-5">
Project
</SortableTh>
<SortableTh column="programme" currentColumn={projectSortCol} direction={projectSortDir} onSort={handleProjectSort} className="py-3.5 px-4">
Programme
</SortableTh>
<SortableTh column="status" currentColumn={projectSortCol} direction={projectSortDir} onSort={handleProjectSort} className="py-3.5 px-4">
Status
</SortableTh>
<SortableTh column="complianceStatus" currentColumn={projectSortCol} direction={projectSortDir} onSort={handleProjectSort} className="py-3.5 px-4">
Compliance
</SortableTh>
<SortableTh column="risk" currentColumn={projectSortCol} direction={projectSortDir} onSort={handleProjectSort} align="center" className="py-3.5 px-4 text-center">
Risk
</SortableTh>
<SortableTh column="budgetAtRisk" currentColumn={projectSortCol} direction={projectSortDir} onSort={handleProjectSort} align="right" className="py-3.5 px-4 text-right">
Budget At Risk
</SortableTh>
<th className="py-3.5 px-5 text-right font-medium">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60">
{sortedAttentionProjects.length === 0 ? (
<tr>
<td colSpan={7} className="py-8 text-center text-xs text-slate-500">
No active projects currently flagged for executive attention. All projects are operating within target thresholds.
</td>
</tr>
) : (
sortedAttentionProjects.map((p) => (
<tr key={p.id} className="hover:bg-slate-800/40 transition-colors">
<td className="py-3.5 px-5">
<span className="font-semibold text-white block text-xs">{p.name}</span>
<span className="text-[10px] text-slate-400 font-mono">ID: {p.id}</span>
</td>
<td className="py-3.5 px-4 text-slate-300 font-medium">{p.programme}</td>
<td className="py-3.5 px-4">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold border ${
p.status === 'At Risk'
? 'bg-rose-500/10 text-rose-400 border-rose-500/30'
: p.status === 'Pending'
? 'bg-amber-500/10 text-amber-400 border-amber-500/30'
: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30'
}`}
>
{p.status}
</span>
</td>
<td className="py-3.5 px-4">
{p.isCompliant ? (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-500/10 text-emerald-400 border border-emerald-500/30">
<CheckCircle className="w-3 h-3 text-emerald-400" />
<span>Compliant</span>
</span>
) : (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-semibold bg-rose-500/10 text-rose-400 border border-rose-500/30">
<AlertTriangle className="w-3 h-3 text-rose-400" />
<span>Noncompliant ({p.unmetCount} Unmet)</span>
</span>
)}
</td>
<td className="py-3.5 px-4 text-center">
<span
className={`inline-flex items-center justify-center font-bold px-2.5 py-0.5 rounded-md text-[10px] shadow-xs ${
p.risk === 'RED'
? 'bg-rose-600 text-white'
: p.risk === 'AMBER'
? 'bg-amber-500 text-white'
: 'bg-emerald-600 text-white'
}`}
>
{p.risk}
</span>
</td>
<td className="py-3.5 px-4 text-right font-mono font-bold text-xs">
<span className={p.budgetAtRisk > 0 ? 'text-rose-400' : 'text-emerald-400'}>
{formatZAR(p.budgetAtRisk)}
</span>
</td>
<td className="py-3.5 px-5 text-right">
<Link
to="/projects"
className="inline-flex items-center gap-1 text-xs font-semibold text-slate-200 hover:text-white bg-slate-900/80 hover:bg-slate-800 border border-slate-700/80 px-3 py-1.5 rounded-lg transition shadow-xs"
>
Review
<ArrowUpRight className="w-3.5 h-3.5" />
</Link>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
};