Refactored UI endpoint usage

This commit is contained in:
2026-09-20 08:26:06 +02:00
parent b703b0c64d
commit 5829bffaa0
35 changed files with 17482 additions and 648 deletions
@@ -48,6 +48,7 @@ import {
getAllMilestones,
} from '../services/api';
import { Modal } from '../components/ui/Modal';
import { HelpTooltip } from '../components/ui/HelpTooltip';
import { useToast } from '../components/ui/Toast';
import {
Award,
@@ -71,13 +72,13 @@ import {
getUserEmail,
} from '../lib/formatters';
// Schemas matching Figure 9 and OpenAPI constraints
// Validation schemas for grant administration
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)'),
referenceNo: z.string().min(4, 'Approval / Reference number is mandatory'),
amount: z.coerce.number().min(1, 'Approved funding amount must be greater than zero'),
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)'),
organisationId: z.coerce.number().min(1, 'Select recipient organisation'),
programmeId: z.coerce.number().min(1, 'Select funding programme'),
});
const contractSchema = z.object({
@@ -253,7 +254,7 @@ export const GrantsPage: React.FC = () => {
queryClient.invalidateQueries({ queryKey: ['contracts'] });
success(
'Contract Executed & Locked',
'Agreement locked as authoritative version (AT-02). Downstream budgets and disbursements enabled.'
'Agreement locked as authoritative version. Downstream budgets and disbursements enabled.'
);
setIsExecuteModalOpen(false);
executeForm.reset();
@@ -333,7 +334,7 @@ export const GrantsPage: React.FC = () => {
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
success(
'Payment Approved & Released',
'Conditions precedent verified. EFT reference generated and disbursement ledger updated (AT-05).'
'Conditions precedent verified. EFT reference generated and disbursement ledger updated.'
);
},
onError: (err: unknown) => {
@@ -354,8 +355,14 @@ export const GrantsPage: React.FC = () => {
const handleGetContractSecureLink = async (contractId: string | number) => {
try {
const url = await getContractSecureLink(contractId);
if (url) {
window.open(url, '_blank', 'noopener,noreferrer');
if (url && typeof url === 'string' && url.trim() !== '') {
const link = document.createElement('a');
link.href = url.trim();
link.target = '_blank';
link.rel = 'noopener noreferrer';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
success('Secure URL Retrieved', 'Temporary signed download link opened.');
} else {
toastError('Download Error', 'No active secure download link generated.');
@@ -400,9 +407,17 @@ export const GrantsPage: React.FC = () => {
<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>
<div className="flex items-center gap-2">
<h2 className="text-2xl font-bold text-white tracking-tight">
Grant & Financial Management
</h2>
<HelpTooltip
title="Grant Financial Lifecycle"
content="Enforces the statutory financial control thread: Awards (M01) -> Contract Execution & Vault (M02) -> Multi-Year Budgets (M03) -> Condition-Checked Tranche Disbursements (M04)."
size="md"
placement="right"
/>
</div>
<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>
@@ -451,15 +466,39 @@ export const GrantsPage: React.FC = () => {
{/* 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>
<div className="flex items-center gap-1.5">
<span className="text-slate-400 font-medium">Total Awards Registered</span>
<HelpTooltip
title="Total Awards"
content="Cumulative grant allocation amounts committed across all approved beneficiary entities."
size="sm"
placement="top"
/>
</div>
<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>
<div className="flex items-center gap-1.5">
<span className="text-emerald-400 font-medium">Disbursed (Verified & Paid)</span>
<HelpTooltip
title="Disbursed Capital"
content="Funds verified, authorized by accounting officer, and disbursed into beneficiary bank accounts."
size="sm"
placement="top"
/>
</div>
<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>
<div className="flex items-center gap-1.5">
<span className="text-amber-400 font-medium">Pending Release</span>
<HelpTooltip
title="Pending Release"
content="Tranches currently pending condition precedent verification, milestone sign-off, or statutory review."
size="sm"
placement="top"
/>
</div>
<span className="font-bold text-amber-300 font-mono">{formatCompactZAR(pendingDisbursementValue)}</span>
</div>
</div>
@@ -819,13 +858,22 @@ export const GrantsPage: React.FC = () => {
onClose={() => setIsAwardModalOpen(false)}
title="Register Approved Funding Award"
subtitle="Step 1 of 3 — Award Details"
helpText="Register an approved grant allocation. Once registered, the system will automatically prepare a draft contract workflow linked to this award."
>
<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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">
Approval / Reference Number *
</label>
<HelpTooltip
title="Statutory Reference Number"
content="Formal committee approval voucher or gazetted award identifier."
size="sm"
placement="top"
/>
</div>
<input
type="text"
{...awardForm.register('referenceNo')}
@@ -838,9 +886,17 @@ export const GrantsPage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
Approved Funding Amount (ZAR) *
</label>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">
Approved Funding Amount (ZAR) *
</label>
<HelpTooltip
title="Award Quantum"
content="Total approved allocation authorized by the statutory grants board."
size="sm"
placement="top"
/>
</div>
<input
type="number"
{...awardForm.register('amount')}
@@ -854,9 +910,17 @@ export const GrantsPage: React.FC = () => {
<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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">
Recipient Organisation *
</label>
<HelpTooltip
title="Beneficiary Entity"
content="Verified enterprise, university, or consortium holding the funding agreement."
size="sm"
placement="top"
/>
</div>
<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"
@@ -870,9 +934,17 @@ export const GrantsPage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
Funding Programme / Instrument *
</label>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">
Funding Programme / Instrument *
</label>
<HelpTooltip
title="Statutory Fund Instrument"
content="The national budget vote or sub-programme financing this grant award."
size="sm"
placement="top"
/>
</div>
<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"
@@ -887,7 +959,15 @@ export const GrantsPage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Funding Period *</label>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Funding Period *</label>
<HelpTooltip
title="Funding Duration"
content="Tenure of the grant agreement (e.g., 24 Months, 36 Months)."
size="sm"
placement="right"
/>
</div>
<input
type="text"
{...awardForm.register('fundingPeriod')}
@@ -926,10 +1006,19 @@ export const GrantsPage: React.FC = () => {
onClose={() => setIsContractModalOpen(false)}
title="Draft Contract Agreement"
subtitle="Convert approved funding decision into legally enforceable terms"
helpText="Draft a legally binding grant agreement. Once drafted, contracts can be executed with digital signature verification to lock the 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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Select Award *</label>
<HelpTooltip
title="Underlying Award"
content="The approved statutory funding decision forming the legal premise for this contract."
size="sm"
placement="right"
/>
</div>
<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"
@@ -943,7 +1032,15 @@ export const GrantsPage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Contract Total Value (ZAR) *</label>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Contract Total Value (ZAR) *</label>
<HelpTooltip
title="Total Contract Value"
content="Maximum statutory financial liability established under this contract agreement."
size="sm"
placement="right"
/>
</div>
<input
type="number"
{...contractForm.register('totalValue')}
@@ -953,7 +1050,15 @@ export const GrantsPage: React.FC = () => {
<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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Effective Date *</label>
<HelpTooltip
title="Effective Date"
content="The legal commencement date from which project activities become eligible for grant reimbursement."
size="sm"
placement="top"
/>
</div>
<input
type="date"
{...contractForm.register('effectiveAt')}
@@ -961,7 +1066,15 @@ export const GrantsPage: React.FC = () => {
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Expiry Date *</label>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Expiry Date *</label>
<HelpTooltip
title="Expiry Date"
content="Formal termination date after which unexpended commitments lapse."
size="sm"
placement="top"
/>
</div>
<input
type="date"
{...contractForm.register('expiresAt')}
@@ -995,6 +1108,7 @@ export const GrantsPage: React.FC = () => {
onClose={() => setIsExecuteModalOpen(false)}
title="Execute Contract & Lock Agreement"
subtitle={`Locking Contract #${selectedContractForExecution?.id} as Authoritative Version`}
helpText="Electronic execution permanently locks the agreement. After execution, the contract cannot be modified, and milestone-linked disbursements can proceed."
>
<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">
@@ -1005,9 +1119,17 @@ export const GrantsPage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
External Electronic Signature ID *
</label>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">
External Electronic Signature ID *
</label>
<HelpTooltip
title="Electronic Signature Hash"
content="Unique audit certificate or transaction ID from the signing authority (e.g. DocuSign, Adobe Sign, or government PKI)."
size="sm"
placement="right"
/>
</div>
<input
type="text"
{...executeForm.register('externalSignatureId')}
@@ -1017,7 +1139,15 @@ export const GrantsPage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Signed Document URL / Vault *</label>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Signed Document URL / Vault *</label>
<HelpTooltip
title="Authoritative Document Vault"
content="Direct secure URL or object storage path to the countersigned PDF agreement."
size="sm"
placement="right"
/>
</div>
<input
type="url"
{...executeForm.register('signedDocumentUrl')}
@@ -1050,10 +1180,19 @@ export const GrantsPage: React.FC = () => {
onClose={() => 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."
>
<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>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Associated Project *</label>
<HelpTooltip
title="Funded Project"
content="Target project entity against which expenditure is tracked."
size="sm"
placement="right"
/>
</div>
<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"
@@ -1067,7 +1206,15 @@ export const GrantsPage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Budget Category Name *</label>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Budget Category Name *</label>
<HelpTooltip
title="Category Name"
content="Standard chart of accounts line (e.g. Personnel, Research Consumables, Capital Equipment)."
size="sm"
placement="right"
/>
</div>
<input
type="text"
{...budgetForm.register('name')}
@@ -1077,7 +1224,15 @@ export const GrantsPage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Approved Line Amount (ZAR) *</label>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Approved Line Amount (ZAR) *</label>
<HelpTooltip
title="Line Allocation"
content="Ceiling limit allocated for this specific cost category."
size="sm"
placement="right"
/>
</div>
<input
type="number"
{...budgetForm.register('amount')}
@@ -1117,11 +1272,20 @@ export const GrantsPage: React.FC = () => {
? `Updating payment tranche DSB-${editingDisbursement.id}`
: 'Link disbursement to milestone verification and conditions precedent'
}
helpText="Create or adjust scheduled disbursement tranches. Payment release requires verified milestone completion and positive compliance checks."
>
<form onSubmit={disbursementForm.handleSubmit((data) => disbursementMutation.mutate(data as any))} className="space-y-4">
{!editingDisbursement && (
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Select Project *</label>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Select Project *</label>
<HelpTooltip
title="Target Project"
content="The project entity receiving this payment tranche."
size="sm"
placement="right"
/>
</div>
<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"
@@ -1136,7 +1300,15 @@ export const GrantsPage: React.FC = () => {
)}
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Linked Milestone Deliverable</label>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Linked Milestone Deliverable</label>
<HelpTooltip
title="Milestone Requirement"
content="If linked, the disbursement is held until the milestone is marked Approved/Verified."
size="sm"
placement="right"
/>
</div>
<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"
@@ -1151,7 +1323,15 @@ export const GrantsPage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">Tranche Amount (ZAR) *</label>
<div className="flex items-center gap-1 mb-1.5">
<label className="block text-xs font-semibold text-slate-300">Tranche Amount (ZAR) *</label>
<HelpTooltip
title="Tranche Quantum"
content="Exact currency value to be transferred upon authorization."
size="sm"
placement="right"
/>
</div>
<input
type="number"
{...disbursementForm.register('amount')}