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, Plus, ShieldCheck, CheckCircle2, AlertCircle, RefreshCw, Search, Filter, CheckSquare, Edit2, FolderKanban, Target, Award, Sparkles, MapPin, FileCheck, } from 'lucide-react'; import { getProjects, getProjectBeneficiaries, createBeneficiary, updateBeneficiary, getBeneficiaryOutcomes, createOutcome, getProjectComplianceItems, 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 { Beneficiary, ComplianceItem, Outcome, ApprovalStatus } from '../types'; import { getApprovalStatusBadge, formatDate, APPROVAL_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(), }); const outcomeSchema = z.object({ name: z.string().min(3, 'Outcome name required'), description: z.string().min(5, 'Description required'), }); 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 [selectedProjectId, setSelectedProjectId] = useState('all'); const [activeTab, setActiveTab] = useState<'beneficiaries' | 'compliance'>('beneficiaries'); const [searchTerm, setSearchTerm] = useState(''); const [categoryFilter, setCategoryFilter] = useState('all'); const [compStatusFilter, setCompStatusFilter] = 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 [selectedBenForDetail, setSelectedBenForDetail] = useState(null); const [isDetailModalOpen, setIsDetailModalOpen] = useState(false); const [isOutcomeModalOpen, setIsOutcomeModalOpen] = useState(false); // Queries const { data: projects = [], isLoading: loadingProjects } = useQuery({ queryKey: ['projects'], queryFn: () => getProjects(), }); // Query beneficiaries const effectiveProjectId = selectedProjectId !== 'all' ? selectedProjectId : projects[0]?.id; const { data: beneficiaries = [], isLoading: loadingBens, refetch: refetchBens, isFetching: fetchingBens, } = useQuery({ queryKey: ['beneficiaries', effectiveProjectId], queryFn: () => (effectiveProjectId ? getProjectBeneficiaries(effectiveProjectId) : Promise.resolve([])), enabled: !!effectiveProjectId, }); // Query compliance items const { data: complianceItems = [], isLoading: loadingComps, refetch: refetchComps, isFetching: fetchingComps, } = useQuery({ queryKey: ['complianceItems', effectiveProjectId], queryFn: () => (effectiveProjectId ? getProjectComplianceItems(effectiveProjectId) : Promise.resolve([])), enabled: !!effectiveProjectId, }); // Query outcomes for selected beneficiary const { data: outcomes = [], refetch: refetchOutcomes } = useQuery({ queryKey: ['outcomes', selectedBenForDetail?.id], queryFn: () => (selectedBenForDetail?.id ? getBeneficiaryOutcomes(selectedBenForDetail.id) : Promise.resolve([])), enabled: !!selectedBenForDetail?.id, }); // 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: '' }, }); // Mutations const createBenMutation = useMutation({ mutationFn: (vals: BeneficiaryFormValues) => { if (!effectiveProjectId) throw new Error('No project selected'); return createBeneficiary({ projectId: effectiveProjectId, 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: any) => toastError('Registration Failed', err.message || 'Unable to save beneficiary.'), }); 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: any) => toastError('Update Failed', err.message || 'Unable to update beneficiary.'), }); const createCompMutation = useMutation({ mutationFn: (vals: ComplianceFormValues) => { if (!effectiveProjectId) throw new Error('No project selected'); return createComplianceItem({ projectId: effectiveProjectId, title: vals.title, requirements: vals.requirements }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['complianceItems'] }); setIsCompModalOpen(false); compForm.reset(); success('Compliance Requirement Logged', 'Compliance item registered for project beneficiaries.'); }, onError: (err: any) => toastError('Creation Failed', err.message || 'Unable to record compliance item.'), }); const updateCompMutation = useMutation({ mutationFn: (vals: ComplianceFormValues) => { if (!editingComp) throw new Error('No item selected'); return updateComplianceItem({ complianceItemId: editingComp.id, title: vals.title, requirements: vals.requirements, status: vals.status ?? ApprovalStatus.Pending, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['complianceItems'] }); setIsCompModalOpen(false); setEditingComp(null); compForm.reset(); success('Compliance Item Updated', 'Status and requirement record updated.'); }, onError: (err: any) => toastError('Update Failed', err.message || 'Unable to update compliance item.'), }); const createOutcomeMutation = useMutation({ mutationFn: (vals: OutcomeFormValues) => { if (!selectedBenForDetail) throw new Error('No beneficiary selected'); return createOutcome({ beneficiaryId: selectedBenForDetail.id, name: vals.name, description: vals.description }); }, onSuccess: () => { refetchOutcomes(); setIsOutcomeModalOpen(false); outcomeForm.reset(); success('Outcome Logged', 'Beneficiary outcome and impact metric registered.'); }, onError: (err: any) => toastError('Failed to Record Outcome', err.message || 'Unable to save outcome.'), }); // 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); // 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; // 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: 'Compliance Requirement', key: 'title' }, { header: 'Requirements Detail', key: 'requirements' }, { header: 'Status', key: 'status', format: (val) => (val === ApprovalStatus.Approved ? 'Approved' : 'Pending Review') }, ]; const handleOpenEditBen = (b: Beneficiary) => { 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 = (c: ComplianceItem) => { setEditingComp(c); compForm.reset({ title: c.title || '', requirements: c.requirements || '', status: c.status ?? ApprovalStatus.Pending, }); setIsCompModalOpen(true); }; return (
{/* Header Banner */}

Beneficiaries & Compliance Register

Manage target recipient demographics, track reached counts, and record verified statutory compliance items.

{/* KPI Cards */}
Beneficiary Cohorts

{beneficiaries.length}

Registered target stakeholder groups

Total Target Reach

{totalTarget.toLocaleString()}

Planned participant headcount

Actual Reached

{totalReached.toLocaleString()}

({reachPercentage}%)
Compliance Items

{complianceItems.length}

({verifiedCompCount} Satisfied)

Statutory & audit verification records

{/* 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 */}
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, Add Compliance Item */}
Compliance Title Requirements Verification Status Created {loadingComps ? ( ) : sortedComps.length === 0 ? ( ) : ( sortedComps.map((c) => ( )) )}
Actions
Loading compliance items...
No compliance items recorded for this project.
{c.title}
{c.requirements || 'None'} {getApprovalStatusBadge(c.status)} {formatDate(c.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 Requirement' : 'Add Compliance Requirement'} size="md" >
(editingComp ? updateCompMutation.mutate(d) : createCompMutation.mutate(d)))} className="space-y-4" >