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 { Coins, FileText, PieChart, Banknote, Plus, CheckCircle, Clock, ShieldCheck, AlertTriangle, ArrowRight, Lock, ExternalLink, DollarSign, FileCheck2, } from 'lucide-react'; import { getAwards, createAward, getContracts, createContract, executeContract, getBudgets, createBudget, getDisbursements, createDisbursement, approveDisbursement, getOrganisations, getAllProgrammes, getProjects, getAllMilestones, } from '../services/api'; import { Modal } from '../components/ui/Modal'; import { useToast } from '../components/ui/Toast'; import { Award, Contract, Budget, Disbursement, ApprovalStatus, ContractStatus, CreateAwardRequest, CreateContractRequest, ExecuteContractRequest, CreateBudgetRequest, CreateDisbursementRequest, } from '../types'; import { formatZAR, formatCompactZAR, formatDate, getApprovalStatusBadge, getContractStatusBadge, getUserEmail, } from '../lib/formatters'; // Schemas matching Figure 9 and OpenAPI constraints const awardSchema = z.object({ referenceNo: z.string().min(4, 'Approval / Reference number is mandatory (FR-M01-002)'), amount: z.coerce.number().min(1, 'Approved funding amount must be > 0 (FR-M01-003)'), fundingPeriod: z.string().min(3, 'Funding period is mandatory (e.g. 24 Months)'), organisationId: z.coerce.number().min(1, 'Select recipient organisation (FR-M01-006)'), programmeId: z.coerce.number().min(1, 'Select funding programme (FR-M01-005)'), }); const contractSchema = z.object({ awardId: z.coerce.number().min(1, 'Select approved award'), totalValue: z.coerce.number().min(1, 'Total value is required'), effectiveAt: z.string().min(4, 'Effective date is mandatory'), expiresAt: z.string().min(4, 'Expiry date is mandatory'), }); const executeContractSchema = z.object({ contractId: z.coerce.number().min(1), externalSignatureId: z.string().min(4, 'Digital signature ID is required'), signedDocumentUrl: z.string().url('Must be a valid document URL'), }); const budgetSchema = z.object({ projectId: z.coerce.number().min(1, 'Select project'), name: z.string().min(3, 'Budget category name required'), amount: z.coerce.number().min(1, 'Amount must be greater than zero'), }); const disbursementSchema = z.object({ projectId: z.coerce.number().min(1, 'Select project'), milestoneId: z.coerce.number().optional(), amount: z.coerce.number().min(1, 'Disbursement tranche amount required'), }); type AwardFormValues = z.infer; type ContractFormValues = z.infer; type ExecuteContractFormValues = z.infer; type BudgetFormValues = z.infer; type DisbursementFormValues = z.infer; export const GrantsPage: React.FC = () => { const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); const [activeTab, setActiveTab] = useState<'awards' | 'contracts' | 'budgets' | 'disbursements'>('awards'); // Modals const [isAwardModalOpen, setIsAwardModalOpen] = useState(false); const [isContractModalOpen, setIsContractModalOpen] = useState(false); const [isExecuteModalOpen, setIsExecuteModalOpen] = useState(false); const [selectedContractForExecution, setSelectedContractForExecution] = useState(null); const [isBudgetModalOpen, setIsBudgetModalOpen] = useState(false); const [isDisbursementModalOpen, setIsDisbursementModalOpen] = useState(false); // Queries const { data: awards = [], isLoading: loadingAwards } = useQuery({ queryKey: ['awards'], queryFn: () => getAwards(1, 100), }); const { data: contracts = [], isLoading: loadingContracts } = useQuery({ queryKey: ['contracts'], queryFn: () => getContracts(1, 100), }); const { data: budgets = [], isLoading: loadingBudgets } = useQuery({ queryKey: ['budgets'], queryFn: () => getBudgets(1, 100), }); const { data: disbursements = [], isLoading: loadingDisbursements } = useQuery({ queryKey: ['disbursements'], queryFn: () => getDisbursements(1, 100), }); const { data: organisations = [] } = useQuery({ queryKey: ['organisations'], queryFn: () => getOrganisations(), }); const { data: programmes = [] } = useQuery({ queryKey: ['programmes'], queryFn: () => getAllProgrammes(), }); const { data: projects = [] } = useQuery({ queryKey: ['projects'], queryFn: () => getProjects(), }); const { data: milestones = [] } = useQuery({ queryKey: ['milestones-all'], queryFn: () => getAllMilestones(), }); // Forms const awardForm = useForm({ resolver: zodResolver(awardSchema) as any, defaultValues: { referenceNo: '', amount: 10000000, fundingPeriod: '24 Months', organisationId: organisations[0]?.id ? Number(organisations[0].id) : 101, programmeId: programmes[0]?.id ? Number(programmes[0].id) : 301, }, }); const contractForm = useForm({ resolver: zodResolver(contractSchema) as any, defaultValues: { awardId: awards[0]?.id ? Number(awards[0].id) : 501, totalValue: 10000000, effectiveAt: new Date().toISOString().slice(0, 10), expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10), }, }); const executeForm = useForm({ resolver: zodResolver(executeContractSchema) as any, defaultValues: { contractId: 0, externalSignatureId: '', signedDocumentUrl: '', }, }); const budgetForm = useForm({ resolver: zodResolver(budgetSchema) as any, defaultValues: { projectId: projects[0]?.id ? Number(projects[0].id) : 401, name: '', amount: 1000000, }, }); const disbursementForm = useForm({ resolver: zodResolver(disbursementSchema) as any, defaultValues: { projectId: projects[0]?.id ? Number(projects[0].id) : 401, amount: 2500000, }, }); // Mutations const awardMutation = useMutation({ mutationFn: (data: CreateAwardRequest) => createAward(data), onSuccess: (newAwd) => { queryClient.invalidateQueries({ queryKey: ['awards'] }); queryClient.invalidateQueries({ queryKey: ['contracts'] }); queryClient.invalidateQueries({ queryKey: ['dashboard'] }); success( 'Award Registered & Contract Initiated', `Award ${newAwd.referenceNo} recorded. Automated Contract workflow initiated (BR-002).` ); setIsAwardModalOpen(false); awardForm.reset(); }, onError: (err: unknown) => { toastError('Award registration failed', String(err)); }, }); const contractMutation = useMutation({ mutationFn: (data: CreateContractRequest) => createContract(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contracts'] }); success('Contract generated', 'New legal agreement created in Draft state'); setIsContractModalOpen(false); contractForm.reset(); }, onError: (err: unknown) => { toastError('Contract creation failed', String(err)); }, }); const executeMutation = useMutation({ mutationFn: (data: ExecuteContractRequest) => executeContract(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contracts'] }); success( 'Contract Executed & Locked', 'Agreement locked as authoritative version (AT-02). Downstream budgets and disbursements enabled.' ); setIsExecuteModalOpen(false); executeForm.reset(); }, onError: (err: unknown) => { toastError('Execution trigger failed', String(err)); }, }); const budgetMutation = useMutation({ mutationFn: (data: CreateBudgetRequest) => createBudget(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['budgets'] }); success('Budget line established', 'Budget line created against project'); setIsBudgetModalOpen(false); budgetForm.reset(); }, onError: (err: unknown) => { toastError('Budget creation failed', String(err)); }, }); const disbursementMutation = useMutation({ mutationFn: (data: CreateDisbursementRequest) => createDisbursement(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['disbursements'] }); success('Disbursement Scheduled', 'Payment tranche added in Pending Review state'); setIsDisbursementModalOpen(false); disbursementForm.reset(); }, onError: (err: unknown) => { toastError('Disbursement creation failed', String(err)); }, }); const approveMutation = useMutation({ mutationFn: (id: string | number) => approveDisbursement(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['disbursements'] }); queryClient.invalidateQueries({ queryKey: ['dashboard'] }); success( 'Payment Approved & Released', 'Conditions precedent verified. EFT reference generated and disbursement ledger updated (AT-05).' ); }, onError: (err: unknown) => { toastError('Disbursement approval failed', String(err)); }, }); const openExecuteModal = (c: Contract) => { setSelectedContractForExecution(c); executeForm.reset({ contractId: Number(c.id), externalSignatureId: `SIG-DIGITAL-${Math.floor(100000 + Math.random() * 900000)}`, signedDocumentUrl: `https://documents.ifri.khongisa.co.za/contracts/CTR-${c.id}-Signed.pdf`, }); setIsExecuteModalOpen(true); }; // Summary calculations const totalAwardValue = awards.reduce((sum, a) => sum + (Number(a.amount) || 0), 0); const totalDisbursedValue = disbursements .filter((d) => d.status === ApprovalStatus.Approved) .reduce((sum, d) => sum + (Number(d.amount) || 0), 0); const pendingDisbursementValue = disbursements .filter((d) => d.status === ApprovalStatus.PendingReview) .reduce((sum, d) => sum + (Number(d.amount) || 0), 0); return (
{/* Header & Execution Pipeline Summary */}

Grant & Financial Management

End-to-end controlled execution thread: Funding Award Intake → Contracting → Budgeting → Condition-Checked Disbursement.

{activeTab === 'awards' && ( )} {activeTab === 'contracts' && ( )} {activeTab === 'budgets' && ( )} {activeTab === 'disbursements' && ( )}
{/* Pipeline Summary Bar */}
Total Awards Registered {formatCompactZAR(totalAwardValue)}
Disbursed (Verified & Paid) {formatCompactZAR(totalDisbursedValue)}
Pending Release {formatCompactZAR(pendingDisbursementValue)}
{/* Tabs */}
{/* Tab 1: Awards Table (M01) */} {activeTab === 'awards' && (
{loadingAwards ? ( ) : ( awards.map((awd) => { const statusBadge = getApprovalStatusBadge(awd.status); return ( ); }) )}
Approval Reference # Recipient Organisation Funding Programme Approved Amount (ZAR) Status Date Registered
Loading registered awards...
{awd.referenceNo} System ID: #{awd.id} {awd.organisationName || `Org #${awd.organisationId}`} {awd.programmeName || `Programme #${awd.programmeId}`} {formatZAR(awd.amount)} {statusBadge.label} {formatDate(awd.createdAt)}
)} {/* Tab 2: Contracts Table (M02) */} {activeTab === 'contracts' && (
{loadingContracts ? ( ) : ( contracts.map((c) => { const statusBadge = getContractStatusBadge(c.status); const isExecuted = c.status === ContractStatus.Executed; return ( ); }) )}
Contract ID Associated Award Total Contract Value Effective / Expiry Status Action
Loading contracts...
CTR-{c.id} Award #{c.awardId} {formatZAR(c.totalValue)} {formatDate(c.effectiveAt)} → {formatDate(c.expiresAt)} {statusBadge.label} {isExecuted ? ( Locked ) : ( )}
)} {/* Tab 3: Budgets (M05) */} {activeTab === 'budgets' && (
{loadingBudgets ? ( ) : ( budgets.map((b) => ( )) )}
Budget Line Category Project ID Approved Amount (ZAR) Date Established Created By
Loading budget lines...
{b.name} Project #{b.projectId} {formatZAR(b.amount)} {formatDate(b.createdAt)} {getUserEmail(b.createdBy)}
)} {/* Tab 4: Disbursements & Approval Trigger (M06) */} {activeTab === 'disbursements' && (
Disbursement Control: Disbursement cannot be released unless contractual conditions are verified. Duplicate approvals blocked.
{loadingDisbursements ? ( ) : ( disbursements.map((d) => { const statusBadge = getApprovalStatusBadge(d.status); const isApproved = d.status === ApprovalStatus.Approved; return ( ); }) )}
Disbursement ID Project ID Milestone Association Tranche Amount Payment Reference Approval Status Approval Action
Loading disbursements...
DSB-{d.id} Project #{d.projectId} {d.milestoneId ? `Milestone #${d.milestoneId}` : 'Initial Advance Tranche'} {formatZAR(d.amount)} {d.paymentReference || Pending Release} {statusBadge.label} {isApproved ? ( Released ) : ( )}
)} {/* Modal: Register Approved Award */} setIsAwardModalOpen(false)} title="Register Approved Funding Award" subtitle="Step 1 of 3 — Award Details" >
awardMutation.mutate(data as any))} className="space-y-4">
{awardForm.formState.errors.referenceNo && (

{awardForm.formState.errors.referenceNo.message}

)}
{awardForm.formState.errors.amount && (

{awardForm.formState.errors.amount.message}

)}
Workflow Automation: Submitting this record will automatically initiate the Contract/Agreement workflow.
{/* Modal: Draft Contract */} setIsContractModalOpen(false)} title="Draft Contract Agreement" subtitle="Convert approved funding decision into legally enforceable terms" >
contractMutation.mutate(data as any))} className="space-y-4">
{/* Modal: Execute Contract */} setIsExecuteModalOpen(false)} title="Execute Contract & Lock Agreement" subtitle={`Locking Contract #${selectedContractForExecution?.id} as Authoritative Version`} >
executeMutation.mutate(data as any))} className="space-y-4">

Obligation: Once executed, the agreement is locked and downstream budget control and disbursement requests are enabled.

{/* Modal: Add Budget */} setIsBudgetModalOpen(false)} title="Add Project Budget Line" subtitle="Maintain approved expenditure breakdown and commitments" >
budgetMutation.mutate(data as any))} className="space-y-4">
{/* Modal: Schedule Disbursement */} setIsDisbursementModalOpen(false)} title="Schedule Disbursement Tranche" subtitle="Link disbursement to milestone verification and conditions precedent" >
disbursementMutation.mutate(data as any))} className="space-y-4">
); };