import React, { useState, useRef } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { FileText, Plus, Upload, Download, Clock, CheckCircle2, AlertTriangle, Building2, FolderKanban, Briefcase, Calendar, Lock, RefreshCw, Search, Filter, ExternalLink, Edit2, FileCheck, Paperclip, Check, ShieldCheck, RotateCcw, } from 'lucide-react'; import { getContracts, createContract, updateContract, uploadContractFile, getContractSecureLink, executeContract, getAwards, getOrganisations, getAllProgrammes, getProjects, } from '../../services/api'; import { toolsService } from '../../services/toolsService'; import { Modal } from '../ui/Modal'; import { useToast } from '../ui/Toast'; import { ProblemDetailsAlert } from '../ui/ProblemDetailsAlert'; import { HelpTooltip } from '../ui/HelpTooltip'; import { ExportButton } from '../ui/ExportButton'; import { ExportColumn } from '../../utils/exportUtils'; import { useTableSort, SortableTh } from '../../utils/tableSort'; import { useRbac } from '../../auth/rbac'; import { Contract, Award, Organisation, Programme, Project, ApprovalStatus, ContractStatus, } from '../../types'; import { formatZAR, formatDate, getContractStatusBadge, getApprovalStatusBadge, CONTRACT_STATUS_OPTIONS, } from '../../lib/formatters'; interface ContractsManagementProps { onOpenCreateModal?: () => void; isCreateModalOpenExternal?: boolean; onCloseCreateModalExternal?: () => void; } export const ContractsManagement: React.FC = ({ isCreateModalOpenExternal, onCloseCreateModalExternal, }) => { const queryClient = useQueryClient(); const { success, error: toastError, info } = useToast(); const { canAdd, canApprove, canApproveRecord, isMaker } = useRbac(); // Component state const [internalCreateModalOpen, setInternalCreateModalOpen] = useState(false); const isCreateModalOpen = isCreateModalOpenExternal ?? internalCreateModalOpen; const setCreateModalOpen = (open: boolean) => { if (onCloseCreateModalExternal && !open) { onCloseCreateModalExternal(); } setInternalCreateModalOpen(open); }; const [statusFilter, setStatusFilter] = useState('all'); const [searchTerm, setSearchTerm] = useState(''); // Selected contract for status update const [contractToUpdate, setContractToUpdate] = useState(null); const [newStatus, setNewStatus] = useState(ContractStatus.Active); // Selected contract for file upload / replace const [contractForFileUpload, setContractForFileUpload] = useState(null); const [selectedUploadFile, setSelectedUploadFile] = useState(null); const fileInputRef = useRef(null); // Create contract form state const [selectedAwardId, setSelectedAwardId] = useState(''); const [totalValueInput, setTotalValueInput] = useState(0); const [effectiveAtInput, setEffectiveAtInput] = useState( new Date().toISOString().split('T')[0] ); const [expiresAtInput, setExpiresAtInput] = useState(() => { const d = new Date(); d.setFullYear(d.getFullYear() + 1); return d.toISOString().split('T')[0]; }); const [contractPdfFile, setContractPdfFile] = useState(null); const [onlyApprovedAwards, setOnlyApprovedAwards] = useState(true); // API Error states for modal banner display const [createApiError, setCreateApiError] = useState(null); const [uploadApiError, setUploadApiError] = useState(null); const [updateApiError, setUpdateApiError] = useState(null); // Loading indicator for secure download const [downloadingContractId, setDownloadingContractId] = useState(null); // Queries const { data: contracts = [], isLoading: loadingContracts, refetch: refetchContracts, isRefetching: refetchingContracts, } = useQuery({ queryKey: ['contracts'], queryFn: () => getContracts(1, 100), }); const { data: awards = [], isLoading: loadingAwards } = useQuery({ queryKey: ['awards'], queryFn: () => getAwards(1, 100), }); const { data: organisations = [] } = useQuery({ queryKey: ['organisations'], queryFn: () => getOrganisations(1, 100), }); const { data: programmes = [] } = useQuery({ queryKey: ['all-programmes'], queryFn: () => getAllProgrammes(1, 100), }); const { data: projects = [] } = useQuery({ queryKey: ['projects'], queryFn: () => getProjects(), }); // Helper to match award and its related metadata const getAwardForContract = (contract: Contract): Award | undefined => { return awards.find( (a) => String(a.id) === String(contract.awardId) || toolsService.encodeIdSync(a.id) === String(contract.awardId) ); }; const getOrganisationForAward = (award?: Award): Organisation | undefined => { if (!award) return undefined; return organisations.find( (o) => String(o.id) === String(award.organisationId) || toolsService.encodeIdSync(o.id) === String(award.organisationId) ); }; const getProgrammeForAward = (award?: Award): Programme | undefined => { if (!award) return undefined; return programmes.find( (p) => String(p.id) === String(award.programmeId) || toolsService.encodeIdSync(p.id) === String(award.programmeId) ); }; const getProjectForAward = (award?: Award, prog?: Programme): Project | undefined => { if (!award) return undefined; // Match by direct project ID on award or by programme link return projects.find( (pr) => (award.projectId && (String(pr.id) === String(award.projectId) || toolsService.encodeIdSync(pr.id) === String(award.projectId))) || (award.programmeId && String(pr.programmeId) === String(award.programmeId)) || (prog && String(pr.programmeId) === String(prog.id)) ); }; // Filter awards for dropdown: Approved status or all const filteredAwardsForDropdown = awards.filter((a) => { if (onlyApprovedAwards) { return Number(a.status) === ApprovalStatus.Approved; } return true; }); // When selected award changes in modal, auto-populate value and defaults const handleAwardSelect = (awardIdStr: string) => { setSelectedAwardId(awardIdStr); setCreateApiError(null); const award = awards.find( (a) => String(a.id) === awardIdStr || toolsService.encodeIdSync(a.id) === awardIdStr ); if (award) { if (award.amount && award.amount > 0) { setTotalValueInput(award.amount); } } }; // Selected award metadata preview const activeSelectedAward = awards.find( (a) => String(a.id) === selectedAwardId || toolsService.encodeIdSync(a.id) === selectedAwardId ); const activeSelectedOrg = getOrganisationForAward(activeSelectedAward); const activeSelectedProg = getProgrammeForAward(activeSelectedAward); const activeSelectedProj = getProjectForAward(activeSelectedAward, activeSelectedProg); // Mutations const createContractMutation = useMutation({ mutationFn: async () => { setCreateApiError(null); if (!selectedAwardId) { throw new Error('Please select an approved award from the list.'); } if (!totalValueInput || totalValueInput <= 0) { throw new Error('Please enter a valid total contract value greater than 0.'); } if (!contractPdfFile) { throw new Error('Please upload a PDF document of the contract.'); } // Step 1: Create contract via POST /v1/api/grants/contracts (awardId will be hashed by service) const createdContract = await createContract({ awardId: selectedAwardId, totalValue: Number(totalValueInput), effectiveAt: new Date(effectiveAtInput).toISOString(), expiresAt: new Date(expiresAtInput).toISOString(), }); // Target contractId for upload const contractIdToUploadAgainst = createdContract?.id ?? (createdContract as any)?.contractId ?? selectedAwardId; // Step 2: Upload PDF document to POST /v1/api/grants/contracts/{contractId} if (contractPdfFile && contractIdToUploadAgainst) { await uploadContractFile(contractIdToUploadAgainst, contractPdfFile); } return createdContract; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contracts'] }); queryClient.invalidateQueries({ queryKey: ['awards'] }); queryClient.invalidateQueries({ queryKey: ['dashboard'] }); success( 'Contract Created & Document Uploaded', 'The contract record has been registered and the PDF document securely stored in S3.' ); setCreateModalOpen(false); resetCreateForm(); }, onError: (err: unknown) => { setCreateApiError(err); toastError('Contract Creation Failed', err); }, }); const uploadFileMutation = useMutation({ mutationFn: async () => { setUploadApiError(null); if (!contractForFileUpload) throw new Error('No contract selected for upload.'); if (!selectedUploadFile) throw new Error('Please select a PDF document to upload.'); await uploadContractFile(contractForFileUpload.id, selectedUploadFile); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contracts'] }); success('Contract Document Updated', 'New contract PDF document uploaded successfully.'); setContractForFileUpload(null); setSelectedUploadFile(null); }, onError: (err: unknown) => { setUploadApiError(err); toastError('Document Upload Failed', err); }, }); const updateStatusMutation = useMutation({ mutationFn: async () => { setUpdateApiError(null); if (!contractToUpdate) throw new Error('No contract selected.'); await updateContract({ contractId: contractToUpdate.id, status: newStatus, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contracts'] }); queryClient.invalidateQueries({ queryKey: ['dashboard'] }); success('Contract Status Updated', 'The contract lifecycle status has been updated.'); setContractToUpdate(null); }, onError: (err: unknown) => { setUpdateApiError(err); toastError('Status Update Failed', err); }, }); const resetCreateForm = () => { setSelectedAwardId(''); setTotalValueInput(0); setContractPdfFile(null); setCreateApiError(null); }; // Secure download link handler (15-minute on-demand link) const handleDownloadSecureLink = async (contract: Contract) => { setDownloadingContractId(contract.id); try { const url = await getContractSecureLink(contract.id); 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( '15-Minute Secure Link Generated', 'A time-limited download URL has been generated on-demand and opened.' ); } else { toastError('Download Link Unavailable', 'The server returned an empty secure download URL.'); } } catch (err: unknown) { toastError('Failed to Retrieve Secure Link', err); } finally { setDownloadingContractId(null); } }; // Filter and search contracts const filteredContracts = contracts.filter((c) => { // Status filter if (statusFilter !== 'all') { const statusNum = Number(c.status); if (statusNum !== Number(statusFilter)) return false; } // Search filter if (searchTerm.trim()) { const q = searchTerm.toLowerCase(); const cId = String(c.id).toLowerCase(); const aId = String(c.awardId).toLowerCase(); const award = getAwardForContract(c); const org = getOrganisationForAward(award); const prog = getProgrammeForAward(award); const proj = getProjectForAward(award, prog); const orgName = org?.name?.toLowerCase() || ''; const progName = prog?.name?.toLowerCase() || ''; const projName = proj?.name?.toLowerCase() || ''; if ( !cId.includes(q) && !aId.includes(q) && !orgName.includes(q) && !progName.includes(q) && !projName.includes(q) ) { return false; } } return true; }); // Calculate summary metrics const totalContractCount = contracts.length; const activeContractsValue = contracts .filter((c) => Number(c.status) === ContractStatus.Active) .reduce((sum, c) => sum + (Number(c.totalValue) || 0), 0); const pendingContractsCount = contracts.filter( (c) => Number(c.status) === ContractStatus.Draft || Number(c.status) === ContractStatus.PendingApproval ).length; const contractExportColumns: ExportColumn[] = [ { header: 'Contract ID', key: 'id' }, { header: 'Description / Title', key: 'description' }, { header: 'Project', key: 'projectId', format: (val) => (val ? `Project #${val}` : '—') }, { header: 'Award ID', key: 'awardId', format: (val) => (val ? `Award #${val}` : '—') }, { header: 'Organisation ID', key: 'organisationId', format: (val) => (val ? `Org #${val}` : '—') }, { header: 'Status', key: 'status', format: (val) => getContractStatusBadge(val).label }, { header: 'Signed Date', key: 'signedDate', format: (val) => (val ? formatDate(val) : '—') }, { header: 'Expiry Date', key: 'expiryDate', format: (val) => (val ? formatDate(val) : '—') }, { header: 'Budget Allocated', key: 'budgetAllocated', format: (val) => (val ? formatZAR(val) : '—') }, { header: 'Document URI', key: 'fileUri', format: (val) => val || '—' }, ]; // Table sorting hook const { sortedData: sortedContracts, sortColumn: contractSortCol, sortDirection: contractSortDir, handleSort: handleContractSort, } = useTableSort(filteredContracts, null, null, { id: (c: any) => c.id, awardId: (c: any) => c.totalValue || c.awardId, programmeId: (c: any) => { const a = getAwardForContract(c); const pr = getProgrammeForAward(a); return pr?.name || ''; }, organisationId: (c: any) => { const a = getAwardForContract(c); const o = getOrganisationForAward(a); return o?.name || ''; }, signedDate: (c: any) => c.signedDate, status: (c: any) => getContractStatusBadge(c.status).label, fileUri: (c: any) => c.fileUri || '', }); return (
{/* Summary Cards */}
Total Contracts
{loadingContracts ? '...' : totalContractCount}

Linked to approved grant funding awards

Active Value (ZAR)
{loadingContracts ? '...' : formatZAR(activeContractsValue)}

Legally executed active commitments

In Review / Draft
{loadingContracts ? '...' : pendingContractsCount}

Pending legal review or signature

Approved Awards Available
{loadingAwards ? '...' : awards.filter((a) => Number(a.status) === ApprovalStatus.Approved).length}

Ready to establish contractual legal binding

{/* Action and Filter Bar */}
{/* Search */}
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" />
{/* Status Filter */}
{(searchTerm || statusFilter !== 'all') && ( )}
{/* Action Buttons: Refresh, Export & Create */}
{canAdd('grant_financials') && ( )}
{/* Contracts Table */}

Contract Registry & Associated Programme Hierarchy

Showing {filteredContracts.length} of {contracts.length} contracts
{loadingContracts ? (
Loading contractual records and resolving Award links...
) : filteredContracts.length === 0 ? (

No contracts found

{searchTerm || statusFilter !== 'all' ? 'No contracts match the current search filters. Try resetting the filters.' : 'No contracts have been created yet. Click "Create Contract & Upload PDF" to link an approved award.'}

{!(searchTerm || statusFilter !== 'all') && ( )}
) : (
Contract / Ref Award & Amount Programme & Project Organisation Validity Period Status Document / S3 {sortedContracts.map((contract) => { const award = getAwardForContract(contract); const org = getOrganisationForAward(award); const prog = getProgrammeForAward(award); const proj = getProjectForAward(award, prog); const statusBadge = getContractStatusBadge(contract.status); const isDownloading = downloadingContractId === contract.id; return ( {/* Contract ID */} {/* Linked Award */} {/* Programme & Project */} {/* Organisation */} {/* Validity Period */} {/* Status */} {/* Document / File S3 Download */} {/* Actions */} ); })}
Actions
#{contract.id}
Hash: {toolsService.encodeIdSync(contract.id)}
{formatZAR(contract.totalValue)}
{award ? (
Award #{award.id} {award.referenceNo && ( ({award.referenceNo}) )}
Funded: {formatZAR(award.amount)}
{award.status && ( {getApprovalStatusBadge(award.status).label} )}
) : ( Award #{contract.awardId} )}
{prog?.name || 'Programme Linked'}
{proj?.name || (award?.projectId ? `Project #${award.projectId}` : 'Project Scope')}
{org?.name || 'Recipient Organisation'}
{org?.registrationNo && (
Reg: {org.registrationNo}
)}
{formatDate(contract.effectiveAt)}
to {formatDate(contract.expiresAt)}
{statusBadge.label}
15-min secure link
)}
{/* ======================================================== */} {/* MODAL 1: Create Contract & Initial PDF Upload Workflow */} {/* ======================================================== */} setCreateModalOpen(false)} title="Create Legal Contract & Attach PDF" >
{ e.preventDefault(); createContractMutation.mutate(); }} className="space-y-4" > {/* ProblemDetails Error Alert */} {createApiError && ( setCreateApiError(null)} title="Contract Creation Rejected" /> )} {/* Award Selection Dropdown */}

Selecting an approved award links Programme, Project, and Organisation automatically.

{/* Associated Context Preview */} {activeSelectedAward && (
Linked Award Hierarchy
Recipient Organisation: {activeSelectedOrg?.name || 'Unknown'}
Funding Programme: {activeSelectedProg?.name || 'Programme Attached'}
Associated Project: {activeSelectedProj?.name || (activeSelectedAward.projectId ? `Project #${activeSelectedAward.projectId}` : 'Portfolio Project')}
Approved Award Amount: {formatZAR(activeSelectedAward.amount)}
)} {/* Total Value */}
setTotalValueInput(Number(e.target.value))} placeholder="e.g. 500000" className="w-full bg-[#151b2c] border border-slate-700 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-blue-500" required />
{/* Dates */}
setEffectiveAtInput(e.target.value)} className="w-full bg-[#151b2c] border border-slate-700 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-blue-500" required />
setExpiresAtInput(e.target.value)} className="w-full bg-[#151b2c] border border-slate-700 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-blue-500" required />
{/* Mandatory Contract PDF File Upload */}
{contractPdfFile ? (

{contractPdfFile.name}

{(contractPdfFile.size / 1024).toFixed(1)} KB • application/pdf

) : ( )}

Upon contract creation, the PDF is immediately uploaded to S3 and linked to this contract record.

{/* Modal Actions */}
{/* ======================================================== */} {/* MODAL 2: Replace / Upload New Contract PDF File */} {/* ======================================================== */} setContractForFileUpload(null)} title={`Upload New Contract Document (Contract #${contractForFileUpload?.id})`} >
{ e.preventDefault(); uploadFileMutation.mutate(); }} className="space-y-4" > {uploadApiError && ( setUploadApiError(null)} title="File Upload Failed" /> )}

Contracts can always upload a new PDF file if there was an issue or amendment with the previous document.

{selectedUploadFile ? (

{selectedUploadFile.name}

{(selectedUploadFile.size / 1024).toFixed(1)} KB • application/pdf

) : ( )}
{/* ======================================================== */} {/* MODAL 3: Update Contract Status */} {/* ======================================================== */} setContractToUpdate(null)} title={`Update Contract Status (Contract #${contractToUpdate?.id})`} >
{ e.preventDefault(); updateStatusMutation.mutate(); }} className="space-y-4" > {updateApiError && ( setUpdateApiError(null)} title="Status Transition Failed" /> )}

Like any other data element, contracts can be updated and their status changed across the lifecycle.

{(() => { const canApproveContract = canApproveRecord('grant_financials', contractToUpdate?.createdBy); const isSelf = isMaker(contractToUpdate?.createdBy); return ( ); })()}
); };