import React, { useState, useRef, useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { ShieldAlert, FileCheck, Upload, Plus, CheckCircle2, XCircle, AlertTriangle, FileText, Clock, TrendingUp, Download, Paperclip, Check, Filter, Link as LinkIcon, Edit2, ChevronRight, } from 'lucide-react'; import { getAllIndicators, createIndicator, recordIndicatorActual, getAllRisks, createRisk, updateRisk, getProjects, getAllMilestones, } from '../services/api'; import { Modal } from '../components/ui/Modal'; import { HelpTooltip } from '../components/ui/HelpTooltip'; import { useToast } from '../components/ui/Toast'; import { ContractsManagement } from '../components/monitoring/ContractsManagement'; import { EvidenceReviewQueue } from '../components/monitoring/EvidenceReviewQueue'; import { RiskHeatmapMatrix } from '../components/ui/RiskHeatmapMatrix'; import { Indicator, Risk, ApprovalStatus, UnitOfMeasure, EvidenceType, RiskLikelihood, RiskImpact, CreateIndicatorRequest, CreateRiskRequest, } from '../types'; import { formatDate, getApprovalStatusBadge, getEvidenceTypeLabel, getUnitOfMeasureLabel, getRiskScoreColor, UNIT_OF_MEASURE_OPTIONS, RISK_LIKELIHOOD_OPTIONS, RISK_IMPACT_OPTIONS, EVIDENCE_TYPE_OPTIONS, getUserEmail, } from '../lib/formatters'; const indicatorSchema = z.object({ projectId: z.coerce.number().min(1, 'Select project'), name: z.string().min(5, 'Indicator definition required'), unitOfMeasure: z.coerce.number().min(1), baselineAmount: z.coerce.number().min(0), targetAmount: z.coerce.number().min(1, 'Target must be > 0'), }); const riskSchema = z.object({ projectId: z.coerce.number().min(1, 'Select project'), name: z.string().min(5, 'Risk title required'), description: z.string().min(10, 'Risk description required'), likelihood: z.coerce.number().min(1).max(5), impact: z.coerce.number().min(1).max(5), }); type IndicatorFormValues = z.infer; type RiskFormValues = z.infer; export const WORKFLOW_STAGES = [ { id: 'planned', label: 'Planned', step: 1, description: 'Milestone scope agreed and deliverables planned' }, { id: 'in_progress', label: 'In Progress', step: 2, description: 'Activity underway by recipient organisation' }, { id: 'submitted', label: 'Submitted', step: 3, description: 'Evidence and deliverables submitted by recipient' }, { id: 'under_review', label: 'Under Review', step: 4, description: 'Programme Manager verifying evidence against criteria' }, { id: 'verified_paid', label: 'Verified & Paid', step: 5, description: 'Milestone verified and disbursement authorized' }, ] as const; export const MonitoringPage: React.FC = () => { const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); const [searchParams, setSearchParams] = useSearchParams(); const tabParam = searchParams.get('tab'); const [activeTab, setActiveTab] = useState<'contracts' | 'evidence' | 'indicators' | 'risks'>( tabParam === 'evidence' || tabParam === 'indicators' || tabParam === 'risks' ? tabParam : 'contracts' ); const [isCreateContractModalOpen, setIsCreateContractModalOpen] = useState(false); useEffect(() => { const t = searchParams.get('tab'); if (t === 'evidence' || t === 'indicators' || t === 'risks' || t === 'contracts') { setActiveTab(t); } }, [searchParams]); const handleTabChange = (tab: 'contracts' | 'evidence' | 'indicators' | 'risks') => { setActiveTab(tab); setSearchParams((prev) => { const next = new URLSearchParams(prev); next.set('tab', tab); return next; }); }; // Modals & States const [isIndicatorModalOpen, setIsIndicatorModalOpen] = useState(false); const [isRiskModalOpen, setIsRiskModalOpen] = useState(false); const [editingRisk, setEditingRisk] = useState(null); const [selectedIndicatorForActual, setSelectedIndicatorForActual] = useState(null); const [actualInput, setActualInput] = useState(0); // Queries const { data: indicators = [], isLoading: loadingIndicators } = useQuery({ queryKey: ['indicators'], queryFn: () => getAllIndicators(), }); const { data: risks = [], isLoading: loadingRisks } = useQuery({ queryKey: ['risks'], queryFn: () => getAllRisks(), }); const { data: projects = [] } = useQuery({ queryKey: ['projects'], queryFn: () => getProjects(), }); const { data: milestones = [] } = useQuery({ queryKey: ['milestones-all'], queryFn: () => getAllMilestones(), }); // Forms const indicatorForm = useForm({ resolver: zodResolver(indicatorSchema) as any, defaultValues: { projectId: projects[0]?.id ? Number(projects[0].id) : 401, name: '', unitOfMeasure: UnitOfMeasure.Count, baselineAmount: 0, targetAmount: 100, }, }); const riskForm = useForm({ resolver: zodResolver(riskSchema) as any, defaultValues: { projectId: projects[0]?.id ? Number(projects[0].id) : 401, name: '', description: '', likelihood: RiskLikelihood.Medium, impact: RiskImpact.Moderate, }, }); // Mutations const indicatorMutation = useMutation({ mutationFn: (data: CreateIndicatorRequest) => createIndicator(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['indicators'] }); success('Indicator defined', 'Indicator added to project monitoring plan (M08)'); setIsIndicatorModalOpen(false); indicatorForm.reset(); }, onError: (err: unknown) => { toastError('Failed to create indicator', err); }, }); const actualMutation = useMutation({ mutationFn: ({ id, amount }: { id: string | number; amount: number }) => recordIndicatorActual(id, amount), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['indicators'] }); success('Actual value recorded', 'M&E variance and indicator status recalculated (BR-009)'); setSelectedIndicatorForActual(null); }, onError: (err: unknown) => { toastError('Failed to record actual', err); }, }); const openCreateRisk = () => { setEditingRisk(null); riskForm.reset({ projectId: projects[0]?.id ? Number(projects[0].id) : 401, name: '', description: '', likelihood: RiskLikelihood.Medium, impact: RiskImpact.Moderate, }); setIsRiskModalOpen(true); }; const openEditRisk = (rk: Risk) => { setEditingRisk(rk); riskForm.reset({ projectId: Number(rk.projectId) || (projects[0]?.id ? Number(projects[0].id) : 401), name: rk.name || '', description: rk.description || '', likelihood: (rk.likelihood as RiskLikelihood) ?? RiskLikelihood.Medium, impact: (rk.impact as RiskImpact) ?? RiskImpact.Moderate, }); setIsRiskModalOpen(true); }; const riskMutation = useMutation({ mutationFn: async (data: RiskFormValues) => { if (editingRisk) { return updateRisk({ riskId: editingRisk.id, name: data.name, description: data.description, likelihood: Number(data.likelihood), impact: Number(data.impact), }); } return createRisk({ projectId: Number(data.projectId), name: data.name, description: data.description, likelihood: Number(data.likelihood), impact: Number(data.impact), }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['risks'] }); queryClient.invalidateQueries({ queryKey: ['dashboard'] }); success( editingRisk ? 'Risk updated' : 'Risk registered', editingRisk ? 'Risk profile updated in project register' : 'Risk item added to project register' ); setIsRiskModalOpen(false); setEditingRisk(null); riskForm.reset(); }, onError: (err: unknown) => { toastError(editingRisk ? 'Failed to update risk' : 'Failed to log risk', err); }, }); // Performance indicator thresholds: // Green >= 90%, Amber 70-89%, Red < 70% const getIndicatorStatus = (actual: number, target: number) => { if (!target) return { label: 'Pending', badge: 'bg-slate-800/60 text-slate-400 border-slate-700/60' }; const pct = (actual / target) * 100; if (pct >= 90) return { label: '≥90%', badge: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20' }; if (pct >= 70) return { label: '70–89%', badge: 'bg-amber-500/10 text-amber-400 border-amber-500/20' }; return { label: '<70%', badge: 'bg-rose-500/10 text-rose-400 border-rose-500/20' }; }; return (
{/* Header */}

M&E, Evidence & Risk Management

Verification workflow: Evidence Upload & Review Queue, Indicator Actuals Recording, and Risk Register.

{activeTab === 'contracts' && ( )} {activeTab === 'indicators' && ( )} {activeTab === 'risks' && ( )}
{/* Tabs */}
{/* Tab 0: Contracts & Legal Documents with PDF Upload */} {activeTab === 'contracts' && ( setIsCreateContractModalOpen(false)} /> )} {/* Tab 1: Project & Milestone Evidence Review Hub */} {activeTab === 'evidence' && ( )} {/* Tab 2: Indicators Table (M08) */} {activeTab === 'indicators' && (
{loadingIndicators ? ( ) : ( indicators.map((ind) => { const statusObj = getIndicatorStatus(ind.actualAmount, ind.targetAmount); return ( ); }) )}
Indicator Name Project ID Unit of Measure Baseline Target Actual Recorded Status Action
Loading indicators...
{ind.name} Project #{ind.projectId} {getUnitOfMeasureLabel(ind.unitOfMeasure)} {ind.baselineAmount} {ind.targetAmount} {ind.actualAmount} {statusObj.label}
)} {/* Tab 3: Risk Register & Heatmap (M09) */} {activeTab === 'risks' && (
{/* Live Heatmap Matrix with cell click inspection */} openEditRisk(r)} title="Project Risk Heatmap & Matrix" subtitle="ISO 31000 standard matrix — click on any cell to filter and inspect risks or edit mitigation controls" /> {/* Risk Register Table */}

Project Risk Register

Complete itemized audit log of identified risks and residual scores

{loadingRisks ? ( ) : risks.length === 0 ? ( ) : ( risks.map((rk) => { const score = rk.likelihood * rk.impact; const color = getRiskScoreColor(rk.likelihood, rk.impact); return ( ); }) )}
Risk Title & Description Project ID Likelihood (1–5) Impact (1–5) Residual Score Risk Owner Mitigation Control Actions
Loading risk register...
No risks recorded in the register. Click "Add Risk" to log your first risk item.
{rk.name} {rk.description} Project #{rk.projectId} {rk.likelihood} {rk.impact} {score} / 25 ({color.level}) {getUserEmail(rk.owner)} {rk.mitigationAction || 'Standard internal audit controls applied.'}
)} {/* Modal: Define Indicator */} setIsIndicatorModalOpen(false)} title="Define M&E Indicator" subtitle="Logical Framework & Theory of Change metric" helpText="Quantitative or qualitative KPI tracked across reporting periods against target thresholds to assess project efficacy." >
indicatorMutation.mutate(data as any))} className="space-y-4">
{/* Modal: Record Actual Amount */} setSelectedIndicatorForActual(null)} title="Record Indicator Actual Value" subtitle={selectedIndicatorForActual?.name || ''} helpText="Logs a certified field or audited measurement against the target, recalculating the achievement percentage and RAG rating." >
Baseline: {selectedIndicatorForActual?.baselineAmount} Target: {selectedIndicatorForActual?.targetAmount}
setActualInput(Number(e.target.value))} className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-mono transition" />
{/* Modal: Add or Edit Risk */} { setIsRiskModalOpen(false); setEditingRisk(null); }} title={editingRisk ? 'Edit Project Risk' : 'Add Risk to Risk Register'} subtitle={editingRisk ? `Updating risk item #${editingRisk.id}` : 'Identify and score project delivery risks'} helpText="Risks are scored by Likelihood (1-5) x Impact (1-5) to plot on the 5x5 Heatmap. High severity risks flag alerts for portfolio governance." >
riskMutation.mutate(data as any))} className="space-y-4"> {!editingRisk && (
)}