import React, { useState, useMemo } 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 { Users, Search, Plus, Edit2, Trash2, Filter, CheckCircle, AlertTriangle, FolderKanban, CheckSquare, ShieldCheck, RefreshCw, Sparkles, MapPin, FileCheck, Lock, } from 'lucide-react'; import { getProjects, getProjectBeneficiaries, getBeneficiary, createBeneficiary, updateBeneficiary, getBeneficiaryOutcomes, getProjectOutcomes, createOutcome, updateOutcome, getProjectComplianceItems, getComplianceItem, createComplianceItem, updateComplianceItem, } from '../services/api'; import { Modal } from '../components/ui/Modal'; import { useToast } from '../components/ui/Toast'; import { HelpTooltip } from '../components/ui/HelpTooltip'; import { ExportButton } from '../components/ui/ExportButton'; import { ExportColumn } from '../utils/exportUtils'; import { useTableSort, SortableTh } from '../utils/tableSort'; import { useRbac } from '../auth/rbac'; import { ApprovalStatusBadge, ComplianceStatusBadge } from '../components/ui/StatusBadge'; import { Beneficiary, ComplianceItem, Outcome, ApprovalStatus, ComplianceStatus } from '../types'; import { getApprovalStatusBadge, getComplianceStatusBadge, formatDate, APPROVAL_STATUS_OPTIONS, COMPLIANCE_STATUS_OPTIONS } from '../lib/formatters'; const beneficiarySchema = z.object({ name: z.string().min(2, 'Beneficiary name required'), category: z.string().min(2, 'Category required'), location: z.string().min(2, 'Location required'), targetCount: z.coerce.number().min(1, 'Target count must be at least 1'), reachedCount: z.coerce.number().min(0, 'Reached count must be 0 or more'), }); const complianceSchema = z.object({ title: z.string().min(3, 'Compliance title required'), requirements: z.string().min(5, 'Requirements detail required'), status: z.coerce.number().optional(), beneficiaryId: z.string().optional(), }); const outcomeSchema = z.object({ name: z.string().min(3, 'Outcome name required'), description: z.string().min(5, 'Description required'), status: z.coerce.number().optional(), beneficiaryId: z.string().optional(), }); type BeneficiaryFormValues = z.infer; type ComplianceFormValues = z.infer; type OutcomeFormValues = z.infer; export const BeneficiariesPage: React.FC = () => { const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); const { canAdd, canApprove, canExport, canApproveRecord, isMaker } = useRbac(); const [selectedProjectId, setSelectedProjectId] = useState('all'); const [activeTab, setActiveTab] = useState<'beneficiaries' | 'compliance' | 'outcomes'>('beneficiaries'); const [searchTerm, setSearchTerm] = useState(''); const [categoryFilter, setCategoryFilter] = useState('all'); const [compStatusFilter, setCompStatusFilter] = useState('all'); // Outcomes tab filters const [outcomeSearchTerm, setOutcomeSearchTerm] = useState(''); const [outcomeStatusFilter, setOutcomeStatusFilter] = useState('all'); const [outcomeBenFilter, setOutcomeBenFilter] = useState('all'); // Modal states const [isBenModalOpen, setIsBenModalOpen] = useState(false); const [editingBen, setEditingBen] = useState(null); const [isCompModalOpen, setIsCompModalOpen] = useState(false); const [editingComp, setEditingComp] = useState(null); const [compTargetProjectId, setCompTargetProjectId] = useState(''); const [selectedBenForDetail, setSelectedBenForDetail] = useState(null); const [isDetailModalOpen, setIsDetailModalOpen] = useState(false); const [isOutcomeModalOpen, setIsOutcomeModalOpen] = useState(false); const [editingOutcome, setEditingOutcome] = useState(null); // Queries const { data: projects = [], isLoading: loadingProjects } = useQuery({ queryKey: ['projects'], queryFn: () => getProjects(), }); // Query beneficiaries across selected project or all projects const { data: beneficiaries = [], isLoading: loadingBens, refetch: refetchBens, isFetching: fetchingBens, } = useQuery({ queryKey: ['beneficiaries', selectedProjectId, projects.map((p) => p.id).join(',')], queryFn: async (): Promise => { if (selectedProjectId !== 'all') { const bens = await getProjectBeneficiaries(selectedProjectId); return (bens || []).map((b) => ({ ...b, projectId: b.projectId || selectedProjectId })); } if (!projects || projects.length === 0) return []; const settled = await Promise.allSettled( projects.map(async (p) => { const bens = await getProjectBeneficiaries(p.id); return (bens || []).map((b) => ({ ...b, projectId: b.projectId || p.id })); }) ); const combined: Beneficiary[] = []; settled.forEach((res) => { if (res.status === 'fulfilled' && Array.isArray(res.value)) { combined.push(...res.value); } }); return combined; }, enabled: projects.length > 0, }); // Query compliance items const { data: complianceItems = [], isLoading: loadingComps, isError: isCompsError, refetch: refetchComps, isFetching: fetchingComps, } = useQuery({ queryKey: ['complianceItems', selectedProjectId, projects.map((p) => p.id).join(',')], queryFn: async (): Promise => { if (selectedProjectId !== 'all') { const items = await getProjectComplianceItems(selectedProjectId); return (items || []).map((ci) => ({ ...ci, projectId: ci.projectId || selectedProjectId, })); } if (!projects || projects.length === 0) return []; const settled = await Promise.allSettled( projects.map(async (p) => { const items = await getProjectComplianceItems(p.id); return (items || []).map((ci) => ({ ...ci, projectId: ci.projectId || p.id, })); }) ); const combined: ComplianceItem[] = []; settled.forEach((res) => { if (res.status === 'fulfilled' && Array.isArray(res.value)) { combined.push(...res.value); } }); return combined; }, enabled: projects.length > 0, }); // Query outcomes for selected beneficiary in inspector modal const { data: outcomes = [], refetch: refetchOutcomes } = useQuery({ queryKey: ['outcomes', selectedBenForDetail?.id], queryFn: () => (selectedBenForDetail?.id ? getBeneficiaryOutcomes(selectedBenForDetail.id) : Promise.resolve([])), enabled: !!selectedBenForDetail?.id, }); // Query all outcomes for the new Outcomes tab const { data: allOutcomes = [], isLoading: loadingOutcomesTab, refetch: refetchAllOutcomes, isFetching: fetchingOutcomesTab, } = useQuery({ queryKey: ['allOutcomes', selectedProjectId, beneficiaries.map((b) => b.id).join(','), projects.map((p) => p.id).join(',')], queryFn: async (): Promise<(Outcome & { beneficiaryName?: string; projectName?: string })[]> => { const projMap = new Map(); projects.forEach((p) => projMap.set(String(p.id), p.name || 'Project')); const benMap = new Map(); beneficiaries.forEach((b) => { benMap.set(String(b.id), b); }); let rawOutcomes: Outcome[] = []; if (selectedProjectId !== 'all') { try { rawOutcomes = await getProjectOutcomes(selectedProjectId); } catch { rawOutcomes = []; } if (rawOutcomes.length === 0 && beneficiaries.length > 0) { const settled = await Promise.allSettled(beneficiaries.map((b) => getBeneficiaryOutcomes(b.id))); settled.forEach((res) => { if (res.status === 'fulfilled' && Array.isArray(res.value)) { rawOutcomes.push(...res.value); } }); } } else { if (!projects || projects.length === 0) return []; const settled = await Promise.allSettled( projects.map(async (p) => { const list = await getProjectOutcomes(p.id); return list; }) ); settled.forEach((res) => { if (res.status === 'fulfilled' && Array.isArray(res.value)) { rawOutcomes.push(...res.value); } }); if (rawOutcomes.length === 0 && beneficiaries.length > 0) { const benSettled = await Promise.allSettled(beneficiaries.map((b) => getBeneficiaryOutcomes(b.id))); benSettled.forEach((res) => { if (res.status === 'fulfilled' && Array.isArray(res.value)) { rawOutcomes.push(...res.value); } }); } } // Deduplicate and enrich const seen = new Set(); const enriched: (Outcome & { beneficiaryName?: string; projectName?: string })[] = []; for (const o of rawOutcomes) { const idKey = String(o.id); if (seen.has(idKey)) continue; seen.add(idKey); const ben = benMap.get(String(o.beneficiaryId)); const benName = ben?.name || `Beneficiary #${o.beneficiaryId}`; const projName = ben?.projectId ? (projMap.get(String(ben.projectId)) || 'Project') : (selectedProjectId !== 'all' ? (projMap.get(selectedProjectId) || 'Project') : 'Project'); enriched.push({ ...o, beneficiaryName: benName, projectName: projName, }); } return enriched; }, enabled: projects.length > 0, }); // Forms const benForm = useForm({ resolver: zodResolver(beneficiarySchema), defaultValues: { name: '', category: 'Youth & Skills', location: 'National', targetCount: 100, reachedCount: 0 }, }); const compForm = useForm({ resolver: zodResolver(complianceSchema), defaultValues: { title: '', requirements: '', status: ApprovalStatus.Pending }, }); const outcomeForm = useForm({ resolver: zodResolver(outcomeSchema), defaultValues: { name: '', description: '', beneficiaryId: '' }, }); // Mutations const createBenMutation = useMutation({ mutationFn: (vals: BeneficiaryFormValues) => { const targetProjId = selectedProjectId !== 'all' ? selectedProjectId : projects[0]?.id; if (!targetProjId) throw new Error('No project selected'); return createBeneficiary({ projectId: targetProjId, name: vals.name, location: vals.location, category: vals.category, targetCount: vals.targetCount, reachedCount: vals.reachedCount, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['beneficiaries'] }); setIsBenModalOpen(false); benForm.reset(); success('Beneficiary Registered', 'New project beneficiary profile successfully created.'); }, onError: (err: unknown) => toastError('Registration Failed', err), }); const updateBenMutation = useMutation({ mutationFn: (vals: BeneficiaryFormValues) => { if (!editingBen) throw new Error('No beneficiary selected'); return updateBeneficiary({ beneficiaryId: editingBen.id, name: vals.name, location: vals.location, category: vals.category, targetCount: vals.targetCount, reachedCount: vals.reachedCount, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['beneficiaries'] }); setIsBenModalOpen(false); setEditingBen(null); benForm.reset(); success('Beneficiary Updated', 'Beneficiary details and targets updated.'); }, onError: (err: unknown) => toastError('Update Failed', err), }); const createCompMutation = useMutation({ mutationFn: (vals: ComplianceFormValues) => { const targetProjId = compTargetProjectId || (selectedProjectId !== 'all' ? selectedProjectId : projects[0]?.id); if (!targetProjId) throw new Error('No project selected'); return createComplianceItem({ projectId: targetProjId, title: vals.title, requirements: vals.requirements, status: vals.status ?? ComplianceStatus.NotMet, beneficiaryId: vals.beneficiaryId && vals.beneficiaryId !== 'none' ? Number(vals.beneficiaryId) : null, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['complianceItems'] }); setIsCompModalOpen(false); compForm.reset(); success('Compliance Requirement Logged', 'Compliance item registered for project beneficiaries.'); }, onError: (err: unknown) => toastError('Creation Failed', err), }); const updateCompMutation = useMutation({ mutationFn: (vals: ComplianceFormValues) => { if (!editingComp) throw new Error('No item selected'); if ( (vals.status === ComplianceStatus.Met || vals.status === ComplianceStatus.Exceeded) && isMaker(editingComp.createdBy) ) { throw new Error('Maker-Checker Violation: You created this compliance item and cannot approve it.'); } return updateComplianceItem({ complianceItemId: editingComp.id, title: vals.title, requirements: vals.requirements, status: vals.status ?? ComplianceStatus.NotMet, beneficiaryId: vals.beneficiaryId && vals.beneficiaryId !== 'none' ? Number(vals.beneficiaryId) : null, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['complianceItems'] }); setIsCompModalOpen(false); setEditingComp(null); compForm.reset(); success('Compliance Item Updated', 'Status and requirement record updated.'); }, onError: (err: unknown) => toastError('Update Failed', err), }); const createOutcomeMutation = useMutation({ mutationFn: (vals: OutcomeFormValues) => { const targetBenId = vals.beneficiaryId || selectedBenForDetail?.id; if (!targetBenId) throw new Error('No beneficiary selected'); return createOutcome({ beneficiaryId: targetBenId, name: vals.name, description: vals.description, status: vals.status ?? ComplianceStatus.NotMet, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['outcomes'] }); queryClient.invalidateQueries({ queryKey: ['allOutcomes'] }); queryClient.invalidateQueries({ queryKey: ['beneficiaryOutcomes'] }); refetchOutcomes(); refetchAllOutcomes(); setIsOutcomeModalOpen(false); setEditingOutcome(null); outcomeForm.reset(); success('Outcome Logged', 'Beneficiary outcome and impact metric registered.'); }, onError: (err: unknown) => toastError('Failed to Record Outcome', err), }); const updateOutcomeMutation = useMutation({ mutationFn: (vals: OutcomeFormValues) => { if (!editingOutcome) throw new Error('No outcome selected for update'); if ( (vals.status === ComplianceStatus.Met || vals.status === ComplianceStatus.Exceeded) && isMaker(editingOutcome.createdBy) ) { throw new Error('Maker-Checker Violation: You created this outcome record and cannot approve or certify fulfillment.'); } return updateOutcome({ outcomeId: editingOutcome.id, name: vals.name, description: vals.description, status: vals.status ?? ComplianceStatus.NotMet, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['outcomes'] }); queryClient.invalidateQueries({ queryKey: ['allOutcomes'] }); queryClient.invalidateQueries({ queryKey: ['beneficiaryOutcomes'] }); refetchOutcomes(); refetchAllOutcomes(); setIsOutcomeModalOpen(false); setEditingOutcome(null); outcomeForm.reset(); success('Outcome Updated', 'Beneficiary outcome and impact metric updated.'); }, onError: (err: unknown) => toastError('Failed to Update Outcome', err), }); // Filtered & Sorted Beneficiaries const filteredBens = useMemo(() => { return beneficiaries.filter((b) => { const matchesSearch = !searchTerm.trim() || (b.name && b.name.toLowerCase().includes(searchTerm.toLowerCase())) || (b.location && b.location.toLowerCase().includes(searchTerm.toLowerCase())); const matchesCat = categoryFilter === 'all' || b.category === categoryFilter; return matchesSearch && matchesCat; }); }, [beneficiaries, searchTerm, categoryFilter]); const { sortedData: sortedBens, sortColumn: benSortCol, sortDirection: benSortDir, handleSort: handleBenSort } = useTableSort(filteredBens); // Filtered & Sorted Compliance Items const filteredComps = useMemo(() => { return complianceItems.filter((c) => { const matchesSearch = !searchTerm.trim() || (c.title && c.title.toLowerCase().includes(searchTerm.toLowerCase())) || (c.requirements && c.requirements.toLowerCase().includes(searchTerm.toLowerCase())); const matchesStatus = compStatusFilter === 'all' || (compStatusFilter === 'approved' && c.status === ApprovalStatus.Approved) || (compStatusFilter === 'pending' && c.status !== ApprovalStatus.Approved); return matchesSearch && matchesStatus; }); }, [complianceItems, searchTerm, compStatusFilter]); const { sortedData: sortedComps, sortColumn: compSortCol, sortDirection: compSortDir, handleSort: handleCompSort } = useTableSort(filteredComps); // Filtered & Sorted Outcomes const filteredOutcomes = useMemo(() => { return allOutcomes.filter((o) => { const term = outcomeSearchTerm.toLowerCase().trim(); const matchesSearch = !term || (o.name && o.name.toLowerCase().includes(term)) || (o.description && o.description.toLowerCase().includes(term)) || (o.beneficiaryName && o.beneficiaryName.toLowerCase().includes(term)); const matchesStatus = outcomeStatusFilter === 'all' || String(o.status) === outcomeStatusFilter || (outcomeStatusFilter === 'met' && (Number(o.status) === ComplianceStatus.Met || Number(o.status) === ComplianceStatus.Exceeded)) || (outcomeStatusFilter === 'notMet' && Number(o.status) === ComplianceStatus.NotMet); const matchesBen = outcomeBenFilter === 'all' || String(o.beneficiaryId) === outcomeBenFilter; return matchesSearch && matchesStatus && matchesBen; }); }, [allOutcomes, outcomeSearchTerm, outcomeStatusFilter, outcomeBenFilter]); const { sortedData: sortedOutcomes, sortColumn: outcomeSortCol, sortDirection: outcomeSortDir, handleSort: handleOutcomeSort, } = useTableSort(filteredOutcomes); // Metrics const totalTarget = beneficiaries.reduce((acc, b) => acc + (b.targetCount || 0), 0); const totalReached = beneficiaries.reduce((acc, b) => acc + (b.reachedCount || 0), 0); const reachPercentage = totalTarget > 0 ? Math.min(100, Math.round((totalReached / totalTarget) * 100)) : 0; const verifiedCompCount = complianceItems.filter((c) => c.status === ApprovalStatus.Approved).length; const metOutcomesCount = useMemo( () => allOutcomes.filter((o) => Number(o.status) === ComplianceStatus.Met || Number(o.status) === ComplianceStatus.Exceeded).length, [allOutcomes] ); // Exports const benExportColumns: ExportColumn[] = [ { header: 'Beneficiary Name', key: 'name' }, { header: 'Category', key: 'category' }, { header: 'Location', key: 'location' }, { header: 'Target Population', key: 'targetCount' }, { header: 'Reached Population', key: 'reachedCount' }, ]; const compExportColumns: ExportColumn[] = [ { header: 'Project', key: 'projectId', format: (val) => projects.find((p) => String(p.id) === String(val))?.name || 'Project', }, { header: 'Compliance Requirement', key: 'title' }, { header: 'Requirements Detail', key: 'requirements' }, { header: 'Status', key: 'status', format: (val) => (val === ApprovalStatus.Approved ? 'Approved' : 'Pending Review') }, ]; const outcomeExportColumns: ExportColumn[] = [ { header: 'Outcome / Metric', key: 'name' }, { header: 'Beneficiary Cohort', key: 'beneficiaryName' }, { header: 'Project', key: 'projectName' }, { header: 'Impact Description', key: 'description' }, { header: 'Fulfillment Status', key: 'status', format: (val) => { const s = Number(val); if (s === ComplianceStatus.Met) return 'Met'; if (s === ComplianceStatus.Exceeded) return 'Exceeded'; return 'Not Met'; }, }, { header: 'Date Recorded', key: 'createdAt', format: (val) => (val ? formatDate(val) : '') }, ]; const handleOpenEditBen = async (b: Beneficiary) => { try { const fresh = await getBeneficiary(b.id); setEditingBen(fresh); benForm.reset({ name: fresh.name || '', category: fresh.category || 'General', location: fresh.location || '', targetCount: fresh.targetCount || 0, reachedCount: fresh.reachedCount || 0, }); setIsBenModalOpen(true); } catch { setEditingBen(b); benForm.reset({ name: b.name || '', category: b.category || 'General', location: b.location || '', targetCount: b.targetCount || 0, reachedCount: b.reachedCount || 0, }); setIsBenModalOpen(true); } }; const handleOpenEditComp = async (c: ComplianceItem) => { try { const fresh = await getComplianceItem(c.id); setEditingComp(fresh); compForm.reset({ title: fresh.title || '', requirements: fresh.requirements || '', status: fresh.status ?? ComplianceStatus.NotMet, beneficiaryId: fresh.beneficiaryId ? String(fresh.beneficiaryId) : 'none', }); setIsCompModalOpen(true); } catch { setEditingComp(c); compForm.reset({ title: c.title || '', requirements: c.requirements || '', status: c.status ?? ComplianceStatus.NotMet, beneficiaryId: c.beneficiaryId ? String(c.beneficiaryId) : 'none', }); setIsCompModalOpen(true); } }; const handleOpenAddOutcome = (ben?: Beneficiary | null) => { setEditingOutcome(null); outcomeForm.reset({ name: '', description: '', status: ComplianceStatus.NotMet, beneficiaryId: ben?.id ? String(ben.id) : (selectedBenForDetail?.id ? String(selectedBenForDetail.id) : (beneficiaries[0]?.id ? String(beneficiaries[0].id) : '')), }); setIsOutcomeModalOpen(true); }; const handleOpenEditOutcome = (o: Outcome) => { setEditingOutcome(o); outcomeForm.reset({ name: o.name || '', description: o.description || '', status: o.status ?? ComplianceStatus.NotMet, beneficiaryId: o.beneficiaryId ? String(o.beneficiaryId) : '', }); setIsOutcomeModalOpen(true); }; const onSubmitOutcome = (data: OutcomeFormValues) => { if (editingOutcome) { updateOutcomeMutation.mutate(data); } else { const targetBenId = data.beneficiaryId || selectedBenForDetail?.id; if (!targetBenId) { toastError('Missing Beneficiary', 'Please select a beneficiary cohort for this outcome.'); return; } createOutcomeMutation.mutate({ ...data, beneficiaryId: String(targetBenId) }); } }; return (
{/* Header Banner */}

Beneficiaries, Compliance & Outcomes

Manage target recipient demographics, track reached counts, monitor statutory compliance items, and record impact metrics.

{/* KPI Cards */}
Beneficiary Cohorts

{beneficiaries.length}

Registered target stakeholder groups

Total Target Reach

{totalTarget.toLocaleString()}

Planned participant headcount

Actual Reached

{totalReached.toLocaleString()}

({reachPercentage}%)

Verified participant reach to date

Compliance Items

{complianceItems.length}

({verifiedCompCount} Satisfied)

Statutory & audit verification records

Recorded Outcomes

{allOutcomes.length}

({metOutcomesCount} Met)

Recorded impact metrics & achievements

{/* Tabs */}
{/* Tab 1: Beneficiaries Tab Toolbar & Content */} {activeTab === 'beneficiaries' && (
{/* Search */}
setSearchTerm(e.target.value)} className="w-full bg-slate-900 border border-slate-800 rounded-xl pl-8 pr-3 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 shadow-xs" />
{/* Category Filter */}
{/* Project Selector */}
{/* Actions: Refresh, Export, Register */}
{canAdd('beneficiaries') && ( )}
Beneficiary / Cohort Category Location Target Headcount Reached Headcount {loadingBens ? ( ) : sortedBens.length === 0 ? ( ) : ( sortedBens.map((b) => { const prog = b.targetCount && b.targetCount > 0 ? Math.round(((b.reachedCount || 0) / b.targetCount) * 100) : 0; return ( ); }) )}
Reach Progress Actions
Loading beneficiaries...
No beneficiaries recorded for this project.
{b.name}
{b.category || 'General'}
{b.location || 'Not Specified'}
{(b.targetCount || 0).toLocaleString()} {(b.reachedCount || 0).toLocaleString()}
{prog}%
)} {/* Tab 2: Compliance Items Table */} {activeTab === 'compliance' && (
{/* Search */}
setSearchTerm(e.target.value)} className="w-full bg-slate-900 border border-slate-800 rounded-xl pl-8 pr-3 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 shadow-xs" />
{/* Status Filter */}
{/* Project Selector */}
{/* Actions: Refresh, Export, Log Compliance Item */}
{canAdd('beneficiaries') && ( )}
Compliance Requirement Requirements Detail Verification Status {loadingComps ? ( ) : sortedComps.length === 0 ? ( ) : ( sortedComps.map((c) => ( )) )}
Project Actions
Loading compliance requirements...
No compliance items recorded for this project.
{c.title}
{projects.find((p) => String(p.id) === String(c.projectId))?.name || 'Project'} {c.requirements || 'No specific requirements'}
)} {/* Tab 3: Beneficiary Outcomes Tab */} {activeTab === 'outcomes' && (
{/* Search */}
setOutcomeSearchTerm(e.target.value)} className="w-full bg-slate-900 border border-slate-800 rounded-xl pl-8 pr-3 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 shadow-xs" />
{/* Status Filter */}
{/* Beneficiary Filter */}
{/* Project Selector */}
{/* Actions: Refresh, Export, Record */}
{canAdd('beneficiaries') && ( )}
{/* Outcomes Table */}
Outcome / Impact Metric Beneficiary Cohort Project Fulfillment Status Recorded Date {loadingOutcomesTab ? ( ) : sortedOutcomes.length === 0 ? ( ) : ( sortedOutcomes.map((o) => ( )) )}
Impact Description Actions
Loading outcomes...
No outcomes registered for this selection.
{o.name}
{o.beneficiaryName}
{o.projectName} {o.description || 'No description provided'} {o.createdAt ? formatDate(o.createdAt) : '—'}
)}
{/* Modal: Create/Edit Beneficiary */} setIsBenModalOpen(false)} title={editingBen ? 'Edit Beneficiary Profile' : 'Register Project Beneficiary'} size="md" >
(editingBen ? updateBenMutation.mutate(d) : createBenMutation.mutate(d)))} className="space-y-4" >
{benForm.formState.errors.name && ( {benForm.formState.errors.name.message} )}
{/* Modal: Create/Edit Compliance Item */} setIsCompModalOpen(false)} title={editingComp ? 'Edit Compliance Item' : 'Log Project Compliance Requirement'} size="md" >
(editingComp ? updateCompMutation.mutate(d) : createCompMutation.mutate(d)))} className="space-y-4" >
{compForm.formState.errors.title && ( {compForm.formState.errors.title.message} )}