import React, { useState } 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 { Target, Plus, TrendingUp, X, RotateCcw, Check, Edit3, } from 'lucide-react'; import { Modal } from '../ui/Modal'; import { HelpTooltip } from '../ui/HelpTooltip'; import { useToast } from '../ui/Toast'; import { getProjectIndicators, createIndicator, recordIndicatorActual, } from '../../services/api'; import { Project, Indicator, UnitOfMeasure } from '../../types'; import { getUnitOfMeasureLabel, UNIT_OF_MEASURE_OPTIONS } from '../../lib/formatters'; const indicatorSchema = z.object({ name: z.string().min(5, 'Indicator definition required (min 5 chars)'), unitOfMeasure: z.coerce.number().min(1), baselineAmount: z.coerce.number().min(0, 'Baseline must be >= 0'), targetAmount: z.coerce.number().min(1, 'Target must be > 0'), }); type IndicatorFormValues = z.infer; interface ProjectIndicatorsModalProps { isOpen: boolean; onClose: () => void; project: Project | null; } export const ProjectIndicatorsModal: React.FC = ({ isOpen, onClose, project, }) => { const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); const [isFormOpen, setIsFormOpen] = useState(false); const [recordingActualForId, setRecordingActualForId] = useState(null); const [actualInput, setActualInput] = useState(0); const projectId = project?.id; const { data: indicators = [], isLoading, refetch, } = useQuery({ queryKey: ['indicators', projectId], queryFn: () => (projectId ? getProjectIndicators(projectId) : Promise.resolve([])), enabled: isOpen && !!projectId, }); const form = useForm({ resolver: zodResolver(indicatorSchema), defaultValues: { name: '', unitOfMeasure: UnitOfMeasure.Count, baselineAmount: 0, targetAmount: 100, }, }); const createMutation = useMutation({ mutationFn: async (data: IndicatorFormValues) => { if (!projectId) return; return createIndicator({ projectId: projectId, name: data.name, unitOfMeasure: Number(data.unitOfMeasure), baselineAmount: Number(data.baselineAmount), targetAmount: Number(data.targetAmount), }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['indicators', projectId] }); queryClient.invalidateQueries({ queryKey: ['indicators'] }); success('Indicator Created', 'M&E Key Performance Indicator added to project'); setIsFormOpen(false); form.reset(); }, onError: (err: unknown) => { toastError('Failed to create indicator', err); }, }); const actualMutation = useMutation({ mutationFn: async ({ indicatorId, actualAmount }: { indicatorId: string | number; actualAmount: number }) => { return recordIndicatorActual(indicatorId, actualAmount); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['indicators', projectId] }); queryClient.invalidateQueries({ queryKey: ['indicators'] }); success('Actual Value Logged', 'M&E progress measurement updated successfully'); setRecordingActualForId(null); }, onError: (err: unknown) => { toastError('Failed to record actual value', err); }, }); if (!project) return null; return ( { setIsFormOpen(false); setRecordingActualForId(null); onClose(); }} title={`M&E Performance Indicators: ${project.name}`} subtitle={`Project #${project.id} • Results & Impact Framework`} helpText="Quantitative indicators monitor baseline vs. target deliverables (e.g. jobs created, patents filed, rural SMMEs trained, renewable MW generated)." maxWidth="xl" >
{/* KPI Summary Card */}
Monitoring & Evaluation Targets
{indicators.length} Tracked performance metrics
{!isFormOpen && ( )}
{/* Create Indicator Form */} {isFormOpen && (
Define New Key Performance Indicator (KPI)
createMutation.mutate(data))} className="space-y-3">
{form.formState.errors.name && (

{form.formState.errors.name.message}

)}
{form.formState.errors.targetAmount && (

{form.formState.errors.targetAmount.message}

)}
)} {/* Indicators List */}
Performance Target Registry ({indicators.length})
{isLoading ? (
Loading indicators...
) : indicators.length === 0 ? (

No M&E indicators defined yet

Track quantitative impact metrics against established baselines.

) : (
{indicators.map((ind) => { const target = Number(ind.targetAmount) || 1; const actual = Number(ind.actualAmount) || 0; const baseline = Number(ind.baselineAmount) || 0; const pct = Math.round((actual / target) * 100); const isRecordingThis = recordingActualForId === ind.id; return (
{ind.name} {getUnitOfMeasureLabel(ind.unitOfMeasure)}
Indicator ID #{ind.id}
= 100 ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30' : pct >= 50 ? 'bg-blue-500/10 text-blue-400 border-blue-500/30' : 'bg-amber-500/10 text-amber-400 border-amber-500/30' }`} > {pct}% Achieved {!isRecordingThis ? ( ) : ( )}
{/* Progress Bar */}
= 100 ? 'bg-emerald-500' : pct >= 50 ? 'bg-gradient-to-r from-blue-500 to-indigo-500' : 'bg-amber-500' }`} style={{ width: `${Math.min(100, Math.max(0, pct))}%` }} />
Baseline: {baseline} Actual: {actual} Target: {target}
{/* Inline Record Actual Form */} {isRecordingThis && (
Update Actual Value:
setActualInput(Number(e.target.value))} className="w-24 px-2 py-1 text-xs border border-slate-700 rounded-lg text-white font-mono bg-[#090d16] focus:outline-none focus:ring-1 focus:ring-indigo-500" />
)}
); })}
)}
); };