import React, { useState, useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; 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 { Building2, Plus, FolderKanban, Edit2, Mail, Shield, Layers, ChevronRight, Search, Coins, Eye, RefreshCw, ArrowRight, FileText, Scale, Calendar, } from 'lucide-react'; import { getOrganisations, getOrganisation, createOrganisation, updateOrganisation, getAllPortfolios, getPortfolios, getOrganisationPortfolios, getPortfolio, createPortfolio, updatePortfolio, getAllProgrammes, getProgrammes, getProgramme, createProgramme, updateProgramme, getInstruments, getInstructions, createInstrument, updateInstrument, getRules, createRule, updateRule, getPortfolioProgrammes, getPortfolioValue, getProgrammeValue, getUsers, getUser, } 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 { useAuth } from '../auth/AuthProvider'; import { useRbac } from '../auth/rbac'; import { useTableSort, SortableTh } from '../utils/tableSort'; import { FundingInstrumentsSection, InstructionsSection } from '../components/governance/InstructionsSection'; import { RulesSection } from '../components/governance/RulesSection'; import { Organisation, Portfolio, Programme, Instrument, Instruction, Rule, User, OrganisationType, ActivityStatus, ApprovalStatus, CreateOrganisationRequest, CreatePortfolioRequest, CreateProgrammeRequest, } from '../types'; import { getOrganisationTypeLabel, getActivityStatusBadge, getOrganisationStatusBadge, getApprovalStatusBadge, formatDate, formatZAR, getUserEmail, registerUserEmail, getUserIdByEmail, ORGANISATION_TYPE_OPTIONS, ACTIVITY_STATUS_OPTIONS, APPROVAL_STATUS_OPTIONS, } from '../lib/formatters'; // Validation Schemas matching API constraints const organisationSchema = z.object({ name: z.string().min(3, 'Name must be at least 3 characters'), registrationNo: z.string().min(4, 'Registration number is mandatory'), email: z.string().email('Enter a valid email address'), idNumber: z.string().min(4, 'Government issued ID number is required'), organisationUser: z.string().min(1, 'Please select a linked user'), type: z.coerce.number().min(1).max(27), status: z.coerce.number().optional(), }); const portfolioSchema = z.object({ name: z.string().min(3, 'Portfolio name is required'), description: z.string().min(5, 'Description is required'), organisationId: z.coerce.number().min(1, 'Select a parent organisation'), ownerEmail: z.string().min(1, 'Owner email is required'), }); const programmeSchema = z.object({ name: z.string().min(3, 'Programme name is required'), portfolioId: z.coerce.number().min(1, 'Select a parent portfolio'), status: z.coerce.number().optional(), ownerEmail: z.string().min(1, 'Owner email is required'), }); type OrganisationFormValues = z.infer; type PortfolioFormValues = z.infer; type ProgrammeFormValues = z.infer; export const GovernancePage: React.FC = () => { const queryClient = useQueryClient(); const { sub, user, claims } = useAuth(); const { canAdd, canApprove, canExport } = useRbac(); const currentUserId = sub || user?.id || ''; const { success, error: toastError } = useToast(); const effectiveUserEmail = user?.email || (claims?.email as string) || (claims?.['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] as string) || (claims?.upn as string) || (sub ? getUserEmail(sub) : '') || ''; useEffect(() => { if (effectiveUserEmail) { if (sub) registerUserEmail(sub, effectiveUserEmail); if (user?.id) registerUserEmail(user.id, effectiveUserEmail); } }, [sub, user, effectiveUserEmail]); const [searchParams, setSearchParams] = useSearchParams(); const tabParam = searchParams.get('tab'); const getValidTab = (val: string | null): 'organisations' | 'portfolios' | 'programmes' | 'instruments' | 'instructions' | 'rules' => { if (val === 'portfolios' || val === 'programmes' || val === 'organisations' || val === 'instruments' || val === 'instructions' || val === 'rules') { return val === 'instructions' ? 'instruments' : val; } return 'organisations'; }; const [activeTab, setActiveTab] = useState<'organisations' | 'portfolios' | 'programmes' | 'instruments' | 'instructions' | 'rules'>(() => getValidTab(tabParam)); useEffect(() => { if (tabParam && (tabParam === 'organisations' || tabParam === 'portfolios' || tabParam === 'programmes' || tabParam === 'instruments' || tabParam === 'instructions' || tabParam === 'rules')) { setActiveTab(tabParam === 'instructions' ? 'instruments' : tabParam); } }, [tabParam]); const handleTabChange = (tab: 'organisations' | 'portfolios' | 'programmes' | 'instruments' | 'instructions' | 'rules') => { const canonicalTab = tab === 'instructions' ? 'instruments' : tab; setActiveTab(canonicalTab); setSearchParams((prev) => { const next = new URLSearchParams(prev); next.set('tab', canonicalTab); return next; }); }; const [preselectedProgForInstruction, setPreselectedProgForInstruction] = useState(null); const [preselectedProgForRule, setPreselectedProgForRule] = useState(null); const [isInstructionCreateTriggered, setIsInstructionCreateTriggered] = useState(false); const [isRuleCreateTriggered, setIsRuleCreateTriggered] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [highlightedPortfolioId, setHighlightedPortfolioId] = useState(null); const handleNavigateToPortfolioTab = (portfolioId?: number | string) => { setIsOrgDetailModalOpen(false); setSelectedOrgForDetail(null); handleTabChange('portfolios'); if (portfolioId != null) { setHighlightedPortfolioId(portfolioId); setTimeout(() => { const el = document.getElementById(`portfolio-card-${portfolioId}`); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, 150); setTimeout(() => { setHighlightedPortfolioId(null); }, 3500); } }; // Modals & Editing State const [isOrgModalOpen, setIsOrgModalOpen] = useState(false); const [editingOrg, setEditingOrg] = useState(null); const [isPortfolioModalOpen, setIsPortfolioModalOpen] = useState(false); const [editingPortfolio, setEditingPortfolio] = useState(null); const [isProgrammeModalOpen, setIsProgrammeModalOpen] = useState(false); const [editingProgramme, setEditingProgramme] = useState(null); // Inspection states const [selectedProgrammeForInstruments, setSelectedProgrammeForInstruments] = useState(null); const [isInstrumentsModalOpen, setIsInstrumentsModalOpen] = useState(false); const [selectedProgrammeForRules, setSelectedProgrammeForRules] = useState(null); const [isRulesModalOpen, setIsRulesModalOpen] = useState(false); const [selectedPortfolioForProgrammes, setSelectedPortfolioForProgrammes] = useState(null); const [isPortfolioProgrammesModalOpen, setIsPortfolioProgrammesModalOpen] = useState(false); const [selectedOrgForDetail, setSelectedOrgForDetail] = useState(null); const [isOrgDetailModalOpen, setIsOrgDetailModalOpen] = useState(false); const [selectedPortfolioForDetail, setSelectedPortfolioForDetail] = useState(null); const [isPortfolioDetailModalOpen, setIsPortfolioDetailModalOpen] = useState(false); const [selectedProgrammeForDetail, setSelectedProgrammeForDetail] = useState(null); const [isProgrammeDetailModalOpen, setIsProgrammeDetailModalOpen] = useState(false); // Queries const { data: usersDirectory = [] } = useQuery({ queryKey: ['users-directory'], queryFn: () => getUsers(1, 200), staleTime: 5 * 60 * 1000, }); const { data: organisations = [], isLoading: loadingOrgs, isFetching: isFetchingOrgs, refetch: refetchOrgs, } = useQuery({ queryKey: ['organisations'], queryFn: () => getOrganisations(), }); const { data: portfolios = [], isLoading: loadingPortfolios, isFetching: isFetchingPortfolios, refetch: refetchPortfolios, } = useQuery({ queryKey: ['portfolios'], queryFn: () => getAllPortfolios(), }); const { data: programmes = [], isLoading: loadingProgrammes, isFetching: isFetchingProgrammes, refetch: refetchProgrammes, } = useQuery({ queryKey: ['programmes'], queryFn: () => getAllProgrammes(), }); const { data: programmeInstruments = [], isLoading: loadingInstruments } = useQuery({ queryKey: ['instruments', selectedProgrammeForInstruments?.id], queryFn: () => (selectedProgrammeForInstruments ? getInstructions(selectedProgrammeForInstruments.id) : Promise.resolve([])), enabled: !!selectedProgrammeForInstruments && isInstrumentsModalOpen, }); const { data: programmeRulesList = [], isLoading: loadingProgrammeRules } = useQuery({ queryKey: ['programme-rules', selectedProgrammeForRules?.id], queryFn: () => (selectedProgrammeForRules ? getRules({ page: 1, pageSize: 200 }, selectedProgrammeForRules.id) : Promise.resolve([])), enabled: !!selectedProgrammeForRules && isRulesModalOpen, }); const { data: programmeDetailRules = [], isLoading: loadingDetailRules } = useQuery({ queryKey: ['programme-detail-rules', selectedProgrammeForDetail?.id], queryFn: () => (selectedProgrammeForDetail ? getRules({ page: 1, pageSize: 200 }, selectedProgrammeForDetail.id) : Promise.resolve([])), enabled: !!selectedProgrammeForDetail && isProgrammeDetailModalOpen, }); const { data: programmeDetailInstructions = [], isLoading: loadingDetailInstructions } = useQuery({ queryKey: ['programme-detail-instructions', selectedProgrammeForDetail?.id], queryFn: () => (selectedProgrammeForDetail ? getInstructions(selectedProgrammeForDetail.id) : Promise.resolve([])), enabled: !!selectedProgrammeForDetail && isProgrammeDetailModalOpen, }); const { data: portfolioProgrammesList = [], isLoading: loadingPortfolioProgrammes } = useQuery({ queryKey: ['portfolioProgrammes', selectedPortfolioForProgrammes?.id], queryFn: () => (selectedPortfolioForProgrammes ? getPortfolioProgrammes(selectedPortfolioForProgrammes.id) : Promise.resolve([])), enabled: !!selectedPortfolioForProgrammes && isPortfolioProgrammesModalOpen, }); const { data: portfolioDirectProgrammes = [] } = useQuery({ queryKey: ['portfolio-direct-programmes', selectedPortfolioForProgrammes?.id], queryFn: () => (selectedPortfolioForProgrammes ? getProgrammes(selectedPortfolioForProgrammes.id) : Promise.resolve([])), enabled: !!selectedPortfolioForProgrammes && isPortfolioProgrammesModalOpen, }); const { data: orgFreshDetail, isLoading: loadingOrgFreshDetail } = useQuery({ queryKey: ['organisation', selectedOrgForDetail?.id], queryFn: () => (selectedOrgForDetail ? getOrganisation(selectedOrgForDetail.id) : null), enabled: !!selectedOrgForDetail && isOrgDetailModalOpen, }); const { data: orgPortfolios = [], isLoading: loadingOrgPortfolios } = useQuery({ queryKey: ['organisation-portfolios', selectedOrgForDetail?.id], queryFn: () => (selectedOrgForDetail ? getOrganisationPortfolios(selectedOrgForDetail.id) : Promise.resolve([])), enabled: !!selectedOrgForDetail && isOrgDetailModalOpen, }); const { data: portfolioFreshDetail, isLoading: loadingPortfolioFreshDetail } = useQuery({ queryKey: ['portfolio', selectedPortfolioForDetail?.id], queryFn: () => (selectedPortfolioForDetail ? getPortfolio(selectedPortfolioForDetail.id) : null), enabled: !!selectedPortfolioForDetail && isPortfolioDetailModalOpen, }); const { data: portfolioValue = 0, isLoading: loadingPortfolioValue } = useQuery({ queryKey: ['portfolio-value', selectedPortfolioForDetail?.id], queryFn: () => (selectedPortfolioForDetail ? getPortfolioValue(selectedPortfolioForDetail.id) : Promise.resolve(0)), enabled: !!selectedPortfolioForDetail && isPortfolioDetailModalOpen, }); const { data: programmeFreshDetail, isLoading: loadingProgrammeFreshDetail } = useQuery({ queryKey: ['programme', selectedProgrammeForDetail?.id], queryFn: () => (selectedProgrammeForDetail ? getProgramme(selectedProgrammeForDetail.id) : null), enabled: !!selectedProgrammeForDetail && isProgrammeDetailModalOpen, }); const { data: programmeValue = 0, isLoading: loadingProgrammeValue } = useQuery({ queryKey: ['programme-value', selectedProgrammeForDetail?.id], queryFn: () => (selectedProgrammeForDetail ? getProgrammeValue(selectedProgrammeForDetail.id) : Promise.resolve(0)), enabled: !!selectedProgrammeForDetail && isProgrammeDetailModalOpen, }); // Owner resolution helpers const resolveOwnerId = (emailOrId: string, fallbackOwnerId?: string | number | null): string => { const trimmed = (emailOrId || '').trim(); if (!trimmed) { return fallbackOwnerId ? String(fallbackOwnerId) : currentUserId; } // If already a valid UUID if (/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(trimmed)) { return trimmed; } const cleanEmail = trimmed.toLowerCase(); // 1. Current logged-in user match if (effectiveUserEmail && cleanEmail === effectiveUserEmail.toLowerCase()) { return sub || user?.id || (fallbackOwnerId ? String(fallbackOwnerId) : currentUserId); } // 2. Reverse cache lookup const cachedId = getUserIdByEmail(cleanEmail); if (cachedId) return cachedId; // 3. Search loaded users directory const matchedUser = usersDirectory.find( (u) => u.email && u.email.trim().toLowerCase() === cleanEmail ); if (matchedUser?.id) return String(matchedUser.id); // 4. Fallback ID match check if (fallbackOwnerId) { const origEmail = getUserEmail(fallbackOwnerId); if (origEmail && origEmail.toLowerCase() === cleanEmail) { return String(fallbackOwnerId); } } // 5. Default fallback return fallbackOwnerId ? String(fallbackOwnerId) : currentUserId; }; const resolveOwnerEmailForDisplay = async (rawOwnerId?: string | number | null): Promise => { if (!rawOwnerId) { return effectiveUserEmail; } const raw = String(rawOwnerId).trim(); if (raw.includes('@')) { return raw; } // Check if it's the current user if ( (sub && raw.toLowerCase() === sub.toLowerCase()) || (user?.id && raw.toLowerCase() === user.id.toLowerCase()) ) { return effectiveUserEmail; } // Check cached email const cached = getUserEmail(raw); if (cached && cached.includes('@')) { return cached; } // Check users directory const found = usersDirectory.find((u) => String(u.id).toLowerCase() === raw.toLowerCase()); if (found?.email) { return found.email; } // If it's a UUID, fetch user canonical record from API if (/^[0-9a-fA-F-]{36}$/.test(raw)) { try { const u = await getUser(raw); if (u?.email) { registerUserEmail(u.id, u.email); return u.email; } } catch { // Fetch failed } } return effectiveUserEmail; }; // Forms const orgForm = useForm({ resolver: zodResolver(organisationSchema) as any, defaultValues: { name: '', registrationNo: '', email: '', idNumber: '', organisationUser: '', type: OrganisationType.Enterprise, status: ActivityStatus.Active, }, }); const portfolioForm = useForm({ resolver: zodResolver(portfolioSchema) as any, defaultValues: { name: '', description: '', organisationId: organisations[0]?.id ? Number(organisations[0].id) : 101, ownerEmail: effectiveUserEmail, }, }); const programmeForm = useForm({ resolver: zodResolver(programmeSchema) as any, defaultValues: { name: '', portfolioId: portfolios[0]?.id ? Number(portfolios[0].id) : 201, status: ApprovalStatus.Approved, ownerEmail: effectiveUserEmail, }, }); // Mutations const orgMutation = useMutation({ mutationFn: (data: OrganisationFormValues) => { if (editingOrg) { return updateOrganisation({ organisationId: editingOrg.id, name: data.name, registrationNo: data.registrationNo, email: data.email, idNumber: data.idNumber, organisationUser: data.organisationUser, type: Number(data.type), status: data.status !== undefined ? Number(data.status) : ActivityStatus.Active, }); } return createOrganisation({ name: data.name, registrationNo: data.registrationNo, email: data.email, idNumber: data.idNumber, organisationUser: data.organisationUser, type: Number(data.type), }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['organisations'] }); success(editingOrg ? 'Organisation updated' : 'Organisation registered successfully'); setIsOrgModalOpen(false); setEditingOrg(null); orgForm.reset(); }, onError: (err: unknown) => { toastError('Failed to save organisation', err); }, }); const portfolioMutation = useMutation({ mutationFn: (data: PortfolioFormValues) => { const resolvedOwnerId = resolveOwnerId(data.ownerEmail, editingPortfolio?.ownedBy); if (editingPortfolio) { return updatePortfolio({ portfolioId: editingPortfolio.id, name: data.name, description: data.description, organisationId: Number(data.organisationId), ownerId: resolvedOwnerId, }); } return createPortfolio({ name: data.name, description: data.description, organisationId: Number(data.organisationId), ownerId: resolvedOwnerId, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['portfolios'] }); success(editingPortfolio ? 'Portfolio updated successfully' : 'Portfolio created successfully'); setIsPortfolioModalOpen(false); setEditingPortfolio(null); portfolioForm.reset(); }, onError: (err: unknown) => { toastError('Failed to save portfolio', err); }, }); const programmeMutation = useMutation({ mutationFn: (data: ProgrammeFormValues) => { const resolvedOwnerId = resolveOwnerId(data.ownerEmail, editingProgramme?.ownedBy); if (editingProgramme) { return updateProgramme({ programmeId: editingProgramme.id, name: data.name, portfolioId: Number(data.portfolioId), status: data.status !== undefined ? Number(data.status) : undefined, ownerId: resolvedOwnerId, }); } return createProgramme({ name: data.name, portfolioId: Number(data.portfolioId), ownerId: resolvedOwnerId, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['programmes'] }); success(editingProgramme ? 'Programme updated successfully' : 'Programme created successfully'); setIsProgrammeModalOpen(false); setEditingProgramme(null); programmeForm.reset(); }, onError: (err: unknown) => { toastError('Failed to save programme', err); }, }); const openCreateOrg = () => { setEditingOrg(null); const defaultUserId = usersDirectory[0]?.id || sub || currentUserId || ''; const defaultUser = usersDirectory.find((u) => u.id === defaultUserId); orgForm.reset({ name: '', registrationNo: '', email: '', idNumber: defaultUser?.idNumber || '', organisationUser: defaultUserId, type: OrganisationType.Enterprise, status: ActivityStatus.Active, }); setIsOrgModalOpen(true); }; const openEditOrg = (org: Organisation) => { setEditingOrg(org); // 1. Resolve target linked user ID synchronously from the organisation model const targetUserId = org.userId || (org as any).UserId || (org as any).organisationUser || (org as any).OrganisationUser || org.createdBy || ''; // 2. Fast O(1) in-memory match from cached users directory const matchedUser = targetUserId ? usersDirectory.find((u) => u.id && String(u.id).toLowerCase() === String(targetUserId).toLowerCase()) : null; // 3. Resolve idNumber: prefer matchedUser's idNumber, or org's idNumber if any, otherwise blank '' const resolvedIdNumber = matchedUser?.idNumber || org.idNumber || (org as any).IdNumber || ''; const selectedUserId = matchedUser?.id || targetUserId || ''; // 4. Perform assignment immediately to form state orgForm.reset({ name: org.name || '', registrationNo: org.registrationNo || '', email: org.email || '', idNumber: resolvedIdNumber, organisationUser: selectedUserId, type: org.type || OrganisationType.Enterprise, status: org.status ?? ActivityStatus.Active, }); // 5. Instantly open modal with fully populated data (no lag, no progressive pop-in) setIsOrgModalOpen(true); // 6. Non-blocking background sync if target user wasn't in cache or org has missing data if (targetUserId && !matchedUser) { getUser(targetUserId).then((fetchedUser) => { if (fetchedUser) { queryClient.setQueryData(['users-directory'], (old = []) => { if (old.some((u) => u.id === fetchedUser.id)) return old; return [fetchedUser, ...old]; }); if (fetchedUser.idNumber && !orgForm.getValues('idNumber')) { orgForm.setValue('idNumber', fetchedUser.idNumber, { shouldValidate: true }); } if (!orgForm.getValues('organisationUser')) { orgForm.setValue('organisationUser', fetchedUser.id, { shouldValidate: true }); } } }).catch(() => {}); } }; const openCreatePortfolio = () => { setEditingPortfolio(null); portfolioForm.reset({ name: '', description: '', organisationId: organisations[0]?.id ? Number(organisations[0].id) : 101, ownerEmail: effectiveUserEmail, }); setIsPortfolioModalOpen(true); }; const openEditPortfolio = async (p: Portfolio) => { setEditingPortfolio(p); const email = await resolveOwnerEmailForDisplay(p.ownedBy); portfolioForm.reset({ name: p.name || '', description: p.description || '', organisationId: Number(p.organisationId) || 101, ownerEmail: email, }); setIsPortfolioModalOpen(true); }; const openCreateProgramme = () => { setEditingProgramme(null); programmeForm.reset({ name: '', portfolioId: portfolios[0]?.id ? Number(portfolios[0].id) : 201, status: ApprovalStatus.Approved, ownerEmail: effectiveUserEmail, }); setIsProgrammeModalOpen(true); }; const openEditProgramme = async (pr: Programme) => { setEditingProgramme(pr); const email = await resolveOwnerEmailForDisplay(pr.ownedBy); programmeForm.reset({ name: pr.name || '', portfolioId: Number(pr.portfolioId) || 201, status: pr.status ?? ApprovalStatus.Approved, ownerEmail: email, }); setIsProgrammeModalOpen(true); }; // Filtered lists const filteredOrgs = organisations.filter( (o) => o.name?.toLowerCase().includes(searchTerm.toLowerCase()) || o.registrationNo?.toLowerCase().includes(searchTerm.toLowerCase()) || o.email?.toLowerCase().includes(searchTerm.toLowerCase()) || String(o.id).includes(searchTerm) ); const filteredPortfolios = portfolios.filter((p) => { if (!searchTerm.trim()) return true; const q = searchTerm.toLowerCase(); const org = organisations.find((o) => String(o.id) === String(p.organisationId)); const orgName = org?.name?.toLowerCase() || ''; const name = (p.name || '').toLowerCase(); const desc = (p.description || '').toLowerCase(); const id = String(p.id); const owner = getUserEmail(p.ownedBy).toLowerCase(); return name.includes(q) || desc.includes(q) || id.includes(q) || orgName.includes(q) || owner.includes(q); }); const filteredProgrammes = programmes.filter((pr) => { if (!searchTerm.trim()) return true; const q = searchTerm.toLowerCase(); const port = portfolios.find((p) => String(p.id) === String(pr.portfolioId)); const portName = port?.name?.toLowerCase() || ''; const name = (pr.name || '').toLowerCase(); const id = String(pr.id); const owner = getUserEmail(pr.ownedBy).toLowerCase(); return name.includes(q) || id.includes(q) || portName.includes(q) || owner.includes(q); }); // Table Sorting Hooks const { sortedData: sortedOrgs, sortColumn: orgSortCol, sortDirection: orgSortDir, handleSort: handleOrgSort, } = useTableSort(filteredOrgs, null, null, { name: (o) => o.name, registrationNo: (o) => o.registrationNo, type: (o) => getOrganisationTypeLabel(o.type), email: (o) => o.email, status: (o) => getOrganisationStatusBadge(o).label, }); const { sortedData: sortedProgrammes, sortColumn: progSortCol, sortDirection: progSortDir, handleSort: handleProgSort, } = useTableSort(filteredProgrammes, null, null, { name: (p) => p.name, portfolioId: (p) => { const port = portfolios.find((po) => String(po.id) === String(p.portfolioId)); return port ? port.name : `Portfolio #${p.portfolioId}`; }, status: (p) => getApprovalStatusBadge(p.status).label, createdAt: (p) => p.createdAt, ownedBy: (p) => getUserEmail(p.ownedBy), }); // Export Columns Definitions const orgExportColumns: ExportColumn[] = [ { header: 'Organisation ID', key: 'id' }, { header: 'Legal Entity Name', key: 'name' }, { header: 'Registration / CIPC No', key: 'registrationNo', format: (val) => val || '—' }, { header: 'Entity Classification', key: 'type', format: (val) => getOrganisationTypeLabel(val) }, { header: 'Official Email', key: 'email', format: (val) => val || '—' }, { header: 'Status', key: 'status', format: (val) => getOrganisationStatusBadge({ status: val }).label }, { header: 'Registration Date', key: 'createdAt', format: (val) => formatDate(val) }, ]; const portfolioExportColumns: ExportColumn[] = [ { header: 'Portfolio ID', key: 'id' }, { header: 'Portfolio Name', key: 'name' }, { header: 'Organisation', key: 'organisationId', format: (val) => { const org = organisations.find((o) => String(o.id) === String(val)); return org ? org.name : `Org #${val}`; }, }, { header: 'Programmes Count', key: 'id', format: (id) => programmes.filter((pr) => String(pr.portfolioId) === String(id)).length, }, { header: 'Owner Email', key: 'ownedBy', format: (val) => getUserEmail(val) }, { header: 'Creation Date', key: 'createdAt', format: (val) => formatDate(val) }, { header: 'Description', key: 'description', format: (val) => val || '—' }, ]; const programmeExportColumns: ExportColumn[] = [ { header: 'Programme ID', key: 'id' }, { header: 'Programme Name', key: 'name' }, { header: 'Parent Portfolio', key: 'portfolioId', format: (val) => { const port = portfolios.find((p) => String(p.id) === String(val)); return port ? port.name : `Portfolio #${val}`; }, }, { header: 'Governance Status', key: 'status', format: (val) => getApprovalStatusBadge(val).label }, { header: 'Creation Date', key: 'createdAt', format: (val) => formatDate(val) }, { header: 'Owner Email', key: 'ownedBy', format: (val) => getUserEmail(val) }, ]; return (
{/* Header Banner */}

Governance & Structure

Directory

Maintain Master Organisation Registry, Funder Portfolios, Strategic Programmes, Funding Instruments, and Programme Rules.

{/* Tab Actions */}
{activeTab === 'organisations' && canAdd('governance') && ( )} {activeTab === 'portfolios' && canAdd('governance') && ( )} {activeTab === 'programmes' && canAdd('governance') && ( )} {(activeTab === 'instruments' || activeTab === 'instructions') && canAdd('governance') && ( )} {activeTab === 'rules' && canAdd('governance') && ( )}
{/* Tabs */}
{/* Search Bar & Actions for Organisations */} {activeTab === 'organisations' && (
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" />
)} {/* Tab 1: Organisations Table */} {activeTab === 'organisations' && (
Organisation Name Registration No Entity Type Contact Email Status {loadingOrgs ? ( ) : sortedOrgs.length === 0 ? ( ) : ( sortedOrgs.map((org) => { const statusBadge = getOrganisationStatusBadge(org); return ( ); }) )}
Actions
Loading registered organisations...
No organisations found matching query.
{org.name} ID: {org.id} {org.registrationNo || '—'} {getOrganisationTypeLabel(org.type)} {org.email} {statusBadge.label}
)} {/* Search Bar & Actions for Portfolios */} {activeTab === 'portfolios' && (
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" />
)} {/* Tab 2: Portfolios List */} {activeTab === 'portfolios' && (
{loadingPortfolios ? (

Loading portfolios...

) : filteredPortfolios.length === 0 ? (

No portfolios found matching query

Try refining your search keywords or create a new portfolio.

) : ( filteredPortfolios.map((p) => { const org = organisations.find((o) => String(o.id) === String(p.organisationId)); const progCount = programmes.filter((pr) => String(pr.portfolioId) === String(p.id)).length; const isHighlighted = String(highlightedPortfolioId) === String(p.id); return (
Portfolio #{p.id} {progCount} Programmes

{p.name}

{p.description}

{org ? org.name : `Org #${p.organisationId}`} {formatDate(p.createdAt)}
Owner: {getUserEmail(p.ownedBy)}
); }) )}
)} {/* Search Bar & Actions for Programmes */} {activeTab === 'programmes' && (
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" />
)} {/* Tab 3: Programmes Hierarchy */} {activeTab === 'programmes' && (
Programme Name Parent Portfolio Governance Status Creation Date Owner Email {loadingProgrammes ? ( ) : sortedProgrammes.length === 0 ? ( ) : ( sortedProgrammes.map((pr) => { const port = portfolios.find((p) => String(p.id) === String(pr.portfolioId)); const statusBadge = getApprovalStatusBadge(pr.status); return ( ); }) )}
Actions
Loading programmes...
No programmes found matching query.
{pr.name} ID: {pr.id} {port ? port.name : `Portfolio #${pr.portfolioId}`} {statusBadge.label} {formatDate(pr.createdAt)} {getUserEmail(pr.ownedBy)}
)} {/* Tab 4: Funding Instruments */} {(activeTab === 'instruments' || activeTab === 'instructions') && ( setPreselectedProgForInstruction(null)} isCreateModalOpenExternal={isInstructionCreateTriggered} onCloseCreateModalExternal={() => setIsInstructionCreateTriggered(false)} /> )} {/* Tab 5: Programme Rules */} {activeTab === 'rules' && ( setPreselectedProgForRule(null)} isCreateModalOpenExternal={isRuleCreateTriggered} onCloseCreateModalExternal={() => setIsRuleCreateTriggered(false)} /> )} {/* Modal: Register / Edit Organisation */} setIsOrgModalOpen(false)} title={editingOrg ? 'Edit Organisation Profile' : 'Register New Organisation'} subtitle="Mandatory master data profile for Recipient or Funder entities (POPIA compliant)" helpText="Maintains formal company or institutional credentials for funding allocation, audited legal standing, and regulatory reporting." >
orgMutation.mutate(data))} className="space-y-4">
{orgForm.formState.errors.name && (

{orgForm.formState.errors.name.message}

)}
{orgForm.formState.errors.registrationNo && (

{orgForm.formState.errors.registrationNo.message}

)}
{orgForm.formState.errors.organisationUser && (

{orgForm.formState.errors.organisationUser.message}

)}
{orgForm.formState.errors.idNumber && (

{orgForm.formState.errors.idNumber.message}

)}
{orgForm.formState.errors.email && (

{orgForm.formState.errors.email.message}

)}
{editingOrg && (
)}
{/* Modal: Create / Edit Portfolio */} { setIsPortfolioModalOpen(false); setEditingPortfolio(null); }} title={editingPortfolio ? 'Edit Funder Portfolio' : 'Create Funder Portfolio'} subtitle="Strategic allocation bucket grouping multiple programmatic initiatives" helpText="Portfolios consolidate thematic grant priorities under an authoritative parent entity (e.g. Science & Innovation Department) for high-level fiscal tracking." >
portfolioMutation.mutate(data))} className="space-y-4">
{portfolioForm.formState.errors.name && (

{portfolioForm.formState.errors.name.message}

)}
{usersDirectory.filter((u) => u.email).map((u) => ( ))} {portfolioForm.formState.errors.ownerEmail && (

{portfolioForm.formState.errors.ownerEmail.message}

)}