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, PieChart, Banknote, Plus, CheckCircle, Clock, ShieldCheck, AlertTriangle, ArrowRight, Lock, ExternalLink, DollarSign, Download, Edit2, Search, RefreshCw, } from 'lucide-react'; import { getAwards, getAward, createAward, getBudgets, getBudget, createBudget, updateBudget, getDisbursements, getDisbursement, createDisbursement, updateDisbursement, approveDisbursement, getOrganisations, getAllProgrammes, getProjects, getAllMilestones, } from '../services/api'; import { Modal } from '../components/ui/Modal'; import { HelpTooltip } from '../components/ui/HelpTooltip'; import { useToast } from '../components/ui/Toast'; import { ExportButton } from '../components/ui/ExportButton'; import { ExportColumn } from '../utils/exportUtils'; import { Award, Budget, Disbursement, ApprovalStatus, CreateAwardRequest, CreateBudgetRequest, UpdateBudgetRequest, CreateDisbursementRequest, } from '../types'; import { formatZAR, formatCompactZAR, formatDate, getApprovalStatusBadge, getUserEmail, } from '../lib/formatters'; // Validation schemas for grant administration const awardSchema = z.object({ referenceNo: z.string().min(4, 'Approval / Reference number is mandatory'), projectId: z.coerce.number().min(1, 'Select target project'), amount: z .coerce .number() .positive('Approved funding amount must be strictly greater than zero (no negative or zero values)'), fundingPeriod: z.string().min(3, 'Funding period is mandatory (e.g. 24 Months)'), organisationId: z.coerce.number().min(1, 'Select recipient organisation'), programmeId: z.coerce.number().min(1, 'Select funding programme'), }); 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() .positive('Disbursement tranche amount must be strictly greater than zero (no negative or zero values)'), }); type AwardFormValues = 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' | 'budgets' | 'disbursements'>('awards'); // Modals const [isAwardModalOpen, setIsAwardModalOpen] = useState(false); const [isBudgetModalOpen, setIsBudgetModalOpen] = useState(false); const [editingBudget, setEditingBudget] = useState(null); const [isEditBudgetModalOpen, setIsEditBudgetModalOpen] = useState(false); const [selectedBudgetStatus, setSelectedBudgetStatus] = useState(ApprovalStatus.Approved); const [isDisbursementModalOpen, setIsDisbursementModalOpen] = useState(false); const [editingDisbursement, setEditingDisbursement] = useState(null); // Queries const { data: awards = [], isLoading: loadingAwards, refetch: refetchAwards, isFetching: isFetchingAwards, } = useQuery({ queryKey: ['awards'], queryFn: () => getAwards(1, 100), }); const { data: budgets = [], isLoading: loadingBudgets, refetch: refetchBudgets, isFetching: isFetchingBudgets, } = useQuery({ queryKey: ['budgets'], queryFn: () => getBudgets(1, 100), }); const { data: disbursements = [], isLoading: loadingDisbursements, refetch: refetchDisbursements, isFetching: isFetchingDisbursements, } = 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: '', projectId: projects[0]?.id ? Number(projects[0].id) : 401, amount: 10000000, fundingPeriod: '24 Months', organisationId: organisations[0]?.id ? Number(organisations[0].id) : 101, programmeId: programmes[0]?.id ? Number(programmes[0].id) : 301, }, }); 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: ['dashboard'] }); success( 'Award Registered', `Award ${newAwd.referenceNo} recorded successfully.` ); setIsAwardModalOpen(false); awardForm.reset(); }, onError: (err: unknown) => { toastError('Award registration failed', 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', err); }, }); const updateBudgetMutation = useMutation({ mutationFn: (data: UpdateBudgetRequest) => updateBudget(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['budgets'] }); success('Budget Updated', 'Budget compliance and approval status updated.'); setIsEditBudgetModalOpen(false); setEditingBudget(null); }, onError: (err: unknown) => { toastError('Budget update failed', err); }, }); const disbursementMutation = useMutation({ mutationFn: async (data: DisbursementFormValues) => { if (editingDisbursement) { return updateDisbursement({ disbursementId: editingDisbursement.id, milestoneId: data.milestoneId ? Number(data.milestoneId) : null, amount: Number(data.amount), }); } return createDisbursement({ projectId: Number(data.projectId), milestoneId: data.milestoneId ? Number(data.milestoneId) : null, amount: Number(data.amount), }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['disbursements'] }); success( editingDisbursement ? 'Disbursement Updated' : 'Disbursement Scheduled', editingDisbursement ? 'Payment tranche details updated' : 'Payment tranche added in Pending Review state' ); setIsDisbursementModalOpen(false); setEditingDisbursement(null); disbursementForm.reset(); }, onError: (err: unknown) => { toastError(editingDisbursement ? 'Disbursement update failed' : 'Disbursement creation failed', err); }, }); // Watched values for award project budget validation const watchedAwardProjectId = awardForm.watch('projectId') || (projects[0]?.id ? Number(projects[0].id) : 0); const watchedAwardAmountRaw = awardForm.watch('amount'); const watchedAwardAmount = Number(watchedAwardAmountRaw) || 0; // Sum of all allocated budgets for the chosen project const selectedProjectBudgetsForAward = budgets.filter( (b) => String(b.projectId) === String(watchedAwardProjectId) ); const totalAllocatedBudgetForAwardProject = selectedProjectBudgetsForAward.reduce( (sum, b) => sum + (Number(b.amount) || 0), 0 ); // Award validation flags const isAwardNegativeOrZero = watchedAwardAmount <= 0; const hasNoBudgetForAwardProject = totalAllocatedBudgetForAwardProject <= 0; const isAwardExceedingBudget = watchedAwardAmount > totalAllocatedBudgetForAwardProject; const isAwardDisallowed = isAwardNegativeOrZero || isAwardExceedingBudget || hasNoBudgetForAwardProject; const handleAwardSubmit = (data: AwardFormValues) => { const amountNum = Number(data.amount); const targetProjId = Number(data.projectId); if (amountNum <= 0) { toastError( 'Invalid Award Amount', 'Approved funding amount must be strictly greater than zero. Negative or zero values are disallowed.' ); return; } const projBudgets = budgets.filter((b) => String(b.projectId) === String(targetProjId)); const totBudget = projBudgets.reduce((sum, b) => sum + (Number(b.amount) || 0), 0); if (totBudget <= 0) { toastError( 'Award Disallowed: No Budget', `The chosen project #${targetProjId} has zero allocated budget lines (ZAR 0.00). Establish budget lines before creating an award.` ); return; } if (amountNum > totBudget) { const overBy = amountNum - totBudget; toastError( 'Award Exceeds Project Budget', `The entered award amount of ${formatZAR(amountNum)} exceeds the total allocated project budget (${formatZAR(totBudget)}) by ${formatZAR(overBy)}. Creation disallowed.` ); return; } awardMutation.mutate({ organisationId: Number(data.organisationId), programmeId: Number(data.programmeId), amount: Number(data.amount), referenceNo: data.referenceNo, fundingPeriod: data.fundingPeriod, }); }; const openCreateAward = () => { const firstProj = projects[0]; const firstProjId = firstProj?.id ? Number(firstProj.id) : 401; const projBudgets = budgets.filter((b) => String(b.projectId) === String(firstProjId)); const totBudget = projBudgets.reduce((sum, b) => sum + (Number(b.amount) || 0), 0); awardForm.reset({ referenceNo: `AWD-2026-${Math.floor(100 + Math.random() * 900)}`, projectId: firstProjId, amount: totBudget > 0 ? totBudget : 0, fundingPeriod: '24 Months', organisationId: organisations[0]?.id ? Number(organisations[0].id) : 101, programmeId: firstProj?.programmeId ? Number(firstProj.programmeId) : programmes[0]?.id ? Number(programmes[0].id) : 301, }); setIsAwardModalOpen(true); }; // Watched values for disbursement budget ceiling and validation const watchedDisbursementProjectId = disbursementForm.watch('projectId') || (projects[0]?.id ? Number(projects[0].id) : 0); const watchedDisbursementAmountRaw = disbursementForm.watch('amount'); const watchedDisbursementAmount = Number(watchedDisbursementAmountRaw) || 0; // 1. Sum of all allocated project budgets const selectedProjectBudgets = budgets.filter( (b) => String(b.projectId) === String(watchedDisbursementProjectId) ); const totalAllocatedProjectBudget = selectedProjectBudgets.reduce( (sum, b) => sum + (Number(b.amount) || 0), 0 ); // 2. Sum of all existing project disbursements (excluding current when editing) const selectedProjectDisbursements = disbursements.filter( (d) => String(d.projectId) === String(watchedDisbursementProjectId) && (!editingDisbursement || String(d.id) !== String(editingDisbursement.id)) ); const totalExistingProjectDisbursements = selectedProjectDisbursements.reduce( (sum, d) => sum + (Number(d.amount) || 0), 0 ); // 3. Available disbursement headroom: sum(allocated project budgets) - sum(existing project disbursements) const availableDisbursementCapacity = Math.max( 0, totalAllocatedProjectBudget - totalExistingProjectDisbursements ); // 4. Strict validation state flags const isDisbursementNegativeOrZero = watchedDisbursementAmount <= 0; const isDisbursementExceedingBudget = watchedDisbursementAmount > availableDisbursementCapacity; const hasNoProjectBudget = totalAllocatedProjectBudget <= 0; const isDisbursementDisallowed = isDisbursementNegativeOrZero || isDisbursementExceedingBudget || hasNoProjectBudget; const handleDisbursementSubmit = (data: DisbursementFormValues) => { const amountNum = Number(data.amount); const targetProjId = Number(data.projectId); if (amountNum <= 0) { toastError( 'Invalid Tranche Amount', 'Disbursement amount must be strictly greater than zero. Negative or zero values are not allowed.' ); return; } const projBudgets = budgets.filter((b) => String(b.projectId) === String(targetProjId)); const totBudget = projBudgets.reduce((sum, b) => sum + (Number(b.amount) || 0), 0); const projDisbs = disbursements.filter( (d) => String(d.projectId) === String(targetProjId) && (!editingDisbursement || String(d.id) !== String(editingDisbursement.id)) ); const totDisb = projDisbs.reduce((sum, d) => sum + (Number(d.amount) || 0), 0); const maxCapacity = Math.max(0, totBudget - totDisb); if (totBudget <= 0) { toastError( 'Disbursement Blocked: No Budget', `Project #${targetProjId} has zero allocated budget lines. Establish budgets in the Budgets tab before scheduling disbursements.` ); return; } if (amountNum > maxCapacity) { const overBy = amountNum - maxCapacity; toastError( 'Disbursement Exceeds Budget Ceilings', `Requested tranche of ${formatZAR(amountNum)} exceeds available budget headroom (${formatZAR(maxCapacity)}) by ${formatZAR(overBy)}. Disallowed.` ); return; } disbursementMutation.mutate(data); }; const openCreateDisbursement = () => { setEditingDisbursement(null); const firstProjId = projects[0]?.id ? Number(projects[0].id) : 401; const projBudgets = budgets.filter((b) => String(b.projectId) === String(firstProjId)); const totBudget = projBudgets.reduce((sum, b) => sum + (Number(b.amount) || 0), 0); const projDisbs = disbursements.filter((d) => String(d.projectId) === String(firstProjId)); const totDisb = projDisbs.reduce((sum, d) => sum + (Number(d.amount) || 0), 0); const avail = Math.max(0, totBudget - totDisb); disbursementForm.reset({ projectId: firstProjId, milestoneId: undefined, amount: avail > 0 ? Math.min(avail, 150000) : 0, }); setIsDisbursementModalOpen(true); }; const openEditDisbursement = (d: Disbursement) => { setEditingDisbursement(d); disbursementForm.reset({ projectId: Number(d.projectId) || (projects[0]?.id ? Number(projects[0].id) : 401), milestoneId: d.milestoneId ? Number(d.milestoneId) : undefined, amount: d.amount, }); setIsDisbursementModalOpen(true); }; 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.' ); }, onError: (err: unknown) => { toastError('Disbursement approval failed', err); }, }); // 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); // Search & Filter State const [searchTerm, setSearchTerm] = useState(''); // Export Columns Definitions const awardExportColumns: ExportColumn[] = [ { header: 'Award Reference', key: 'referenceNo', format: (val, item) => val || `AWD-${item.id}` }, { header: 'Award ID', key: 'id' }, { header: 'Recipient Organisation', key: 'organisationId', format: (val, item) => { const org = organisations.find((o) => String(o.id) === String(val)); return org ? org.name : item.organisationName || `Org #${val}`; }, }, { header: 'Funding Programme', key: 'programmeId', format: (val, item) => { const prog = programmes.find((p) => String(p.id) === String(val)); return prog ? prog.name : item.programmeName || `Programme #${val}`; }, }, { header: 'Approved Amount (ZAR)', key: 'amount', format: (val) => formatZAR(val) }, { header: 'Status', key: 'status', format: (val) => getApprovalStatusBadge(val).label }, { header: 'Date Registered', key: 'createdAt', format: (val) => formatDate(val) }, { header: 'Created By', key: 'createdBy', format: (val) => getUserEmail(val) }, ]; const budgetExportColumns: ExportColumn[] = [ { header: 'Budget Line ID', key: 'id' }, { header: 'Budget Line Category', key: 'name' }, { header: 'Target Project', key: 'projectId', format: (val) => { const proj = projects.find((p) => String(p.id) === String(val)); return proj ? `${proj.name} (#${val})` : `Project #${val}`; }, }, { header: 'Approved Amount (ZAR)', key: 'amount', format: (val) => formatZAR(val) }, { header: 'Status', key: 'status', format: (val) => getApprovalStatusBadge(val ?? ApprovalStatus.Approved).label }, { header: 'Date Established', key: 'createdAt', format: (val) => formatDate(val) }, { header: 'Created By', key: 'createdBy', format: (val) => getUserEmail(val) }, ]; const disbursementExportColumns: ExportColumn[] = [ { header: 'Disbursement ID', key: 'id', format: (val) => `DSB-${val}` }, { header: 'Project', key: 'projectId', format: (val) => { const proj = projects.find((p) => String(p.id) === String(val)); return proj ? `${proj.name} (#${val})` : `Project #${val}`; }, }, { header: 'Linked Milestone', key: 'milestoneId', format: (val) => { if (!val) return 'Advance Tranche (No milestone)'; const ms = milestones.find((m) => String(m.id) === String(val)); return ms ? `${ms.name} (#${val})` : `Milestone #${val}`; }, }, { header: 'Tranche Amount (ZAR)', key: 'amount', format: (val) => formatZAR(val) }, { header: 'Payment Reference', key: 'paymentReference', format: (val) => val || 'Pending Release' }, { header: 'Approval Status', key: 'status', format: (val) => getApprovalStatusBadge(val).label }, { header: 'Creation Date', key: 'createdAt', format: (val) => formatDate(val) }, { header: 'Created By', key: 'createdBy', format: (val) => getUserEmail(val) }, ]; // Filtering const filteredAwards = awards.filter((a) => { if (!searchTerm.trim()) return true; const q = searchTerm.toLowerCase(); const ref = (a.referenceNo || '').toLowerCase(); const id = String(a.id); const org = organisations.find((o) => String(o.id) === String(a.organisationId)); const orgName = (org?.name || a.organisationName || '').toLowerCase(); const prog = programmes.find((p) => String(p.id) === String(a.programmeId)); const progName = (prog?.name || a.programmeName || '').toLowerCase(); const creator = getUserEmail(a.createdBy).toLowerCase(); return ref.includes(q) || id.includes(q) || orgName.includes(q) || progName.includes(q) || creator.includes(q); }); const filteredBudgets = budgets.filter((b) => { if (!searchTerm.trim()) return true; const q = searchTerm.toLowerCase(); const name = (b.name || '').toLowerCase(); const id = String(b.id); const projId = String(b.projectId); const proj = projects.find((p) => String(p.id) === String(b.projectId)); const projName = (proj?.name || '').toLowerCase(); const creator = getUserEmail(b.createdBy).toLowerCase(); return name.includes(q) || id.includes(q) || projId.includes(q) || projName.includes(q) || creator.includes(q); }); const filteredDisbursements = disbursements.filter((d) => { if (!searchTerm.trim()) return true; const q = searchTerm.toLowerCase(); const id = String(d.id); const projId = String(d.projectId); const proj = projects.find((p) => String(p.id) === String(d.projectId)); const projName = (proj?.name || '').toLowerCase(); const ref = (d.paymentReference || '').toLowerCase(); const msId = d.milestoneId ? String(d.milestoneId) : ''; const creator = getUserEmail(d.createdBy).toLowerCase(); return id.includes(q) || projId.includes(q) || projName.includes(q) || ref.includes(q) || msId.includes(q) || creator.includes(q); }); return (
{/* Header & Execution Pipeline Summary */}

Grant & Financial Management

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

{activeTab === 'awards' && ( )} {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' && (
setSearchTerm(e.target.value)} className="w-full pl-9 pr-4 py-2.5 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 text-slate-200 placeholder:text-slate-500 transition shadow-xs" />
{loadingAwards ? ( ) : filteredAwards.length === 0 ? ( ) : ( filteredAwards.map((awd) => { const statusBadge = getApprovalStatusBadge(awd.status); return ( ); }) )}
Approval Reference # Recipient Organisation Funding Programme Approved Amount (ZAR) Status Date Registered
Loading registered awards...
{searchTerm ? 'No awards match your search query.' : 'No registered awards found.'}
{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 3: Budgets (M05) */} {activeTab === 'budgets' && (
setSearchTerm(e.target.value)} className="w-full pl-9 pr-4 py-2.5 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 text-slate-200 placeholder:text-slate-500 transition shadow-xs" />
{loadingBudgets ? ( ) : filteredBudgets.length === 0 ? ( ) : ( filteredBudgets.map((b) => { const statusBadge = getApprovalStatusBadge(b.status ?? ApprovalStatus.Approved); return ( ); }) )}
Budget Line Category Project ID Approved Amount (ZAR) Date Established Status Created By Actions
Loading budget lines...
{searchTerm ? 'No budget lines match your search query.' : 'No budget lines established yet.'}
{b.name} ID: #{b.id} Project #{b.projectId} {formatZAR(b.amount)} {formatDate(b.createdAt)} {statusBadge.label} {getUserEmail(b.createdBy)}
{b.status !== ApprovalStatus.Approved && ( )}
)} {/* Tab 4: Disbursements & Approval Trigger (M06) */} {activeTab === 'disbursements' && (
setSearchTerm(e.target.value)} className="w-full pl-9 pr-4 py-2.5 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 text-slate-200 placeholder:text-slate-500 transition shadow-xs" />
Disbursement Control: Disbursement cannot be released unless contractual conditions are verified. Duplicate approvals blocked.
{loadingDisbursements ? ( ) : filteredDisbursements.length === 0 ? ( ) : ( filteredDisbursements.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...
{searchTerm ? 'No disbursements match your search query.' : 'No disbursement tranches recorded yet.'}
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" helpText="Register an approved grant allocation. The award amount cannot exceed the sum of all allocated budgets for the chosen project." >
{/* Project Budget Ceiling Card */}
Project Budget Ceiling Project #{watchedAwardProjectId}
Allocated Budget Sum 0 ? 'text-emerald-400' : 'text-rose-400'}`}> {formatZAR(totalAllocatedBudgetForAwardProject)}
Budget Lines {selectedProjectBudgetsForAward.length} Category Line{selectedProjectBudgetsForAward.length === 1 ? '' : 's'}
{totalAllocatedBudgetForAwardProject > 0 && (
Max allowable award for this project:
)}
{/* Validation Alert Banners */} {hasNoBudgetForAwardProject && (
No Budget Allocated The chosen project has ZAR 0.00 in allocated budget lines. Establish budget lines in the Budgets tab before creating an award.
)} {isAwardExceedingBudget && !hasNoBudgetForAwardProject && (
Exceeds Project Budget Ceiling The entered award amount of{' '} {formatZAR(watchedAwardAmount)}{' '} exceeds the sum of all allocated budgets for the chosen project ({' '} {formatZAR(totalAllocatedBudgetForAwardProject)}{' '} ) by{' '} {formatZAR(watchedAwardAmount - totalAllocatedBudgetForAwardProject)} . Creation is disallowed.
)} {isAwardNegativeOrZero && (
Invalid Award Value Approved funding amount must be strictly greater than zero. Negative or zero values are prohibited.
)}
{awardForm.formState.errors.referenceNo && (

{awardForm.formState.errors.referenceNo.message}

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

{awardForm.formState.errors.amount.message}

)}
{/* Modal: Add Budget */} setIsBudgetModalOpen(false)} title="Add Project Budget Line" subtitle="Maintain approved expenditure breakdown and commitments" helpText="Establishes financial lines for project cost categories. Ensures all disbursements are mapped to audited budget ceilings." >
budgetMutation.mutate(data as any))} className="space-y-4">
{/* Modal: Update Budget Status */} { setIsEditBudgetModalOpen(false); setEditingBudget(null); }} title={`Update Budget: ${editingBudget?.name || ''}`} subtitle={`Budget Line #${editingBudget?.id || ''} for Project #${editingBudget?.projectId || ''}`} helpText="Update the statutory approval and compliance status for this project budget category line." >
Budget Category: {editingBudget?.name}
Allocated Amount: {editingBudget ? formatZAR(editingBudget.amount) : '—'}
{/* Modal: Schedule or Edit Disbursement */} { setIsDisbursementModalOpen(false); setEditingDisbursement(null); }} title={editingDisbursement ? 'Edit Disbursement Tranche' : 'Schedule Disbursement Tranche'} subtitle={ editingDisbursement ? `Updating payment tranche DSB-${editingDisbursement.id}` : 'Link disbursement to milestone verification and conditions precedent' } helpText="Create or adjust scheduled disbursement tranches. Payment releases are strictly limited to the project's allocated budget ceiling minus all existing disbursements." >
{!editingDisbursement && (
)} {/* Budget Ceiling & Headroom Calculation Card */}
Disbursement Budget Ceiling Project #{watchedDisbursementProjectId}
Allocated Budget {formatZAR(totalAllocatedProjectBudget)}
Prior Disbursements {formatZAR(totalExistingProjectDisbursements)}
Available Disbursement Capacity 0 ? 'text-emerald-400' : 'text-rose-400' }`} > {formatZAR(availableDisbursementCapacity)}
{availableDisbursementCapacity > 0 && !editingDisbursement && ( )}
{/* Validation Alert Banners */} {hasNoProjectBudget && (
No Budget Lines Allocated This project currently has ZAR 0.00 in allocated budgets. You must establish budget lines in the Budgets tab before creating disbursements.
)} {isDisbursementExceedingBudget && !hasNoProjectBudget && (
Exceeds Project Budget Ceiling The requested tranche of{' '} {formatZAR(watchedDisbursementAmount)}{' '} exceeds the available budget capacity of{' '} {formatZAR(availableDisbursementCapacity)}{' '} by{' '} {formatZAR(watchedDisbursementAmount - availableDisbursementCapacity)} . Creation is disallowed.
)} {isDisbursementNegativeOrZero && (
Invalid Tranche Value Disbursement amounts must be strictly positive. Negative or zero values are prohibited.
)}
{availableDisbursementCapacity > 0 && watchedDisbursementAmount > 0 && !isDisbursementExceedingBudget && ( Remaining after tranche: {formatZAR(availableDisbursementCapacity - watchedDisbursementAmount)} )}
{disbursementForm.formState.errors.amount && (

{disbursementForm.formState.errors.amount.message}

)}
); };