Files
pfm/PostFundManagement.ConsoleUI/src/pages/MonitoringPage.tsx
T
2026-09-20 13:51:01 +02:00

890 lines
39 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, 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<typeof indicatorSchema>;
type RiskFormValues = z.infer<typeof riskSchema>;
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<Risk | null>(null);
const [selectedIndicatorForActual, setSelectedIndicatorForActual] = useState<Indicator | null>(null);
const [actualInput, setActualInput] = useState<number>(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<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', 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 (
<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">
<span>M&E, Evidence & Risk Management</span>
<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>
<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 === '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"
>
<Plus className="w-4 h-4" />
Create Contract & 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"
>
<Plus className="w-4 h-4" />
Define Indicator
</button>
)}
{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"
>
<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={() => handleTabChange('contracts')}
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 cursor-pointer ${
activeTab === 'contracts'
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
}`}
>
<FileText className="w-4 h-4" />
<span>Contracts & PDF Documents</span>
<HelpTooltip
title="Executed Contracts & Vault"
content="Digitally executed bilateral agreements, legal riders, and authoritative PDF repository."
size="sm"
placement="top"
/>
</button>
<button
onClick={() => handleTabChange('evidence')}
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 cursor-pointer ${
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'
}`}
>
<FileCheck className="w-4 h-4" />
<span>Evidence Review & Verification</span>
<HelpTooltip
title="Evidence Review & Verification"
content="Peer review, document inspection, and verification sign-offs for deliverables submitted against projects and milestones."
size="sm"
placement="top"
/>
</button>
<button
onClick={() => handleTabChange('indicators')}
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 cursor-pointer ${
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" />
<span>Indicators & Actuals ({indicators.length})</span>
<HelpTooltip
title="Quantitative Indicators"
content="Baseline vs. target metric progression with automated green/amber/red performance thresholds."
size="sm"
placement="top"
/>
</button>
<button
onClick={() => handleTabChange('risks')}
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 cursor-pointer ${
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" />
<span>Risk Register & Heatmap ({risks.length})</span>
<HelpTooltip
title="Risk Register & Heatmap"
content="5x5 severity matrix scoring likelihood against impact to safeguard public grant investments."
size="sm"
placement="top"
/>
</button>
</div>
{/* Tab 0: Contracts & Legal Documents with PDF Upload */}
{activeTab === 'contracts' && (
<ContractsManagement
isCreateModalOpenExternal={isCreateContractModalOpen}
onCloseCreateModalExternal={() => setIsCreateContractModalOpen(false)}
/>
)}
{/* Tab 1: Project & Milestone Evidence Review Hub */}
{activeTab === 'evidence' && (
<EvidenceReviewQueue
initialProjectId={searchParams.get('projectId') || 'all'}
initialMilestoneId={searchParams.get('milestoneId') || 'all'}
initialActivityId={searchParams.get('activityId') || 'all'}
initialUserId={searchParams.get('userId') || 'all'}
/>
)}
{/* 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 & Heatmap (M09) */}
{activeTab === 'risks' && (
<div className="space-y-6">
{/* Live Heatmap Matrix with cell click inspection */}
<RiskHeatmapMatrix
risks={risks}
projects={projects}
onSelectRisk={(r) => 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 */}
<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>
<h4 className="text-sm font-bold text-white">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>
</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">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>
<th className="py-3.5 px-4 text-right">Actions</th>
</tr>
</thead>
<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>
</tr>
) : risks.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>
</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>
<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"
title="Edit Risk"
>
<Edit2 className="w-3.5 h-3.5" />
</button>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Modal: Define Indicator */}
<Modal
isOpen={isIndicatorModalOpen}
onClose={() => 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."
>
<form onSubmit={indicatorForm.handleSubmit((data) => indicatorMutation.mutate(data as any))} className="space-y-4">
<div>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Project *</label>
<HelpTooltip
title="Target Project"
content="The project against which this indicator is tracked."
size="sm"
placement="right"
/>
</div>
<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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Indicator Statement *</label>
<HelpTooltip
title="Measurement Statement"
content="Precise description of the result or output being counted or verified."
size="sm"
placement="right"
/>
</div>
<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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Unit of Measure *</label>
<HelpTooltip
title="Metric Dimension"
content="Counting unit (e.g. Number, Percentage, Currency ZAR, Rating Score)."
size="sm"
placement="top"
/>
</div>
<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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Baseline Value *</label>
<HelpTooltip
title="Pre-grant Baseline"
content="Observed initial starting value prior to project intervention."
size="sm"
placement="top"
/>
</div>
<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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Target Value *</label>
<HelpTooltip
title="Target Benchmark"
content="Contracted goal to achieve before contract completion."
size="sm"
placement="top"
/>
</div>
<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 || ''}
helpText="Logs a certified field or audited measurement against the target, recalculating the achievement percentage and RAG rating."
>
<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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Actual Value to Record *</label>
<HelpTooltip
title="Monitored Measurement"
content="Current realized value for this reporting period. Automatically updates to ≥90%, 70–89%, or <70% performance threshold status."
size="sm"
placement="right"
/>
</div>
<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 or Edit Risk */}
<Modal
isOpen={isRiskModalOpen}
onClose={() => {
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."
>
<form onSubmit={riskForm.handleSubmit((data) => riskMutation.mutate(data as any))} className="space-y-4">
{!editingRisk && (
<div>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Associated Project *</label>
<HelpTooltip
title="Context Project"
content="The project directly exposed to this potential threat or operational hazard."
size="sm"
placement="right"
/>
</div>
<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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Risk Title *</label>
<HelpTooltip
title="Event Name"
content="Succinct identifier of the uncertainty (e.g. Key Personnel Departure, Import Tariff Hike)."
size="sm"
placement="right"
/>
</div>
<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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Likelihood (1–5) *</label>
<HelpTooltip
title="Occurrence Probability"
content="Rated 1 (Rare) through 5 (Almost Certain)."
size="sm"
placement="top"
/>
</div>
<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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Impact (1–5) *</label>
<HelpTooltip
title="Consequence Severity"
content="Rated 1 (Insignificant) through 5 (Catastrophic to schedule/budget)."
size="sm"
placement="top"
/>
</div>
<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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Risk Description *</label>
<HelpTooltip
title="Root Cause & Mitigation"
content="Detail underlying triggers, preventative actions, and contingency response steps."
size="sm"
placement="right"
/>
</div>
<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);
setEditingRisk(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="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 ? 'Saving...' : editingRisk ? 'Update Risk' : 'Log Risk'}
</button>
</div>
</form>
</Modal>
</div>
);
};