Files
pfm/PostFundManagement.ConsoleUI/src/pages/GrantsPage.tsx
T
2026-09-11 12:13:25 +02:00

1035 lines
57 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<typeof awardSchema>;
type ContractFormValues = z.infer<typeof contractSchema>;
type ExecuteContractFormValues = z.infer<typeof executeContractSchema>;
type BudgetFormValues = z.infer<typeof budgetSchema>;
type DisbursementFormValues = z.infer<typeof disbursementSchema>;
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<Contract | null>(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<AwardFormValues>({
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<ContractFormValues>({
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<ExecuteContractFormValues>({
resolver: zodResolver(executeContractSchema) as any,
defaultValues: {
contractId: 0,
externalSignatureId: '',
signedDocumentUrl: '',
},
});
const budgetForm = useForm<BudgetFormValues>({
resolver: zodResolver(budgetSchema) as any,
defaultValues: {
projectId: projects[0]?.id ? Number(projects[0].id) : 401,
name: '',
amount: 1000000,
},
});
const disbursementForm = useForm<DisbursementFormValues>({
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 (
<div className="space-y-6 max-w-7xl mx-auto">
{/* Header & Execution Pipeline Summary */}
<div className="bg-[#0f1523]/80 backdrop-blur-md p-6 rounded-2xl border border-slate-800/80 shadow-xl space-y-5">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-bold text-white tracking-tight flex items-center gap-2">
Grant & Financial Management
</h2>
<p className="text-xs text-slate-400 mt-1">
End-to-end controlled execution thread: Funding Award Intake &rarr; Contracting &rarr; Budgeting &rarr; Condition-Checked Disbursement.
</p>
</div>
<div className="flex items-center gap-2">
{activeTab === 'awards' && (
<button
onClick={() => setIsAwardModalOpen(true)}
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition"
>
<Plus className="w-4 h-4" />
Register Approved Award
</button>
)}
{activeTab === 'contracts' && (
<button
onClick={() => setIsContractModalOpen(true)}
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition"
>
<Plus className="w-4 h-4" />
Draft Contract
</button>
)}
{activeTab === 'budgets' && (
<button
onClick={() => setIsBudgetModalOpen(true)}
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition"
>
<Plus className="w-4 h-4" />
Add Budget Line
</button>
)}
{activeTab === 'disbursements' && (
<button
onClick={() => setIsDisbursementModalOpen(true)}
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition"
>
<Plus className="w-4 h-4" />
Schedule Disbursement
</button>
)}
</div>
</div>
{/* Pipeline Summary Bar */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 pt-3 border-t border-slate-800/80 text-xs">
<div className="p-3.5 rounded-xl bg-slate-900/60 border border-slate-800/80 flex items-center justify-between">
<span className="text-slate-400 font-medium">Total Awards Registered</span>
<span className="font-bold text-white font-mono">{formatCompactZAR(totalAwardValue)}</span>
</div>
<div className="p-3.5 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-between">
<span className="text-emerald-400 font-medium">Disbursed (Verified & Paid)</span>
<span className="font-bold text-emerald-300 font-mono">{formatCompactZAR(totalDisbursedValue)}</span>
</div>
<div className="p-3.5 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-between">
<span className="text-amber-400 font-medium">Pending Release</span>
<span className="font-bold text-amber-300 font-mono">{formatCompactZAR(pendingDisbursementValue)}</span>
</div>
</div>
</div>
{/* Tabs */}
<div className="flex gap-2 border-b border-slate-800/80 pb-3 overflow-x-auto">
<button
onClick={() => setActiveTab('awards')}
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 ${activeTab === 'awards'
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
}`}
>
<Coins className="w-4 h-4" />
Awards Intake ({awards.length})
</button>
<button
onClick={() => setActiveTab('contracts')}
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 ${activeTab === 'contracts'
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
}`}
>
<FileText className="w-4 h-4" />
Contracts & Agreements ({contracts.length})
</button>
<button
onClick={() => setActiveTab('budgets')}
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 ${activeTab === 'budgets'
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
}`}
>
<PieChart className="w-4 h-4" />
Budget Lines ({budgets.length})
</button>
<button
onClick={() => setActiveTab('disbursements')}
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 ${activeTab === 'disbursements'
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
}`}
>
<Banknote className="w-4 h-4" />
Disbursements & Approvals ({disbursements.length})
</button>
</div>
{/* Tab 1: Awards Table (M01) */}
{activeTab === 'awards' && (
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
<th className="py-3.5 px-5">Approval Reference #</th>
<th className="py-3.5 px-4">Recipient Organisation</th>
<th className="py-3.5 px-4">Funding Programme</th>
<th className="py-3.5 px-4 text-right">Approved Amount (ZAR)</th>
<th className="py-3.5 px-4">Status</th>
<th className="py-3.5 px-5 text-right">Date Registered</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{loadingAwards ? (
<tr>
<td colSpan={6} className="py-8 text-center text-slate-500">
Loading registered awards...
</td>
</tr>
) : (
awards.map((awd) => {
const statusBadge = getApprovalStatusBadge(awd.status);
return (
<tr key={awd.id} className="hover:bg-slate-800/40 transition-colors">
<td className="py-3.5 px-5">
<span className="font-mono font-bold text-white block">{awd.referenceNo}</span>
<span className="text-[10px] text-slate-500 font-mono">System ID: #{awd.id}</span>
</td>
<td className="py-3.5 px-4 text-slate-200 font-medium">
{awd.organisationName || `Org #${awd.organisationId}`}
</td>
<td className="py-3.5 px-4 text-slate-400">
{awd.programmeName || `Programme #${awd.programmeId}`}
</td>
<td className="py-3.5 px-4 text-right font-mono font-bold text-white text-xs">
{formatZAR(awd.amount)}
</td>
<td className="py-3.5 px-4">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${statusBadge.className}`}>
{statusBadge.label}
</span>
</td>
<td className="py-3.5 px-5 text-right font-mono text-slate-400">
{formatDate(awd.createdAt)}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
)}
{/* Tab 2: Contracts Table (M02) */}
{activeTab === 'contracts' && (
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
<th className="py-3.5 px-5">Contract ID</th>
<th className="py-3.5 px-4">Associated Award</th>
<th className="py-3.5 px-4 text-right">Total Contract Value</th>
<th className="py-3.5 px-4">Effective / Expiry</th>
<th className="py-3.5 px-4">Status</th>
<th className="py-3.5 px-5 text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{loadingContracts ? (
<tr>
<td colSpan={6} className="py-8 text-center text-slate-500">
Loading contracts...
</td>
</tr>
) : (
contracts.map((c) => {
const statusBadge = getContractStatusBadge(c.status);
const isExecuted = c.status === ContractStatus.Executed;
return (
<tr key={c.id} className="hover:bg-slate-800/40 transition-colors">
<td className="py-3.5 px-5 font-mono font-bold text-white">
CTR-{c.id}
</td>
<td className="py-3.5 px-4 text-slate-300 font-medium">Award #{c.awardId}</td>
<td className="py-3.5 px-4 text-right font-mono font-bold text-white">
{formatZAR(c.totalValue)}
</td>
<td className="py-3.5 px-4 text-slate-400 font-mono text-[11px]">
{formatDate(c.effectiveAt)} &rarr; {formatDate(c.expiresAt)}
</td>
<td className="py-3.5 px-4">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold border ${statusBadge.className}`}>
{statusBadge.label}
</span>
</td>
<td className="py-3.5 px-5 text-right">
{isExecuted ? (
<span className="inline-flex items-center gap-1.5 text-[11px] text-emerald-400 font-semibold bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20">
<Lock className="w-3.5 h-3.5" />
Locked
</span>
) : (
<button
onClick={() => openExecuteModal(c)}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-lg shadow-md shadow-blue-500/20 transition"
>
<FileCheck2 className="w-3.5 h-3.5" />
Execute Contract
</button>
)}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
)}
{/* Tab 3: Budgets (M05) */}
{activeTab === 'budgets' && (
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
<th className="py-3.5 px-5">Budget Line Category</th>
<th className="py-3.5 px-4">Project ID</th>
<th className="py-3.5 px-4 text-right">Approved Amount (ZAR)</th>
<th className="py-3.5 px-4">Date Established</th>
<th className="py-3.5 px-5 text-right">Created By</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{loadingBudgets ? (
<tr>
<td colSpan={5} className="py-8 text-center text-slate-500">
Loading budget lines...
</td>
</tr>
) : (
budgets.map((b) => (
<tr key={b.id} className="hover:bg-slate-800/40 transition-colors">
<td className="py-3.5 px-5 font-semibold text-white">{b.name}</td>
<td className="py-3.5 px-4 text-slate-400 font-mono">Project #{b.projectId}</td>
<td className="py-3.5 px-4 text-right font-mono font-bold text-white">
{formatZAR(b.amount)}
</td>
<td className="py-3.5 px-4 font-mono text-slate-400">{formatDate(b.createdAt)}</td>
<td className="py-3.5 px-5 text-right font-mono text-slate-400">{getUserEmail(b.createdBy)}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
{/* Tab 4: Disbursements & Approval Trigger (M06) */}
{activeTab === 'disbursements' && (
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
<div className="p-4 border-b border-slate-800/80 bg-amber-500/10 flex items-center justify-between">
<div className="flex items-center gap-2 text-xs text-amber-300">
<ShieldCheck className="w-4 h-4 text-amber-400" />
<span className="font-semibold">Disbursement Control:</span>
<span className="text-amber-200/80">Disbursement cannot be released unless contractual conditions are verified. Duplicate approvals blocked.</span>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
<th className="py-3.5 px-5">Disbursement ID</th>
<th className="py-3.5 px-4">Project ID</th>
<th className="py-3.5 px-4">Milestone Association</th>
<th className="py-3.5 px-4 text-right">Tranche Amount</th>
<th className="py-3.5 px-4">Payment Reference</th>
<th className="py-3.5 px-4">Approval Status</th>
<th className="py-3.5 px-5 text-right">Approval Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{loadingDisbursements ? (
<tr>
<td colSpan={7} className="py-8 text-center text-slate-500">
Loading disbursements...
</td>
</tr>
) : (
disbursements.map((d) => {
const statusBadge = getApprovalStatusBadge(d.status);
const isApproved = d.status === ApprovalStatus.Approved;
return (
<tr key={d.id} className="hover:bg-slate-800/40 transition-colors">
<td className="py-3.5 px-5 font-mono font-bold text-white">
DSB-{d.id}
</td>
<td className="py-3.5 px-4 text-slate-300 font-mono">Project #{d.projectId}</td>
<td className="py-3.5 px-4 text-slate-400 font-medium">
{d.milestoneId ? `Milestone #${d.milestoneId}` : 'Initial Advance Tranche'}
</td>
<td className="py-3.5 px-4 text-right font-mono font-bold text-white text-xs">
{formatZAR(d.amount)}
</td>
<td className="py-3.5 px-4 font-mono text-slate-400 text-[11px]">
{d.paymentReference || <span className="text-slate-500 italic">Pending Release</span>}
</td>
<td className="py-3.5 px-4">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${statusBadge.className}`}>
{statusBadge.label}
</span>
</td>
<td className="py-3.5 px-5 text-right">
{isApproved ? (
<span className="inline-flex items-center gap-1 text-xs text-emerald-400 font-semibold bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20">
<CheckCircle className="w-3.5 h-3.5" />
Released
</span>
) : (
<button
onClick={() => approveMutation.mutate(d.id)}
disabled={approveMutation.isPending}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-lg shadow-md shadow-blue-500/20 transition"
>
<CheckCircle className="w-3.5 h-3.5" />
Approve Payment
</button>
)}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
)}
{/* Modal: Register Approved Award */}
<Modal
isOpen={isAwardModalOpen}
onClose={() => setIsAwardModalOpen(false)}
title="Register Approved Funding Award"
subtitle="Step 1 of 3 — Award Details"
>
<form onSubmit={awardForm.handleSubmit((data) => awardMutation.mutate(data as any))} className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
Approval / Reference Number *
</label>
<input
type="text"
{...awardForm.register('referenceNo')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-mono transition"
placeholder="AWD-2026-NSI-088"
/>
{awardForm.formState.errors.referenceNo && (
<p className="text-[10px] text-rose-400 mt-1">{awardForm.formState.errors.referenceNo.message}</p>
)}
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
Approved Funding Amount (ZAR) *
</label>
<input
type="number"
{...awardForm.register('amount')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-mono transition"
/>
{awardForm.formState.errors.amount && (
<p className="text-[10px] text-rose-400 mt-1">{awardForm.formState.errors.amount.message}</p>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
Recipient Organisation [search] *
</label>
<select
{...awardForm.register('organisationId')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition [&>option]:bg-slate-900 [&>option]:text-white"
>
{organisations.map((o) => (
<option key={o.id} value={o.id}>
{o.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
Funding Programme / Instrument *
</label>
<select
{...awardForm.register('programmeId')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition [&>option]:bg-slate-900 [&>option]:text-white"
>
{programmes.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Funding Period *</label>
<input
type="text"
{...awardForm.register('fundingPeriod')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition"
placeholder="e.g. 24 Months (1 Oct 2026 – 30 Sep 2028)"
/>
</div>
<div className="p-3 rounded-xl bg-blue-500/10 border border-blue-500/20 text-[11px] text-blue-300 space-y-1">
<span className="font-bold block">Workflow Automation:</span>
<span>Submitting this record will automatically initiate the Contract/Agreement workflow.</span>
</div>
<div className="pt-3 border-t border-slate-800/80 flex justify-end gap-2">
<button
type="button"
onClick={() => setIsAwardModalOpen(false)}
className="px-4 py-2 text-xs font-medium text-slate-300 bg-slate-800/80 hover:bg-slate-700 rounded-xl transition"
>
Cancel
</button>
<button
type="submit"
disabled={awardMutation.isPending}
className="px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition"
>
{awardMutation.isPending ? 'Submitting...' : 'Submit Award & Initiate Contract'}
</button>
</div>
</form>
</Modal>
{/* Modal: Draft Contract */}
<Modal
isOpen={isContractModalOpen}
onClose={() => setIsContractModalOpen(false)}
title="Draft Contract Agreement"
subtitle="Convert approved funding decision into legally enforceable terms"
>
<form onSubmit={contractForm.handleSubmit((data) => contractMutation.mutate(data as any))} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Select Award *</label>
<select
{...contractForm.register('awardId')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition [&>option]:bg-slate-900 [&>option]:text-white"
>
{awards.map((a) => (
<option key={a.id} value={a.id}>
{a.referenceNo} — {formatZAR(a.amount)}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Contract Total Value (ZAR) *</label>
<input
type="number"
{...contractForm.register('totalValue')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-mono transition"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Effective Date *</label>
<input
type="date"
{...contractForm.register('effectiveAt')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Expiry Date *</label>
<input
type="date"
{...contractForm.register('expiresAt')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition"
/>
</div>
</div>
<div className="pt-3 border-t border-slate-800/80 flex justify-end gap-2">
<button
type="button"
onClick={() => setIsContractModalOpen(false)}
className="px-4 py-2 text-xs font-medium text-slate-300 bg-slate-800/80 hover:bg-slate-700 rounded-xl transition"
>
Cancel
</button>
<button
type="submit"
disabled={contractMutation.isPending}
className="px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition"
>
{contractMutation.isPending ? 'Generating...' : 'Create Contract'}
</button>
</div>
</form>
</Modal>
{/* Modal: Execute Contract */}
<Modal
isOpen={isExecuteModalOpen}
onClose={() => setIsExecuteModalOpen(false)}
title="Execute Contract & Lock Agreement"
subtitle={`Locking Contract #${selectedContractForExecution?.id} as Authoritative Version`}
>
<form onSubmit={executeForm.handleSubmit((data) => executeMutation.mutate(data as any))} className="space-y-4">
<div className="p-3.5 bg-slate-900/60 border border-slate-800/80 rounded-xl text-xs space-y-1 text-slate-300">
<p>
<strong className="text-white">Obligation:</strong> Once executed, the agreement is locked and downstream budget control and
disbursement requests are enabled.
</p>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
External Electronic Signature ID *
</label>
<input
type="text"
{...executeForm.register('externalSignatureId')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-mono transition"
placeholder="e.g. SIG-DOCUSIGN-993821"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Signed Document URL / Vault *</label>
<input
type="url"
{...executeForm.register('signedDocumentUrl')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-mono transition"
/>
</div>
<div className="pt-3 border-t border-slate-800/80 flex justify-end gap-2">
<button
type="button"
onClick={() => setIsExecuteModalOpen(false)}
className="px-4 py-2 text-xs font-medium text-slate-300 bg-slate-800/80 hover:bg-slate-700 rounded-xl transition"
>
Cancel
</button>
<button
type="submit"
disabled={executeMutation.isPending}
className="px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition"
>
{executeMutation.isPending ? 'Executing...' : 'Confirm Execution & Lock'}
</button>
</div>
</form>
</Modal>
{/* Modal: Add Budget */}
<Modal
isOpen={isBudgetModalOpen}
onClose={() => setIsBudgetModalOpen(false)}
title="Add Project Budget Line"
subtitle="Maintain approved expenditure breakdown and commitments"
>
<form onSubmit={budgetForm.handleSubmit((data) => budgetMutation.mutate(data as any))} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Associated Project *</label>
<select
{...budgetForm.register('projectId')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition [&>option]:bg-slate-900 [&>option]:text-white"
>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Budget Category Name *</label>
<input
type="text"
{...budgetForm.register('name')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition"
placeholder="e.g. Specialized Equipment & Prototype Probes"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Approved Line Amount (ZAR) *</label>
<input
type="number"
{...budgetForm.register('amount')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-mono transition"
/>
</div>
<div className="pt-3 border-t border-slate-800/80 flex justify-end gap-2">
<button
type="button"
onClick={() => setIsBudgetModalOpen(false)}
className="px-4 py-2 text-xs font-medium text-slate-300 bg-slate-800/80 hover:bg-slate-700 rounded-xl transition"
>
Cancel
</button>
<button
type="submit"
disabled={budgetMutation.isPending}
className="px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition"
>
{budgetMutation.isPending ? 'Saving...' : 'Add Budget Line'}
</button>
</div>
</form>
</Modal>
{/* Modal: Schedule Disbursement */}
<Modal
isOpen={isDisbursementModalOpen}
onClose={() => setIsDisbursementModalOpen(false)}
title="Schedule Disbursement Tranche"
subtitle="Link disbursement to milestone verification and conditions precedent"
>
<form onSubmit={disbursementForm.handleSubmit((data) => disbursementMutation.mutate(data as any))} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Select Project *</label>
<select
{...disbursementForm.register('projectId')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition [&>option]:bg-slate-900 [&>option]:text-white"
>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Linked Milestone Deliverable</label>
<select
{...disbursementForm.register('milestoneId')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition [&>option]:bg-slate-900 [&>option]:text-white"
>
<option value="">-- Advance Tranche (No linked milestone) --</option>
{milestones.map((m) => (
<option key={m.id} value={m.id}>
{m.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Tranche Amount (ZAR) *</label>
<input
type="number"
{...disbursementForm.register('amount')}
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-mono transition"
/>
</div>
<div className="pt-3 border-t border-slate-800/80 flex justify-end gap-2">
<button
type="button"
onClick={() => setIsDisbursementModalOpen(false)}
className="px-4 py-2 text-xs font-medium text-slate-300 bg-slate-800/80 hover:bg-slate-700 rounded-xl transition"
>
Cancel
</button>
<button
type="submit"
disabled={disbursementMutation.isPending}
className="px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition"
>
{disbursementMutation.isPending ? 'Scheduling...' : 'Schedule Disbursement'}
</button>
</div>
</form>
</Modal>
</div>
);
};