import React, { useState, useRef } from 'react'; 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, } from 'lucide-react'; import { getAllIndicators, createIndicator, recordIndicatorActual, getAllEvidence, uploadEvidenceFile, updateEvidence, getAllRisks, createRisk, getProjects, getAllMilestones, } from '../services/api'; import { Modal } from '../components/ui/Modal'; import { useToast } from '../components/ui/Toast'; import { Indicator, Evidence, 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 MonitoringPage: React.FC = () => { const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); const [activeTab, setActiveTab] = useState<'indicators' | 'evidence' | 'risks'>('evidence'); const fileInputRef = useRef(null); // Modals & States const [isIndicatorModalOpen, setIsIndicatorModalOpen] = useState(false); const [isRiskModalOpen, setIsRiskModalOpen] = useState(false); const [selectedIndicatorForActual, setSelectedIndicatorForActual] = useState(null); const [actualInput, setActualInput] = useState(0); const [uploadEvidenceType, setUploadEvidenceType] = useState(EvidenceType.CompletionCertificate); // Figure 10 Reviewer commentary const [reviewerComment, setReviewerComment] = useState(''); // Queries const { data: indicators = [], isLoading: loadingIndicators } = useQuery({ queryKey: ['indicators'], queryFn: () => getAllIndicators(), }); const { data: evidenceList = [], isLoading: loadingEvidence } = useQuery({ queryKey: ['evidence'], queryFn: () => getAllEvidence(), }); 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', String(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', String(err)); }, }); const uploadMutation = useMutation({ mutationFn: (formData: FormData) => uploadEvidenceFile(formData), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['evidence'] }); success('Evidence uploaded', 'Supporting file registered for reviewer inspection (Figure 10 / M14)'); }, onError: (err: unknown) => { toastError('Upload failed', String(err)); }, }); const reviewEvidenceMutation = useMutation({ mutationFn: ({ id, status }: { id: string | number; status: ApprovalStatus }) => updateEvidence({ evidenceId: id, version: 2, evidenceType: EvidenceType.DeliverableProof, status, }), onSuccess: (_, variables) => { queryClient.invalidateQueries({ queryKey: ['evidence'] }); if (variables.status === ApprovalStatus.Approved) { success( 'Evidence Approved & Verified', 'Approval recorded. Auto-triggers payment eligibility check in Disbursements.' ); } else { success('Rework Requested', 'Evidence returned to recipient for correction.'); } setReviewerComment(''); }, onError: (err: unknown) => { toastError('Review update failed', String(err)); }, }); const riskMutation = useMutation({ mutationFn: (data: CreateRiskRequest) => createRisk(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['risks'] }); queryClient.invalidateQueries({ queryKey: ['dashboard'] }); success('Risk registered', 'Risk item added to project register'); setIsRiskModalOpen(false); riskForm.reset(); }, onError: (err: unknown) => { toastError('Failed to log risk', String(err)); }, }); const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const formData = new FormData(); formData.append('file', file); formData.append('projectId', '401'); formData.append('milestoneId', '803'); formData.append('evidenceType', String(uploadEvidenceType)); uploadMutation.mutate(formData); } }; // Indicator thresholds from SRS 9.6: // 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: 'Green (≥90%)', badge: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20' }; if (pct >= 70) return { label: 'Amber (70–89%)', badge: 'bg-amber-500/10 text-amber-400 border-amber-500/20' }; return { label: 'Red (<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 === 'indicators' && ( )} {activeTab === 'risks' && ( )}
{/* Tabs */}
{/* Tab 1: Milestone Evidence Submission & Review (Exact Implementation of Figure 10) */} {activeTab === 'evidence' && (
{/* Wireframe Container from SRS Figure 10 */}
{/* Header Bar */}
RECIPIENT PORTAL • Project XYZ • Milestone 3: Prototype Delivery Milestone Evidence Submission
{/* Workflow Progress Steps */}
Planned
In Progress
Submitted
Under Review
Verified & Paid
Current status: SUBMITTED — awaiting Programme Manager review
{/* Upload Evidence & Attached Evidence Panels (Matching Figure 10) */}
{/* Upload Box */}

Upload Evidence

Drag & drop or browse files. Accepted formats: PDF, JPG, DOCX, XLSX (Max 25MB).

fileInputRef.current?.click()} className="border-2 border-dashed border-slate-700/80 hover:border-blue-500/80 bg-slate-900/40 hover:bg-blue-500/5 rounded-2xl p-8 text-center cursor-pointer transition flex flex-col items-center justify-center space-y-3" >
Click to Browse or Drag Files Here Automated SHA-256 hash stamp applied on intake
{/* Attached Evidence List */}

Attached Evidence (v2)

{evidenceList.length} files loaded
{evidenceList.map((ev) => (
{ev.fileName || `evidence_file_${ev.id}.pdf`} {ev.fileSize || '3.2 MB'} • v{ev.version} • {getEvidenceTypeLabel(ev.type)}
Under Review {ev.documentUrl && ( )}
))}
{/* Programme Manager Review Section (Figure 10) */}

Programme Manager Review

setReviewerComment(e.target.value)} placeholder="Mandatory if requesting rework; optional for approvals..." 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 transition" />

Approval auto-triggers payment eligibility check.

)} {/* 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 (M09) */} {activeTab === 'risks' && (
{loadingRisks ? ( ) : ( 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
Loading risk register...
{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" >
indicatorMutation.mutate(data as any))} className="space-y-4">
{/* Modal: Record Actual Amount */} setSelectedIndicatorForActual(null)} title="Record Indicator Actual Value" subtitle={selectedIndicatorForActual?.name || ''} >
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 Risk */} setIsRiskModalOpen(false)} title="Add Risk to Risk Register" subtitle="Identify and score project delivery risks" >
riskMutation.mutate(data as any))} className="space-y-4">