Removed settings page mock data and replaced with api calls

This commit is contained in:
2026-09-27 10:58:08 +02:00
parent 6ab01830f3
commit fb27cc8c92
10 changed files with 1067 additions and 557 deletions
@@ -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<Indicator | null>(null);
const [actualInput, setActualInput] = useState<number>(0);
// Indicator Filter States
const [indicatorSearchTerm, setIndicatorSearchTerm] = useState('');
const [indicatorProjectFilter, setIndicatorProjectFilter] = useState<string | number | 'all'>('all');
const [indicatorUnitFilter, setIndicatorUnitFilter] = useState<string | number | 'all'>('all');
const [indicatorStatusFilter, setIndicatorStatusFilter] = useState<string>('all');
// Risk Filter States
const [riskSearchTerm, setRiskSearchTerm] = useState('');
const [riskProjectFilter, setRiskProjectFilter] = useState<string | number | 'all'>('all');
const [riskSeverityFilter, setRiskSeverityFilter] = useState<string>('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<Indicator>[] = [
{ 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<Risk>[] = [
{ 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 (
<div className="space-y-6 max-w-7xl mx-auto">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-[#0f1523]/80 backdrop-blur-md p-6 rounded-2xl border border-slate-800/80 shadow-xl">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-[#0d121f]/90 p-5 rounded-2xl border border-slate-800/80 backdrop-blur-md shadow-xl">
<div>
<h2 className="text-2xl font-bold text-white tracking-tight flex items-center gap-2">
<span>M&E, Evidence & Risk Management</span>
<div className="flex items-center gap-2">
<h2 className="text-xl font-bold text-white tracking-tight">
M&amp;E, Evidence &amp; Risk Management
</h2>
<HelpTooltip
title="Monitoring & Evaluation Hub"
content="Track performance indicators, verify tangible deliverable evidence against statutory milestones, and maintain proactive risk mitigation plans."
placement="right"
/>
</h2>
</div>
<p className="text-xs text-slate-400 mt-1">
Verification workflow: Evidence Upload & Review Queue, Indicator Actuals Recording, and Risk Register.
Verification workflow: Evidence Upload &amp; Review Queue, Indicator Actuals Recording, and Risk Register.
</p>
</div>
@@ -286,16 +397,16 @@ export const MonitoringPage: React.FC = () => {
{activeTab === 'contracts' && (
<button
onClick={() => setIsCreateContractModalOpen(true)}
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition cursor-pointer"
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-md shadow-blue-500/20 transition cursor-pointer"
>
<Plus className="w-4 h-4" />
Create Contract & Upload PDF
Create Contract &amp; Upload PDF
</button>
)}
{activeTab === 'indicators' && (
<button
onClick={() => setIsIndicatorModalOpen(true)}
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition cursor-pointer"
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-md shadow-blue-500/20 transition cursor-pointer"
>
<Plus className="w-4 h-4" />
Define Indicator
@@ -304,7 +415,7 @@ export const MonitoringPage: React.FC = () => {
{activeTab === 'risks' && (
<button
onClick={openCreateRisk}
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition cursor-pointer"
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-md shadow-blue-500/20 transition cursor-pointer"
>
<Plus className="w-4 h-4" />
Add Risk to Register
@@ -405,63 +516,196 @@ export const MonitoringPage: React.FC = () => {
{/* Tab 2: Indicators Table (M08) */}
{activeTab === 'indicators' && (
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
<th className="py-3.5 px-5">Indicator Name</th>
<th className="py-3.5 px-4">Project ID</th>
<th className="py-3.5 px-4">Unit of Measure</th>
<th className="py-3.5 px-4 text-right">Baseline</th>
<th className="py-3.5 px-4 text-right">Target</th>
<th className="py-3.5 px-4 text-right">Actual Recorded</th>
<th className="py-3.5 px-4">Status</th>
<th className="py-3.5 px-5 text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{loadingIndicators ? (
<tr>
<td colSpan={8} className="py-8 text-center text-slate-500">
Loading indicators...
</td>
<div className="space-y-4">
{/* Indicators Search and Filter Toolbar */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 bg-[#0d121f]/90 p-4 rounded-2xl border border-slate-800/80 backdrop-blur-sm">
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 flex-1 flex-wrap">
{/* Search */}
<div className="relative flex-1 min-w-[200px] max-w-md">
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-3" />
<input
type="text"
placeholder="Search indicators, statement, project ID..."
value={indicatorSearchTerm}
onChange={(e) => setIndicatorSearchTerm(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"
/>
</div>
{/* Project Filter */}
<div className="flex items-center gap-2">
<label className="text-xs text-slate-400 whitespace-nowrap font-medium">Project:</label>
<select
value={indicatorProjectFilter}
onChange={(e) => setIndicatorProjectFilter(e.target.value === 'all' ? 'all' : Number(e.target.value))}
className="px-3 py-2 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl text-slate-200 focus:outline-none focus:ring-1 focus:ring-blue-500 [&>option]:bg-slate-900"
>
<option value="all">All Projects ({projects.length})</option>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name} (#{p.id})
</option>
))}
</select>
</div>
{/* Status Filter */}
<div className="flex items-center gap-2">
<label className="text-xs text-slate-400 whitespace-nowrap flex items-center gap-1 font-medium">
<Filter className="w-3.5 h-3.5 text-slate-400" />
Status:
</label>
<select
value={indicatorStatusFilter}
onChange={(e) => setIndicatorStatusFilter(e.target.value)}
className="px-3 py-2 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl text-slate-200 focus:outline-none focus:ring-1 focus:ring-blue-500 [&>option]:bg-slate-900"
>
<option value="all">All Statuses</option>
<option value="green">≥90% Target Achieved</option>
<option value="amber">70–89% On Track</option>
<option value="red">&lt;70% Underperforming</option>
<option value="pending">Pending Actuals</option>
</select>
</div>
{/* Unit Filter */}
<div className="flex items-center gap-2">
<label className="text-xs text-slate-400 whitespace-nowrap font-medium">Unit:</label>
<select
value={indicatorUnitFilter}
onChange={(e) => setIndicatorUnitFilter(e.target.value === 'all' ? 'all' : Number(e.target.value))}
className="px-3 py-2 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl text-slate-200 focus:outline-none focus:ring-1 focus:ring-blue-500 [&>option]:bg-slate-900"
>
<option value="all">All Units</option>
{UNIT_OF_MEASURE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
{(indicatorSearchTerm || indicatorProjectFilter !== 'all' || indicatorStatusFilter !== 'all' || indicatorUnitFilter !== 'all') && (
<button
type="button"
onClick={() => {
setIndicatorSearchTerm('');
setIndicatorProjectFilter('all');
setIndicatorStatusFilter('all');
setIndicatorUnitFilter('all');
}}
className="inline-flex items-center gap-1 text-[11px] font-semibold text-slate-400 hover:text-white px-2.5 py-2 bg-slate-800 hover:bg-slate-700 rounded-xl transition cursor-pointer"
>
<RotateCcw className="w-3 h-3" />
<span>Reset</span>
</button>
)}
</div>
{/* Action Buttons: Refresh & Export */}
<div className="flex items-center gap-2 self-end lg:self-center">
<button
type="button"
onClick={() => {
refetchIndicators();
success('Refreshed', 'Latest performance metrics loaded from API.');
}}
disabled={isFetchingIndicators}
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 indicators from API"
>
<RefreshCw className={`w-3.5 h-3.5 text-blue-400 ${isFetchingIndicators ? 'animate-spin' : ''}`} />
<span>Refresh</span>
</button>
<ExportButton
title="M&E Performance Indicators"
subtitle="Baseline and target metrics tracking for monitoring and evaluation"
filename={`pfm-indicators-${new Date().toISOString().split('T')[0]}`}
columns={indicatorExportColumns}
data={filteredIndicators}
/>
</div>
</div>
{/* Indicators Table Card */}
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
<div className="p-4 border-b border-slate-800/80 flex items-center justify-between">
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-blue-400" />
Performance Indicators &amp; Actuals Ledger
</h3>
<span className="text-xs text-slate-400 font-mono">
Showing {filteredIndicators.length} of {indicators.length} indicators
</span>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
<th className="py-3.5 px-5">Indicator Name</th>
<th className="py-3.5 px-4">Project ID</th>
<th className="py-3.5 px-4">Unit of Measure</th>
<th className="py-3.5 px-4 text-right">Baseline</th>
<th className="py-3.5 px-4 text-right">Target</th>
<th className="py-3.5 px-4 text-right">Actual Recorded</th>
<th className="py-3.5 px-4">Status</th>
<th className="py-3.5 px-5 text-right">Action</th>
</tr>
) : (
indicators.map((ind) => {
const statusObj = getIndicatorStatus(ind.actualAmount, ind.targetAmount);
return (
<tr key={ind.id} className="hover:bg-slate-800/40 transition-colors">
<td className="py-3.5 px-5 font-semibold text-white">{ind.name}</td>
<td className="py-3.5 px-4 text-slate-400 font-mono">Project #{ind.projectId}</td>
<td className="py-3.5 px-4 text-slate-300">{getUnitOfMeasureLabel(ind.unitOfMeasure)}</td>
<td className="py-3.5 px-4 text-right font-mono text-slate-400">{ind.baselineAmount}</td>
<td className="py-3.5 px-4 text-right font-mono font-bold text-white">{ind.targetAmount}</td>
<td className="py-3.5 px-4 text-right font-mono font-bold text-emerald-400 text-xs">
{ind.actualAmount}
</td>
<td className="py-3.5 px-4">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${statusObj.badge}`}>
{statusObj.label}
</span>
</td>
<td className="py-3.5 px-5 text-right">
<button
onClick={() => {
setSelectedIndicatorForActual(ind);
setActualInput(ind.actualAmount);
}}
className="inline-flex items-center gap-1 text-xs font-semibold text-slate-300 hover:text-white bg-slate-800/80 hover:bg-slate-700 px-3 py-1.5 rounded-lg border border-slate-700/60 transition"
>
Record Actual
</button>
</td>
</tr>
);
})
)}
</tbody>
</table>
</thead>
<tbody className="divide-y divide-slate-800/50">
{loadingIndicators ? (
<tr>
<td colSpan={8} className="py-12 text-center text-slate-500">
<RefreshCw className="w-6 h-6 animate-spin mx-auto mb-2 text-blue-400" />
Loading project indicators from API...
</td>
</tr>
) : filteredIndicators.length === 0 ? (
<tr>
<td colSpan={8} className="py-12 text-center text-slate-500">
<TrendingUp className="w-8 h-8 text-slate-600 mx-auto mb-2" />
{indicatorSearchTerm || indicatorProjectFilter !== 'all' || indicatorStatusFilter !== 'all' || indicatorUnitFilter !== 'all'
? 'No indicators match your filter criteria.'
: 'No performance indicators defined yet. Click "Define Indicator" to begin.'}
</td>
</tr>
) : (
filteredIndicators.map((ind) => {
const statusObj = getIndicatorStatus(ind.actualAmount, ind.targetAmount);
return (
<tr key={ind.id} className="hover:bg-slate-800/40 transition-colors">
<td className="py-3.5 px-5 font-semibold text-white">{ind.name}</td>
<td className="py-3.5 px-4 text-slate-400 font-mono">Project #{ind.projectId}</td>
<td className="py-3.5 px-4 text-slate-300">{getUnitOfMeasureLabel(ind.unitOfMeasure)}</td>
<td className="py-3.5 px-4 text-right font-mono text-slate-400">{ind.baselineAmount}</td>
<td className="py-3.5 px-4 text-right font-mono font-bold text-white">{ind.targetAmount}</td>
<td className="py-3.5 px-4 text-right font-mono font-bold text-emerald-400 text-xs">
{ind.actualAmount}
</td>
<td className="py-3.5 px-4">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${statusObj.badge}`}>
{statusObj.label}
</span>
</td>
<td className="py-3.5 px-5 text-right">
<button
onClick={() => {
setSelectedIndicatorForActual(ind);
setActualInput(ind.actualAmount);
}}
className="inline-flex items-center gap-1 text-xs font-semibold text-slate-300 hover:text-white bg-slate-800/80 hover:bg-slate-700 px-3 py-1.5 rounded-lg border border-slate-700/60 transition cursor-pointer"
>
Record Actual
</button>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
</div>
)}
@@ -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 */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 bg-[#0d121f]/90 p-4 rounded-2xl border border-slate-800/80 backdrop-blur-sm">
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 flex-1 flex-wrap">
{/* Search */}
<div className="relative flex-1 min-w-[200px] max-w-md">
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-3" />
<input
type="text"
placeholder="Search risks by title, description, owner, mitigations..."
value={riskSearchTerm}
onChange={(e) => 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"
/>
</div>
{/* Project Filter */}
<div className="flex items-center gap-2">
<label className="text-xs text-slate-400 whitespace-nowrap font-medium">Project:</label>
<select
value={riskProjectFilter}
onChange={(e) => setRiskProjectFilter(e.target.value === 'all' ? 'all' : Number(e.target.value))}
className="px-3 py-2 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl text-slate-200 focus:outline-none focus:ring-1 focus:ring-blue-500 [&>option]:bg-slate-900"
>
<option value="all">All Projects ({projects.length})</option>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name} (#{p.id})
</option>
))}
</select>
</div>
{/* Severity Filter */}
<div className="flex items-center gap-2">
<label className="text-xs text-slate-400 whitespace-nowrap flex items-center gap-1 font-medium">
<Filter className="w-3.5 h-3.5 text-slate-400" />
Severity:
</label>
<select
value={riskSeverityFilter}
onChange={(e) => setRiskSeverityFilter(e.target.value)}
className="px-3 py-2 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl text-slate-200 focus:outline-none focus:ring-1 focus:ring-blue-500 [&>option]:bg-slate-900"
>
<option value="all">All Severities</option>
<option value="critical">Critical (16–25)</option>
<option value="high">High (12–15)</option>
<option value="medium">Medium (6–10)</option>
<option value="low">Low (1–5)</option>
</select>
</div>
{(riskSearchTerm || riskProjectFilter !== 'all' || riskSeverityFilter !== 'all') && (
<button
type="button"
onClick={() => {
setRiskSearchTerm('');
setRiskProjectFilter('all');
setRiskSeverityFilter('all');
}}
className="inline-flex items-center gap-1 text-[11px] font-semibold text-slate-400 hover:text-white px-2.5 py-2 bg-slate-800 hover:bg-slate-700 rounded-xl transition cursor-pointer"
>
<RotateCcw className="w-3 h-3" />
<span>Reset</span>
</button>
)}
</div>
{/* Action Buttons: Refresh & Export */}
<div className="flex items-center gap-2 self-end lg:self-center">
<button
type="button"
onClick={() => {
refetchRisks();
success('Refreshed', 'Latest risk evaluations and residual scores loaded.');
}}
disabled={isFetchingRisks}
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 risks from API"
>
<RefreshCw className={`w-3.5 h-3.5 text-blue-400 ${isFetchingRisks ? 'animate-spin' : ''}`} />
<span>Refresh</span>
</button>
<ExportButton
title="Project Risk Register & Heatmap"
subtitle="Itemized audit log of identified risks and mitigation controls"
filename={`pfm-risks-${new Date().toISOString().split('T')[0]}`}
columns={riskExportColumns}
data={filteredRisks}
/>
</div>
</div>
{/* Risk Register Table Card */}
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
<div className="p-4 border-b border-slate-800/80 flex items-center justify-between bg-slate-900/60">
<div className="p-4 border-b border-slate-800/80 flex items-center justify-between">
<div>
<h4 className="text-sm font-bold text-white">Project Risk Register</h4>
<h4 className="text-sm font-bold text-white flex items-center gap-2">
<ShieldAlert className="w-4 h-4 text-blue-400" />
Project Risk Register
</h4>
<p className="text-[11px] text-slate-400">Complete itemized audit log of identified risks and residual scores</p>
</div>
<button
onClick={openCreateRisk}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-xl transition shadow-md shadow-blue-500/20"
>
<Plus className="w-3.5 h-3.5" />
Add Risk
</button>
<span className="text-xs text-slate-400 font-mono">
Showing {filteredRisks.length} of {risks.length} risks
</span>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-xs border-collapse">
@@ -510,18 +846,22 @@ export const MonitoringPage: React.FC = () => {
<tbody className="divide-y divide-slate-800/50">
{loadingRisks ? (
<tr>
<td colSpan={8} className="py-8 text-center text-slate-500">
Loading risk register...
<td colSpan={8} className="py-12 text-center text-slate-500">
<RefreshCw className="w-6 h-6 animate-spin mx-auto mb-2 text-blue-400" />
Loading risk register from API...
</td>
</tr>
) : risks.length === 0 ? (
) : filteredRisks.length === 0 ? (
<tr>
<td colSpan={8} className="py-8 text-center text-slate-500">
No risks recorded in the register. Click &quot;Add Risk&quot; to log your first risk item.
<td colSpan={8} className="py-12 text-center text-slate-500">
<ShieldAlert className="w-8 h-8 text-slate-600 mx-auto mb-2" />
{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.'}
</td>
</tr>
) : (
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 = () => {
<td className="py-3.5 px-4 text-right">
<button
onClick={() => openEditRisk(rk)}
className="p-1.5 text-slate-400 hover:text-white hover:bg-slate-700/60 rounded-lg transition"
className="p-1.5 text-slate-400 hover:text-white hover:bg-slate-700/60 rounded-lg transition cursor-pointer"
title="Edit Risk"
>
<Edit2 className="w-3.5 h-3.5" />