diff --git a/PostFundManagement.ConsoleUI/src/App.tsx b/PostFundManagement.ConsoleUI/src/App.tsx index 084a573..ac1d233 100644 --- a/PostFundManagement.ConsoleUI/src/App.tsx +++ b/PostFundManagement.ConsoleUI/src/App.tsx @@ -10,6 +10,9 @@ const LandingPage = lazy(() => import('./pages/LandingPage').then(m => ({ defaul const ExecutiveDashboard = lazy(() => import('./pages/ExecutiveDashboard').then(m => ({ default: m.ExecutiveDashboard }))); const GovernancePage = lazy(() => import('./pages/GovernancePage').then(m => ({ default: m.GovernancePage }))); const ProjectsPage = lazy(() => import('./pages/ProjectsPage').then(m => ({ default: m.ProjectsPage }))); +const BeneficiariesPage = lazy(() => import('./pages/BeneficiariesPage').then(m => ({ default: m.BeneficiariesPage }))); +const ChangeManagementPage = lazy(() => import('./pages/ChangeManagementPage').then(m => ({ default: m.ChangeManagementPage }))); +const SiteVisitsPage = lazy(() => import('./pages/SiteVisitsPage').then(m => ({ default: m.SiteVisitsPage }))); const GrantsPage = lazy(() => import('./pages/GrantsPage').then(m => ({ default: m.GrantsPage }))); const MonitoringPage = lazy(() => import('./pages/MonitoringPage').then(m => ({ default: m.MonitoringPage }))); const SettingsPage = lazy(() => import('./pages/SettingsPage').then(m => ({ default: m.SettingsPage }))); @@ -46,6 +49,11 @@ export default function App() { } /> } /> } /> + } /> + } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/PostFundManagement.ConsoleUI/src/components/governance/InstructionsSection.tsx b/PostFundManagement.ConsoleUI/src/components/governance/InstructionsSection.tsx index cbee4f7..a66e72f 100644 --- a/PostFundManagement.ConsoleUI/src/components/governance/InstructionsSection.tsx +++ b/PostFundManagement.ConsoleUI/src/components/governance/InstructionsSection.tsx @@ -24,6 +24,7 @@ import { HelpTooltip } from '../ui/HelpTooltip'; import { useToast } from '../ui/Toast'; import { ExportButton } from '../ui/ExportButton'; import { ExportColumn } from '../../utils/exportUtils'; +import { useTableSort, SortableTh } from '../../utils/tableSort'; import { Instrument, Instruction, @@ -237,6 +238,20 @@ export const FundingInstrumentsSection: React.FC }); }, [instruments, selectedProgrammeFilter, statusFilter, searchTerm, programmeMap]); + // Sorting hook + const { + sortedData: sortedInstruments, + sortColumn: instSortCol, + sortDirection: instSortDir, + handleSort: handleInstSort, + } = useTableSort(filteredInstruments, null, null, { + code: (inst: Instrument) => inst.code || `INST-${inst.id}`, + name: (inst: Instrument) => inst.name, + programmeId: (inst: Instrument) => programmeMap.get(String(inst.programmeId))?.name || `Programme #${inst.programmeId}`, + status: (inst: Instrument) => getActivityStatusBadge(inst.status).label, + createdAt: (inst: Instrument) => inst.createdAt, + }); + // Export columns definition const instrumentExportColumns: ExportColumn[] = useMemo( () => [ @@ -400,16 +415,26 @@ export const FundingInstrumentsSection: React.FC - - - - - + + Code / Ref + + + Instrument Title + + + Associated Programme + + + Status + + + Created Info + - {filteredInstruments.map((inst) => { + {sortedInstruments.map((inst) => { const statusBadge = getActivityStatusBadge(inst.status); const parentProg = programmeMap.get(String(inst.programmeId)); diff --git a/PostFundManagement.ConsoleUI/src/components/governance/RulesSection.tsx b/PostFundManagement.ConsoleUI/src/components/governance/RulesSection.tsx index 58d9501..60517fd 100644 --- a/PostFundManagement.ConsoleUI/src/components/governance/RulesSection.tsx +++ b/PostFundManagement.ConsoleUI/src/components/governance/RulesSection.tsx @@ -24,6 +24,7 @@ import { HelpTooltip } from '../ui/HelpTooltip'; import { useToast } from '../ui/Toast'; import { ExportButton } from '../ui/ExportButton'; import { ExportColumn } from '../../utils/exportUtils'; +import { useTableSort, SortableTh } from '../../utils/tableSort'; import { Rule, Programme, @@ -233,6 +234,20 @@ export const RulesSection: React.FC = ({ }); }, [rules, selectedProgrammeFilter, statusFilter, searchTerm, programmeMap]); + // Sorting hook + const { + sortedData: sortedRules, + sortColumn: ruleSortCol, + sortDirection: ruleSortDir, + handleSort: handleRuleSort, + } = useTableSort(filteredRules, null, null, { + name: (r: Rule) => r.name, + programmeId: (r: Rule) => programmeMap.get(String(r.programmeId))?.name || `Programme #${r.programmeId}`, + version: (r: Rule) => r.version || 1, + status: (r: Rule) => getActivityStatusBadge(r.status).label, + createdAt: (r: Rule) => r.createdAt, + }); + // Export columns definition const ruleExportColumns: ExportColumn[] = useMemo( () => [ @@ -396,16 +411,26 @@ export const RulesSection: React.FC = ({
Code / RefInstrument TitleAssociated ProgrammeStatusCreated Info Actions
- - - - - + + Rule Title & Directives + + + Associated Programme + + + Version + + + Status + + + Audit Info + - {filteredRules.map((r) => { + {sortedRules.map((r) => { const statusBadge = getActivityStatusBadge(r.status); const parentProg = programmeMap.get(String(r.programmeId)); diff --git a/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx b/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx index 3c13685..91d0722 100644 --- a/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx +++ b/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx @@ -11,10 +11,14 @@ import { Menu, X, ChevronRight, + ChevronDown, ShieldCheck, Database, Home, User, + Users, + GitPullRequest, + MapPin, } from 'lucide-react'; import { useAuth } from '../../auth/AuthProvider'; import { BrandLogo } from '../ui/BrandLogo'; @@ -23,7 +27,22 @@ import { useQuery } from '@tanstack/react-query'; import { getUsers } from '../../services/api'; import { getUserEmail, registerUserEmail } from '../../lib/formatters'; -const navItems = [ +interface SubNavItem { + name: string; + path: string; + icon: React.ElementType; + help?: string; +} + +interface NavItem { + name: string; + path: string; + icon: React.ElementType; + help: string; + subItems?: SubNavItem[]; +} + +const navItems: NavItem[] = [ { name: 'Executive Dashboard', path: '/dashboard', @@ -41,6 +60,26 @@ const navItems = [ path: '/projects', icon: FolderKanban, help: 'Project Work Breakdown Structure (WBS), contractual deliverables, scheduled milestones, evidence verification, and execution tracking.', + subItems: [ + { + name: 'Beneficiaries', + path: '/projects/beneficiaries', + icon: Users, + help: 'Manage target beneficiaries, demographics, and statutory compliance items satisfying beneficiaries.', + }, + { + name: 'Change Management', + path: '/projects/change-management', + icon: GitPullRequest, + help: 'Track project issues, contractual change requests, and resolve changes in-place.', + }, + ], + }, + { + name: 'Site Visits', + path: '/site-visits', + icon: MapPin, + help: 'Log physical site visits, field observations, inspector findings, and project feedback.', }, { name: 'Grant & Financials', @@ -151,21 +190,52 @@ export const AppLayout: React.FC = () => { diff --git a/PostFundManagement.ConsoleUI/src/components/monitoring/ContractsManagement.tsx b/PostFundManagement.ConsoleUI/src/components/monitoring/ContractsManagement.tsx index 3d22fce..261008f 100644 --- a/PostFundManagement.ConsoleUI/src/components/monitoring/ContractsManagement.tsx +++ b/PostFundManagement.ConsoleUI/src/components/monitoring/ContractsManagement.tsx @@ -42,6 +42,7 @@ import { useToast } from '../ui/Toast'; import { ProblemDetailsAlert } from '../ui/ProblemDetailsAlert'; import { ExportButton } from '../ui/ExportButton'; import { ExportColumn } from '../../utils/exportUtils'; +import { useTableSort, SortableTh } from '../../utils/tableSort'; import { Contract, Award, @@ -406,6 +407,30 @@ export const ContractsManagement: React.FC = ({ { header: 'Document URI', key: 'fileUri', format: (val) => val || '—' }, ]; + // Table sorting hook + const { + sortedData: sortedContracts, + sortColumn: contractSortCol, + sortDirection: contractSortDir, + handleSort: handleContractSort, + } = useTableSort(filteredContracts, null, null, { + id: (c: any) => c.id, + awardId: (c: any) => c.totalValue || c.awardId, + programmeId: (c: any) => { + const a = getAwardForContract(c); + const pr = getProgrammeForAward(a); + return pr?.name || ''; + }, + organisationId: (c: any) => { + const a = getAwardForContract(c); + const o = getOrganisationForAward(a); + return o?.name || ''; + }, + signedDate: (c: any) => c.signedDate, + status: (c: any) => getContractStatusBadge(c.status).label, + fileUri: (c: any) => c.fileUri || '', + }); + return (
{/* Summary Cards */} @@ -612,18 +637,32 @@ export const ContractsManagement: React.FC = ({
Rule Title & DirectivesAssociated ProgrammeVersionStatusAudit Info Actions
- - - - - - - - + + Contract / Ref + + + Award & Amount + + + Programme & Project + + + Organisation + + + Validity Period + + + Status + + + Document / S3 + + - {filteredContracts.map((contract) => { + {sortedContracts.map((contract) => { const award = getAwardForContract(contract); const org = getOrganisationForAward(award); const prog = getProgrammeForAward(award); diff --git a/PostFundManagement.ConsoleUI/src/components/monitoring/EvidenceReviewQueue.tsx b/PostFundManagement.ConsoleUI/src/components/monitoring/EvidenceReviewQueue.tsx index ed6165d..64a8cd9 100644 --- a/PostFundManagement.ConsoleUI/src/components/monitoring/EvidenceReviewQueue.tsx +++ b/PostFundManagement.ConsoleUI/src/components/monitoring/EvidenceReviewQueue.tsx @@ -233,7 +233,7 @@ export const EvidenceReviewQueue: React.FC = ({ if (variables.status === ApprovalStatus.Approved) { success('Evidence Approved & Verified', 'Approval recorded. Contributes towards contractual milestone sign-off.'); } else if (variables.status === ApprovalStatus.Rejected) { - success('Evidence Rejected', 'Evidence marked as Rejected via verification endpoint.'); + success('Evidence Rejected', 'Evidence marked as Rejected by reviewer.'); } else { success('Status Updated', 'Evidence status updated successfully.'); } @@ -971,7 +971,7 @@ export const EvidenceReviewQueue: React.FC = ({

- Review the deliverable document against contractual requirements. If the evidence does not satisfy requirements, reject the submission using the endpoint action below. Approved evidence validates progress towards milestone completion. + Review the deliverable document against contractual requirements. If the evidence does not satisfy requirements, reject the submission using the review actions below. Approved evidence validates progress towards milestone completion.

diff --git a/PostFundManagement.ConsoleUI/src/components/ui/ExportButton.tsx b/PostFundManagement.ConsoleUI/src/components/ui/ExportButton.tsx index c02effc..f7cf4d5 100644 --- a/PostFundManagement.ConsoleUI/src/components/ui/ExportButton.tsx +++ b/PostFundManagement.ConsoleUI/src/components/ui/ExportButton.tsx @@ -73,7 +73,7 @@ export function ExportButton({ }; return ( -
+
{isOpen && !isDisabled && ( -
+
Export Format ({data.length} items) diff --git a/PostFundManagement.ConsoleUI/src/pages/BeneficiariesPage.tsx b/PostFundManagement.ConsoleUI/src/pages/BeneficiariesPage.tsx new file mode 100644 index 0000000..391ff80 --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/pages/BeneficiariesPage.tsx @@ -0,0 +1,928 @@ +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'); + + // 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({ ...vals, projectId: effectiveProjectId }); + }, + 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({ ...vals, beneficiaryId: editingBen.id }); + }, + 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) => { + return ( + !searchTerm.trim() || + (c.title && c.title.toLowerCase().includes(searchTerm.toLowerCase())) || + (c.requirements && c.requirements.toLowerCase().includes(searchTerm.toLowerCase())) + ); + }); + }, [complianceItems, searchTerm]); + + 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', accessor: (c) => (c.status === 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. +

+
+
+
+ +
+ + + {activeTab === 'beneficiaries' ? ( + + ) : ( + + )} + + + + +
+
+ + {/* 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 & Filters */} +
+
+
+ + + +
+ +
+
+ + Project: + +
+ +
+ + setSearchTerm(e.target.value)} + className="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 w-40 sm:w-56" + /> +
+
+
+ + {/* Tab 1: Beneficiaries Table */} + {activeTab === 'beneficiaries' && ( +
+
+
Contract / RefAward & AmountProgramme & ProjectOrganisationValidity PeriodStatusDocument / S3ActionsActions
+ + + + 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 ProgressActions
+
+
+ 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' && ( +
+
+ + + + + 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" + > +
+ + +
+ +
+ +