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

849 lines
47 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, 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<typeof indicatorSchema>;
type RiskFormValues = z.infer<typeof riskSchema>;
export const MonitoringPage: React.FC = () => {
const queryClient = useQueryClient();
const { success, error: toastError } = useToast();
const [activeTab, setActiveTab] = useState<'indicators' | 'evidence' | 'risks'>('evidence');
const fileInputRef = useRef<HTMLInputElement>(null);
// Modals & States
const [isIndicatorModalOpen, setIsIndicatorModalOpen] = useState(false);
const [isRiskModalOpen, setIsRiskModalOpen] = useState(false);
const [selectedIndicatorForActual, setSelectedIndicatorForActual] = useState<Indicator | null>(null);
const [actualInput, setActualInput] = useState<number>(0);
const [uploadEvidenceType, setUploadEvidenceType] = useState<number>(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<IndicatorFormValues>({
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<RiskFormValues>({
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<HTMLInputElement>) => {
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 (
<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>
<h2 className="text-2xl font-bold text-white tracking-tight flex items-center gap-2">
M&E, Evidence & Risk Management
</h2>
<p className="text-xs text-slate-400 mt-1">
Verification workflow: Evidence Upload & Review Queue, Indicator Actuals Recording, and Risk Register.
</p>
</div>
<div className="flex items-center gap-2">
{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"
>
<Plus className="w-4 h-4" />
Define Indicator
</button>
)}
{activeTab === 'risks' && (
<button
onClick={() => setIsRiskModalOpen(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"
>
<Plus className="w-4 h-4" />
Add Risk to Register
</button>
)}
</div>
</div>
{/* Tabs */}
<div className="flex gap-2 border-b border-slate-800/80 pb-3 overflow-x-auto">
<button
onClick={() => setActiveTab('evidence')}
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 ${activeTab === 'evidence'
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
}`}
>
<Paperclip className="w-4 h-4" />
Milestone Evidence & Review Queue
</button>
<button
onClick={() => setActiveTab('indicators')}
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 ${activeTab === 'indicators'
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
}`}
>
<TrendingUp className="w-4 h-4" />
Indicators & Actuals ({indicators.length})
</button>
<button
onClick={() => setActiveTab('risks')}
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 ${activeTab === 'risks'
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
}`}
>
<ShieldAlert className="w-4 h-4" />
Risk Register & Heatmap ({risks.length})
</button>
</div>
{/* Tab 1: Milestone Evidence Submission & Review (Exact Implementation of Figure 10) */}
{activeTab === 'evidence' && (
<div className="space-y-6">
{/* Wireframe Container from SRS Figure 10 */}
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
{/* Header Bar */}
<div className="bg-slate-900/90 text-white px-6 py-4 flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-slate-800/80">
<span className="text-xs font-mono font-semibold tracking-wider text-blue-400 uppercase">
RECIPIENT PORTAL &bull; Project XYZ &bull; Milestone 3: Prototype Delivery
</span>
<span className="text-[11px] text-slate-400 font-mono">Milestone Evidence Submission</span>
</div>
{/* Workflow Progress Steps */}
<div className="p-6 border-b border-slate-800/80 bg-slate-900/40">
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2 text-center text-xs font-semibold">
<div className="py-2.5 px-3 rounded-xl bg-slate-800/60 text-slate-400 border border-slate-700/50">Planned</div>
<div className="py-2.5 px-3 rounded-xl bg-slate-800/60 text-slate-400 border border-slate-700/50">In Progress</div>
<div className="py-2.5 px-3 rounded-xl bg-blue-600 text-white shadow-lg shadow-blue-600/30 ring-2 ring-blue-500/40">
Submitted
</div>
<div className="py-2.5 px-3 rounded-xl bg-amber-500/15 text-amber-300 border border-amber-500/30">
Under Review
</div>
<div className="py-2.5 px-3 rounded-xl bg-slate-900/40 text-slate-500 border border-dashed border-slate-800">
Verified & Paid
</div>
</div>
<div className="mt-4 flex items-center justify-between text-xs">
<div className="flex items-center gap-2 text-slate-300">
<span className="font-semibold">Current status:</span>
<span className="font-mono text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20 font-bold">
SUBMITTED — awaiting Programme Manager review
</span>
</div>
</div>
</div>
{/* Upload Evidence & Attached Evidence Panels (Matching Figure 10) */}
<div className="grid grid-cols-1 md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-slate-800/80">
{/* Upload Box */}
<div className="p-6 space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-white">Upload Evidence</h4>
<div className="flex items-center gap-1.5">
<label className="text-[11px] font-medium text-slate-400">Type:</label>
<select
value={uploadEvidenceType}
onChange={(e) => setUploadEvidenceType(Number(e.target.value))}
className="text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl px-2.5 py-1.5 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 max-w-[180px] [&>option]:bg-slate-900 [&>option]:text-white"
>
{EVIDENCE_TYPE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
</div>
<p className="text-xs text-slate-400">
Drag & drop or browse files. Accepted formats: PDF, JPG, DOCX, XLSX (Max 25MB).
</p>
<div
onClick={() => 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"
>
<Upload className="w-8 h-8 text-slate-400" />
<div>
<span className="text-xs font-bold text-white block">Click to Browse or Drag Files Here</span>
<span className="text-[10px] text-slate-500">Automated SHA-256 hash stamp applied on intake</span>
</div>
<button
type="button"
className="px-4 py-1.5 text-xs font-semibold text-white bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded-xl shadow-xs transition"
>
Browse Files
</button>
</div>
<input
type="file"
ref={fileInputRef}
onChange={handleFileUpload}
className="hidden"
accept=".pdf,.jpg,.jpeg,.png,.docx,.xlsx,.zip"
/>
</div>
{/* Attached Evidence List */}
<div className="p-6 space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-white">Attached Evidence (v2)</h4>
<span className="text-[11px] text-slate-400 font-mono">{evidenceList.length} files loaded</span>
</div>
<div className="space-y-2.5">
{evidenceList.map((ev) => (
<div
key={ev.id}
className="p-3 rounded-xl border border-slate-800/80 bg-slate-900/60 flex items-center justify-between gap-3 text-xs"
>
<div className="flex items-center gap-2.5 min-w-0">
<FileText className="w-4 h-4 text-blue-400 shrink-0" />
<div className="min-w-0">
<span className="font-semibold text-slate-200 block truncate font-mono">
{ev.fileName || `evidence_file_${ev.id}.pdf`}
</span>
<span className="text-[10px] text-slate-400 font-mono">
{ev.fileSize || '3.2 MB'} &bull; v{ev.version} &bull; {getEvidenceTypeLabel(ev.type)}
</span>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold bg-amber-500/10 text-amber-400 border border-amber-500/20">
Under Review
</span>
{ev.documentUrl && (
<a
href={ev.documentUrl}
target="_blank"
rel="noopener noreferrer"
className="text-slate-400 hover:text-white p-1 transition"
title="Download evidence file"
>
<Download className="w-3.5 h-3.5" />
</a>
)}
</div>
</div>
))}
</div>
</div>
</div>
{/* Programme Manager Review Section (Figure 10) */}
<div className="p-6 border-t border-slate-800/80 bg-slate-900/40 space-y-4">
<h4 className="text-sm font-bold text-white">Programme Manager Review</h4>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
Reviewer comment / rejection reason
</label>
<input
type="text"
value={reviewerComment}
onChange={(e) => 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"
/>
</div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-2">
<div className="flex items-center gap-3">
<button
onClick={() =>
reviewEvidenceMutation.mutate({
id: evidenceList[0]?.id || 1201,
status: ApprovalStatus.Rejected,
})
}
disabled={reviewEvidenceMutation.isPending}
className="px-4 py-2 text-xs font-semibold text-white bg-rose-600/90 hover:bg-rose-600 rounded-xl shadow-lg shadow-rose-600/20 transition"
>
Reject / Rework
</button>
<button
onClick={() =>
reviewEvidenceMutation.mutate({
id: evidenceList[0]?.id || 1201,
status: ApprovalStatus.Approved,
})
}
disabled={reviewEvidenceMutation.isPending}
className="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"
>
Approve & Verify
</button>
</div>
<p className="text-[11px] text-slate-400 italic">
Approval auto-triggers payment eligibility check.
</p>
</div>
</div>
</div>
</div>
)}
{/* 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>
</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>
</div>
</div>
)}
{/* Tab 3: Risk Register (M09) */}
{activeTab === 'risks' && (
<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">Risk Title & Description</th>
<th className="py-3.5 px-4">Project ID</th>
<th className="py-3.5 px-4 text-center">Likelihood (1–5)</th>
<th className="py-3.5 px-4 text-center">Impact (1–5)</th>
<th className="py-3.5 px-4 text-center">Residual Score</th>
<th className="py-3.5 px-4">Risk Owner</th>
<th className="py-3.5 px-5">Mitigation Control</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{loadingRisks ? (
<tr>
<td colSpan={7} className="py-8 text-center text-slate-500">
Loading risk register...
</td>
</tr>
) : (
risks.map((rk) => {
const score = rk.likelihood * rk.impact;
const color = getRiskScoreColor(rk.likelihood, rk.impact);
return (
<tr key={rk.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">{rk.name}</span>
<span className="text-[11px] text-slate-400 line-clamp-1">{rk.description}</span>
</td>
<td className="py-3.5 px-4 text-slate-400 font-mono">Project #{rk.projectId}</td>
<td className="py-3.5 px-4 text-center font-mono font-bold text-slate-200">{rk.likelihood}</td>
<td className="py-3.5 px-4 text-center font-mono font-bold text-slate-200">{rk.impact}</td>
<td className="py-3.5 px-4 text-center">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-bold ${color.badge}`}>
{score} / 25 ({color.level})
</span>
</td>
<td className="py-3.5 px-4 text-slate-300 font-medium">{getUserEmail(rk.owner)}</td>
<td className="py-3.5 px-5 text-slate-400 text-[11px]">
{rk.mitigationAction || 'Standard internal audit controls applied.'}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
)}
{/* Modal: Define Indicator */}
<Modal
isOpen={isIndicatorModalOpen}
onClose={() => setIsIndicatorModalOpen(false)}
title="Define M&E Indicator"
subtitle="Logical Framework & Theory of Change metric"
>
<form onSubmit={indicatorForm.handleSubmit((data) => indicatorMutation.mutate(data as any))} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Project *</label>
<select
{...indicatorForm.register('projectId')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition [&>option]:bg-slate-900 [&>option]:text-white"
>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Indicator Statement *</label>
<input
type="text"
{...indicatorForm.register('name')}
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"
placeholder="e.g. Smallholder farmers adopting precision drone analytics"
/>
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Unit of Measure *</label>
<select
{...indicatorForm.register('unitOfMeasure')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition [&>option]:bg-slate-900 [&>option]:text-white"
>
{UNIT_OF_MEASURE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Baseline Value *</label>
<input
type="number"
{...indicatorForm.register('baselineAmount')}
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"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Target Value *</label>
<input
type="number"
{...indicatorForm.register('targetAmount')}
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"
/>
</div>
</div>
<div className="pt-3 border-t border-slate-800/80 flex justify-end gap-2">
<button
type="button"
onClick={() => setIsIndicatorModalOpen(false)}
className="px-4 py-2 text-xs font-medium text-slate-300 bg-slate-800/80 hover:bg-slate-700 rounded-xl transition"
>
Cancel
</button>
<button
type="submit"
disabled={indicatorMutation.isPending}
className="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"
>
{indicatorMutation.isPending ? 'Saving...' : 'Define Indicator'}
</button>
</div>
</form>
</Modal>
{/* Modal: Record Actual Amount */}
<Modal
isOpen={!!selectedIndicatorForActual}
onClose={() => setSelectedIndicatorForActual(null)}
title="Record Indicator Actual Value"
subtitle={selectedIndicatorForActual?.name || ''}
>
<div className="space-y-4">
<div className="p-3.5 bg-slate-900/60 border border-slate-800/80 rounded-xl text-xs flex justify-between text-slate-300">
<span>Baseline: <strong className="text-white">{selectedIndicatorForActual?.baselineAmount}</strong></span>
<span>Target: <strong className="text-white">{selectedIndicatorForActual?.targetAmount}</strong></span>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Actual Value to Record *</label>
<input
type="number"
value={actualInput}
onChange={(e) => 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"
/>
</div>
<div className="pt-3 border-t border-slate-800/80 flex justify-end gap-2">
<button
type="button"
onClick={() => setSelectedIndicatorForActual(null)}
className="px-4 py-2 text-xs font-medium text-slate-300 bg-slate-800/80 hover:bg-slate-700 rounded-xl transition"
>
Cancel
</button>
<button
type="button"
onClick={() =>
selectedIndicatorForActual &&
actualMutation.mutate({
id: selectedIndicatorForActual.id,
amount: actualInput,
})
}
disabled={actualMutation.isPending}
className="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"
>
{actualMutation.isPending ? 'Recording...' : 'Submit Actual Value'}
</button>
</div>
</div>
</Modal>
{/* Modal: Add Risk */}
<Modal
isOpen={isRiskModalOpen}
onClose={() => setIsRiskModalOpen(false)}
title="Add Risk to Risk Register"
subtitle="Identify and score project delivery risks"
>
<form onSubmit={riskForm.handleSubmit((data) => riskMutation.mutate(data as any))} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Associated Project *</label>
<select
{...riskForm.register('projectId')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition [&>option]:bg-slate-900 [&>option]:text-white"
>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Risk Title *</label>
<input
type="text"
{...riskForm.register('name')}
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"
placeholder="e.g. Foreign Exchange Currency Volatility"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Likelihood (1–5) *</label>
<select
{...riskForm.register('likelihood')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition [&>option]:bg-slate-900 [&>option]:text-white"
>
{RISK_LIKELIHOOD_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Impact (1–5) *</label>
<select
{...riskForm.register('impact')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition [&>option]:bg-slate-900 [&>option]:text-white"
>
{RISK_IMPACT_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Risk Description *</label>
<textarea
rows={2}
{...riskForm.register('description')}
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"
/>
</div>
<div className="pt-3 border-t border-slate-800/80 flex justify-end gap-2">
<button
type="button"
onClick={() => setIsRiskModalOpen(false)}
className="px-4 py-2 text-xs font-medium text-slate-300 bg-slate-800/80 hover:bg-slate-700 rounded-xl transition"
>
Cancel
</button>
<button
type="submit"
disabled={riskMutation.isPending}
className="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"
>
{riskMutation.isPending ? 'Logging...' : 'Log Risk'}
</button>
</div>
</form>
</Modal>
</div>
);
};