Improved compliance monitoring

This commit is contained in:
2026-10-22 15:42:36 +02:00
parent 9835121fd9
commit 299d6ff4dc
27 changed files with 4245 additions and 149 deletions
@@ -15,6 +15,7 @@ import {
ShieldAlert,
RotateCcw,
LayoutDashboard,
ShieldCheck,
} from 'lucide-react';
import {
getDashboard,
@@ -26,9 +27,10 @@ import {
getDisbursements,
getAllRisks,
getAllIndicators,
getProjectComplianceItems,
} from '../services/api';
import { formatCompactZAR, formatZAR, getIndustrySectorLabel } from '../lib/formatters';
import { ApprovalStatus } from '../types';
import { ApprovalStatus, ComplianceStatus } from '../types';
import { RiskHeatmapMatrix } from '../components/ui/RiskHeatmapMatrix';
import { HelpTooltip } from '../components/ui/HelpTooltip';
import { useTableSort, SortableTh } from '../utils/tableSort';
@@ -39,6 +41,10 @@ interface AttentionProject {
programme: string;
status: string;
risk: string;
complianceStatus: string;
isCompliant: boolean;
unmetCount: number;
budgetAtRisk: number;
financialVariance: string;
varianceType: string;
maxScore: number;
@@ -108,6 +114,25 @@ export const ExecutiveDashboard: React.FC = () => {
queryFn: () => getAllIndicators(),
});
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();
@@ -217,6 +242,34 @@ export const ExecutiveDashboard: React.FC = () => {
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;
@@ -325,21 +378,38 @@ export const ExecutiveDashboard: React.FC = () => {
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')
.sort((a, b) => b.maxScore - a.maxScore)
.slice(0, 5);
}, [projects, filteredProjects, risks]);
.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,
@@ -498,8 +568,8 @@ export const ExecutiveDashboard: React.FC = () => {
</div>
</div>
{/* 6 Headline Portfolio KPI Cards */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{/* 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">
@@ -637,6 +707,50 @@ export const ExecutiveDashboard: React.FC = () => {
<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) */}
@@ -774,7 +888,7 @@ export const ExecutiveDashboard: React.FC = () => {
<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 Requiring Attention</h3>
<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."
@@ -807,11 +921,14 @@ export const ExecutiveDashboard: React.FC = () => {
<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="financialVariance" currentColumn={projectSortCol} direction={projectSortDir} onSort={handleProjectSort} align="right" className="py-3.5 px-4 text-right">
Financial Variance
<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>
@@ -819,7 +936,7 @@ export const ExecutiveDashboard: React.FC = () => {
<tbody className="divide-y divide-slate-800/60">
{sortedAttentionProjects.length === 0 ? (
<tr>
<td colSpan={6} className="py-8 text-center text-xs text-slate-500">
<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>
@@ -844,6 +961,19 @@ export const ExecutiveDashboard: React.FC = () => {
{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 ${
@@ -857,13 +987,9 @@ export const ExecutiveDashboard: React.FC = () => {
{p.risk}
</span>
</td>
<td className="py-3.5 px-4 text-right">
<span
className={`font-mono font-bold text-xs ${
p.varianceType === 'negative' ? 'text-rose-400' : 'text-emerald-400'
}`}
>
{p.financialVariance}
<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">