Files
pfm/PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx
T
2026-09-11 12:13:25 +02:00

656 lines
38 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 } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import {
TrendingUp,
AlertTriangle,
Users,
Layers,
Coins,
CheckCircle,
ArrowUpRight,
Filter,
RefreshCw,
Clock,
ShieldAlert,
} from 'lucide-react';
import {
getDashboard,
getProjects,
getAllPortfolios,
getAllProgrammes,
getAwards,
getBudgets,
getDisbursements,
getAllRisks,
} from '../services/api';
import { formatCompactZAR, formatZAR, getIndustrySectorLabel } from '../lib/formatters';
import { ApprovalStatus } from '../types';
export const ExecutiveDashboard: React.FC = () => {
const [selectedProgramme, setSelectedProgramme] = useState('All');
const [selectedSector, setSelectedSector] = useState('All');
const [selectedPeriod, setSelectedPeriod] = useState('FY 2026/27');
// Real Queries from OpenAPI Endpoints
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 refetchAll = () => {
refetchDashboard();
};
// Headline metrics sourced directly from live API models
const portfolioValue = useMemo(() => {
if (dashboard?.portfolioValue && dashboard.portfolioValue > 0) {
return dashboard.portfolioValue;
}
const budgetTotal = budgets.reduce((sum, b) => sum + (b.amount || 0), 0);
if (budgetTotal > 0) return budgetTotal;
const awardsTotal = awards.reduce((sum, a) => sum + (a.amount || 0), 0);
return awardsTotal;
}, [dashboard, budgets, awards]);
const activeAwards = dashboard?.activeAwards ?? (awards.length || projects.length);
const disbursed = useMemo(() => {
if (dashboard?.disbursed && dashboard.disbursed > 0) {
return dashboard.disbursed;
}
return disbursements
.filter((d) => d.status === ApprovalStatus.Approved || (d.status as number) === 3)
.reduce((sum, d) => sum + (d.amount || 0), 0);
}, [dashboard, disbursements]);
const onTrackPercentage = useMemo(() => {
if (dashboard?.onTrackPercentage !== undefined && dashboard.onTrackPercentage > 0) {
return dashboard.onTrackPercentage;
}
if (projects.length === 0) return 0;
const activeOnTrack = projects.filter((p) => p.status === ApprovalStatus.Approved).length;
return Math.round((activeOnTrack / projects.length) * 100);
}, [dashboard, projects]);
const atRiskCount = useMemo(() => {
if (dashboard?.atRiskCount !== undefined) {
return dashboard.atRiskCount;
}
return risks.filter((r) => r.likelihood * r.impact >= 12).length;
}, [dashboard, risks]);
const beneficiariesReached = dashboard?.beneficiariesReached ?? 0;
// Real sector breakdown calculated dynamically from actual project records
const sectorData = useMemo(() => {
if (!projects || projects.length === 0) return [];
const counts: Record<string, number> = {};
projects.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 = projects.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, portfolioValue]);
// Risk heatmap matrix computed directly from registered risk records
const riskHeatmap = useMemo(() => {
const matrix = {
highImpHighLik: risks.filter((r) => r.impact >= 4 && r.likelihood >= 4).length,
highImpMedLik: risks.filter((r) => r.impact >= 4 && r.likelihood === 3).length,
highImpLowLik: risks.filter((r) => r.impact >= 4 && r.likelihood <= 2).length,
medImpHighLik: risks.filter((r) => r.impact === 3 && r.likelihood >= 4).length,
medImpMedLik: risks.filter((r) => r.impact === 3 && r.likelihood === 3).length,
medImpLowLik: risks.filter((r) => r.impact === 3 && r.likelihood <= 2).length,
lowImpHighLik: risks.filter((r) => r.impact <= 2 && r.likelihood >= 4).length,
lowImpMedLik: risks.filter((r) => r.impact <= 2 && r.likelihood === 3).length,
lowImpLowLik: risks.filter((r) => r.impact <= 2 && r.likelihood <= 2).length,
flaggedCount: risks.filter((r) => r.impact * r.likelihood >= 12).length,
};
return matrix;
}, [risks]);
// Quarterly disbursements distribution computed from real disbursement items
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]);
// Attention Projects derived directly from live project data
const attentionProjects = useMemo(() => {
if (!projects || projects.length === 0) return [];
return projects
.filter((p) => p.status === ApprovalStatus.Rejected || p.status === ApprovalStatus.Pending || p.status === ApprovalStatus.Review)
.slice(0, 5)
.map((p) => {
const isRejected = p.status === ApprovalStatus.Rejected;
const isPending = p.status === ApprovalStatus.Pending || p.status === ApprovalStatus.Review;
return {
id: p.id,
name: p.name,
programme: p.programmeName || `Programme ${p.programmeId}`,
status: isRejected ? 'At Risk' : isPending ? 'Pending' : 'Active',
risk: isRejected ? 'RED' : isPending ? 'AMBER' : 'GREEN',
financialVariance: isRejected ? '-15%' : '0%',
varianceType: isRejected ? 'negative' : 'positive',
};
});
}, [projects]);
return (
<div className="space-y-6 max-w-7xl mx-auto">
{/* Page Title & Filter Bar */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-[#0d121f]/90 p-5 rounded-2xl border border-slate-800/80 backdrop-blur-md shadow-xl">
<div>
<div className="flex items-center gap-2">
<h2 className="text-xl font-bold text-white tracking-tight">
Executive Portfolio Dashboard
</h2>
<span className="text-[11px] font-semibold text-blue-400 bg-blue-500/10 px-2.5 py-0.5 rounded-full border border-blue-500/30">
Live STS Feed
</span>
</div>
<p className="text-xs text-slate-400 mt-1">
Real-time portfolio overview, disbursement tracking, risk heatmap, and projects requiring governance action.
</p>
</div>
{/* Filters */}
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-1.5 text-xs text-slate-400">
<Filter className="w-3.5 h-3.5 text-slate-500" />
<span>Filter:</span>
</div>
<select
value={selectedProgramme}
onChange={(e) => setSelectedProgramme(e.target.value)}
className="text-xs font-medium bg-[#090d16] border border-slate-700/80 rounded-xl px-3 py-1.5 text-slate-200 focus:outline-none focus:ring-1 focus:ring-blue-500 transition cursor-pointer"
>
<option value="All">Programme: All</option>
{programmes.map((pr) => (
<option key={pr.id} value={pr.id}>
{pr.name}
</option>
))}
</select>
<select
value={selectedSector}
onChange={(e) => setSelectedSector(e.target.value)}
className="text-xs font-medium bg-[#090d16] border border-slate-700/80 rounded-xl px-3 py-1.5 text-slate-200 focus:outline-none focus:ring-1 focus:ring-blue-500 transition cursor-pointer"
>
<option value="All">Sector: All</option>
{sectorData.map((s) => (
<option key={s.name} value={s.name}>
{s.name}
</option>
))}
</select>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="text-xs font-medium bg-[#090d16] border border-slate-700/80 rounded-xl px-3 py-1.5 text-slate-200 focus:outline-none focus:ring-1 focus:ring-blue-500 transition cursor-pointer"
>
<option value="FY 2026/27">Period: FY 2026/27</option>
<option value="FY 2025/26">Period: FY 2025/26</option>
<option value="All-Time">All-Time Cumulative</option>
</select>
<button
onClick={refetchAll}
disabled={isFetching}
className="p-2 text-slate-400 hover:text-white bg-slate-900/80 hover:bg-slate-800 border border-slate-700/80 rounded-xl transition cursor-pointer shadow-xs"
title="Refresh dashboard metrics"
>
<RefreshCw className={`w-4 h-4 ${isFetching ? 'animate-spin text-blue-400' : ''}`} />
</button>
</div>
</div>
{/* 6 Headline Portfolio KPI Cards */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{/* 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">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">Portfolio Value</span>
<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">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">Active Awards</span>
<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">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">Disbursed</span>
<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">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">On Track</span>
<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">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">At Risk</span>
<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">
<span className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">Beneficiaries</span>
<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>
</div>
{/* Visual Models Section (Sector, Heatmap, Impact Trend) */}
<div className="grid grid-cols-1 lg:grid-cols-3 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">
<h3 className="text-sm font-semibold text-white">Funding by Sector</h3>
<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">
API Calculated
</span>
</div>
{/* Circular Graphic Simulation */}
<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" 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: Portfolio Risk Heatmap */}
<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">
<h3 className="text-sm font-semibold text-white">Portfolio Risk Heatmap</h3>
<span className="text-[11px] font-mono text-rose-400 bg-rose-500/10 px-2 py-0.5 rounded-md border border-rose-500/30 font-semibold">
{riskHeatmap.flaggedCount} Flagged
</span>
</div>
<p className="text-[11px] text-slate-400 my-2">
Matrix distribution of project risks by Likelihood (1–5) and Impact (1–5).
</p>
{/* 3x3 Heatmap Matrix with real counted risks */}
<div className="grid grid-cols-3 gap-2 my-4">
{/* Row 1 - High Impact */}
<div className="bg-amber-950/40 text-amber-200 p-2.5 rounded-xl text-center text-xs font-semibold border border-amber-800/40">
<span className="text-[10px] block text-amber-400 font-normal">Moderate</span>
{riskHeatmap.highImpLowLik} {riskHeatmap.highImpLowLik === 1 ? 'Risk' : 'Risks'}
</div>
<div className="bg-rose-950/50 text-rose-200 p-2.5 rounded-xl text-center text-xs font-semibold border border-rose-800/60">
<span className="text-[10px] block text-rose-400 font-normal">High</span>
{riskHeatmap.highImpMedLik} {riskHeatmap.highImpMedLik === 1 ? 'Risk' : 'Risks'}
</div>
<div className="bg-rose-900/80 text-rose-100 p-2.5 rounded-xl text-center text-xs font-bold border border-rose-500 shadow-md">
<span className="text-[10px] block text-rose-300 font-normal">Critical</span>
{riskHeatmap.highImpHighLik} {riskHeatmap.highImpHighLik === 1 ? 'Risk' : 'Risks'}
</div>
{/* Row 2 - Medium Impact */}
<div className="bg-emerald-950/40 text-emerald-200 p-2.5 rounded-xl text-center text-xs font-semibold border border-emerald-800/40">
<span className="text-[10px] block text-emerald-400 font-normal">Low</span>
{riskHeatmap.medImpLowLik} {riskHeatmap.medImpLowLik === 1 ? 'Risk' : 'Risks'}
</div>
<div className="bg-amber-950/40 text-amber-200 p-2.5 rounded-xl text-center text-xs font-semibold border border-amber-800/40">
<span className="text-[10px] block text-amber-400 font-normal">Moderate</span>
{riskHeatmap.medImpMedLik} {riskHeatmap.medImpMedLik === 1 ? 'Risk' : 'Risks'}
</div>
<div className="bg-rose-950/40 text-rose-200 p-2.5 rounded-xl text-center text-xs font-semibold border border-rose-800/50">
<span className="text-[10px] block text-rose-400 font-normal">High</span>
{riskHeatmap.medImpHighLik} {riskHeatmap.medImpHighLik === 1 ? 'Risk' : 'Risks'}
</div>
{/* Row 3 - Low Impact */}
<div className="bg-slate-900/80 text-slate-300 p-2.5 rounded-xl text-center text-xs font-semibold border border-slate-800">
<span className="text-[10px] block text-slate-500 font-normal">Trivial</span>
{riskHeatmap.lowImpLowLik} {riskHeatmap.lowImpLowLik === 1 ? 'Risk' : 'Risks'}
</div>
<div className="bg-emerald-950/40 text-emerald-200 p-2.5 rounded-xl text-center text-xs font-semibold border border-emerald-800/40">
<span className="text-[10px] block text-emerald-400 font-normal">Low</span>
{riskHeatmap.lowImpMedLik} {riskHeatmap.lowImpMedLik === 1 ? 'Risk' : 'Risks'}
</div>
<div className="bg-amber-950/40 text-amber-200 p-2.5 rounded-xl text-center text-xs font-semibold border border-amber-800/40">
<span className="text-[10px] block text-amber-400 font-normal">Moderate</span>
{riskHeatmap.lowImpHighLik} {riskHeatmap.lowImpHighLik === 1 ? 'Risk' : 'Risks'}
</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 Registered Risks: {risks.length}</span>
<Link to="/monitoring" className="text-blue-400 font-medium hover:underline flex items-center gap-1">
Risk Register <ArrowUpRight className="w-3 h-3" />
</Link>
</div>
</div>
{/* Panel 3: Funding-to-Impact Trend */}
<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">
<h3 className="text-sm font-semibold text-white">Disbursement Distribution</h3>
<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>
{/* Graphic Trend Visualization from Live Disbursements */}
<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>
{/* 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>
<h3 className="text-base font-semibold text-white">Projects Requiring Attention</h3>
<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]">
<th className="py-3.5 px-5">Project</th>
<th className="py-3.5 px-4">Programme</th>
<th className="py-3.5 px-4">Status</th>
<th className="py-3.5 px-4 text-center">Risk</th>
<th className="py-3.5 px-4 text-right">Financial Variance</th>
<th className="py-3.5 px-5 text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60">
{attentionProjects.length === 0 ? (
<tr>
<td colSpan={6} 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>
) : (
attentionProjects.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 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">
<span
className={`font-mono font-bold text-xs ${p.varianceType === 'negative' ? 'text-rose-400' : 'text-emerald-400'
}`}
>
{p.financialVariance}
</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>
);
};