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
@@ -19,13 +19,16 @@ import {
} from 'lucide-react';
import {
getOrganisations,
getOrganisation,
createOrganisation,
updateOrganisation,
getAllPortfolios,
getPortfolios,
getPortfolio,
createPortfolio,
updatePortfolio,
getAllProgrammes,
getProgrammes,
getProgramme,
createProgramme,
updateProgramme,
@@ -33,6 +36,7 @@ import {
getPortfolioProgrammes,
} from '../services/api';
import { Modal } from '../components/ui/Modal';
import { HelpTooltip } from '../components/ui/HelpTooltip';
import { useToast } from '../components/ui/Toast';
import { useAuth } from '../auth/AuthProvider';
import {
@@ -111,6 +115,15 @@ export const GovernancePage: React.FC = () => {
const [selectedPortfolioForProgrammes, setSelectedPortfolioForProgrammes] = useState<Portfolio | null>(null);
const [isPortfolioProgrammesModalOpen, setIsPortfolioProgrammesModalOpen] = useState(false);
const [selectedOrgForDetail, setSelectedOrgForDetail] = useState<Organisation | null>(null);
const [isOrgDetailModalOpen, setIsOrgDetailModalOpen] = useState(false);
const [selectedPortfolioForDetail, setSelectedPortfolioForDetail] = useState<Portfolio | null>(null);
const [isPortfolioDetailModalOpen, setIsPortfolioDetailModalOpen] = useState(false);
const [selectedProgrammeForDetail, setSelectedProgrammeForDetail] = useState<Programme | null>(null);
const [isProgrammeDetailModalOpen, setIsProgrammeDetailModalOpen] = useState(false);
// Queries
const { data: organisations = [], isLoading: loadingOrgs } = useQuery({
queryKey: ['organisations'],
@@ -139,6 +152,36 @@ export const GovernancePage: React.FC = () => {
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 ? getPortfolios(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: programmeFreshDetail, isLoading: loadingProgrammeFreshDetail } = useQuery({
queryKey: ['programme', selectedProgrammeForDetail?.id],
queryFn: () => (selectedProgrammeForDetail ? getProgramme(selectedProgrammeForDetail.id) : null),
enabled: !!selectedProgrammeForDetail && isProgrammeDetailModalOpen,
});
// Forms
const orgForm = useForm<OrganisationFormValues>({
resolver: zodResolver(organisationSchema) as any,
@@ -336,6 +379,11 @@ export const GovernancePage: React.FC = () => {
<span className="text-[11px] font-semibold text-blue-400 bg-blue-500/10 px-2.5 py-0.5 rounded-full border border-blue-500/30">
Directory
</span>
<HelpTooltip
title="Statutory Governance Architecture"
content="Maintain authoritative organizational profiles, funding portfolios, and strategic programmes. Ensures compliance with national grant mandates and statutory reporting standards."
placement="right"
/>
</div>
<p className="text-xs text-slate-400 mt-1">
Maintain Master Organisation Registry, Funder Portfolios, and Strategic Programmes hierarchy.
@@ -385,7 +433,13 @@ export const GovernancePage: React.FC = () => {
}`}
>
<Building2 className="w-4 h-4" />
Organisations Directory ({organisations.length})
<span>Organisations Directory ({organisations.length})</span>
<HelpTooltip
title="Organisations Master"
content="Official register of participating recipient and funder entities, verified against legal and regulatory standards."
size="sm"
placement="top"
/>
</button>
<button
onClick={() => setActiveTab('portfolios')}
@@ -396,7 +450,13 @@ export const GovernancePage: React.FC = () => {
}`}
>
<FolderKanban className="w-4 h-4" />
Funder Portfolios ({portfolios.length})
<span>Funder Portfolios ({portfolios.length})</span>
<HelpTooltip
title="Funder Portfolios"
content="High-level funding groupings designated for specific strategic objectives, themes, or departmental allocations."
size="sm"
placement="top"
/>
</button>
<button
onClick={() => setActiveTab('programmes')}
@@ -407,7 +467,13 @@ export const GovernancePage: React.FC = () => {
}`}
>
<Layers className="w-4 h-4" />
Programmes Hierarchy ({programmes.length})
<span>Programmes Hierarchy ({programmes.length})</span>
<HelpTooltip
title="Strategic Programmes"
content="Operational funding vehicles linked to portfolios that govern grant instruments, projects, and target metrics."
size="sm"
placement="top"
/>
</button>
</div>
@@ -494,13 +560,27 @@ export const GovernancePage: React.FC = () => {
</span>
</td>
<td className="py-3.5 px-5 text-right">
<button
onClick={() => openEditOrg(org)}
className="inline-flex items-center gap-1 text-xs font-semibold text-slate-200 hover:text-white bg-slate-900/80 hover:bg-slate-800 border border-slate-700/80 px-3 py-1.5 rounded-lg transition shadow-xs cursor-pointer"
>
<Edit2 className="w-3 h-3" />
Edit
</button>
<div className="flex items-center justify-end gap-1.5">
<button
type="button"
onClick={() => {
setSelectedOrgForDetail(org);
setIsOrgDetailModalOpen(true);
}}
className="inline-flex items-center gap-1 text-xs font-semibold text-blue-400 hover:text-white bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 px-2.5 py-1.5 rounded-lg transition shadow-xs cursor-pointer"
title="Inspect Organisation (GET /v1/api/governance/organisations/{id})"
>
<Eye className="w-3 h-3" />
Inspect
</button>
<button
onClick={() => openEditOrg(org)}
className="inline-flex items-center gap-1 text-xs font-semibold text-slate-200 hover:text-white bg-slate-900/80 hover:bg-slate-800 border border-slate-700/80 px-3 py-1.5 rounded-lg transition shadow-xs cursor-pointer"
>
<Edit2 className="w-3 h-3" />
Edit
</button>
</div>
</td>
</tr>
);
@@ -544,6 +624,18 @@ export const GovernancePage: React.FC = () => {
Owner: <span className="text-slate-200 font-mono text-[11px]">{getUserEmail(p.ownedBy)}</span>
</span>
<div className="flex items-center gap-1.5">
<button
type="button"
onClick={() => {
setSelectedPortfolioForDetail(p);
setIsPortfolioDetailModalOpen(true);
}}
className="inline-flex items-center gap-1 text-xs font-semibold text-cyan-400 hover:text-white bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/30 px-2.5 py-1 rounded-lg transition shadow-xs cursor-pointer"
title="Inspect Portfolio (GET /v1/api/governance/portfolios/{id})"
>
<Eye className="w-3 h-3" />
Inspect
</button>
<button
type="button"
onClick={() => {
@@ -619,6 +711,18 @@ export const GovernancePage: React.FC = () => {
</td>
<td className="py-3.5 px-5 text-right">
<div className="flex items-center justify-end gap-1.5">
<button
type="button"
onClick={() => {
setSelectedProgrammeForDetail(pr);
setIsProgrammeDetailModalOpen(true);
}}
className="inline-flex items-center gap-1 text-xs font-semibold text-cyan-400 hover:text-white bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/30 px-2.5 py-1 rounded-lg transition shadow-xs cursor-pointer"
title="Inspect Programme (GET /v1/api/projects/programme/{id})"
>
<Eye className="w-3 h-3" />
Inspect
</button>
<button
type="button"
onClick={() => {
@@ -657,12 +761,21 @@ export const GovernancePage: React.FC = () => {
onClose={() => 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."
>
<form onSubmit={orgForm.handleSubmit((data) => orgMutation.mutate(data))} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Legal Organisation Name *
</label>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">
Legal Organisation Name *
</label>
<HelpTooltip
title="Registered Legal Entity"
content="Registered statutory name matching company incorporation or university charter."
size="sm"
placement="right"
/>
</div>
<input
type="text"
{...orgForm.register('name')}
@@ -676,9 +789,17 @@ export const GovernancePage: React.FC = () => {
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Registration / CIPC Number *
</label>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">
Registration / CIPC Number *
</label>
<HelpTooltip
title="Official Registration Number"
content="Companies and Intellectual Property Commission (CIPC) registration number or institutional tax ID."
size="sm"
placement="top"
/>
</div>
<input
type="text"
{...orgForm.register('registrationNo')}
@@ -691,9 +812,17 @@ export const GovernancePage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Entity Classification *
</label>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">
Entity Classification *
</label>
<HelpTooltip
title="Institutional Type"
content="Classification determining applicable compliance rules, indirect cost rates, and audit tiers."
size="sm"
placement="top"
/>
</div>
<select
{...orgForm.register('type')}
className="w-full px-3 py-2 text-xs border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 bg-[#090d16] text-white font-medium"
@@ -709,9 +838,17 @@ export const GovernancePage: React.FC = () => {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Official Contact Email *
</label>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">
Official Contact Email *
</label>
<HelpTooltip
title="Primary Notice Email"
content="Designated address for formal legal notices, grant awards, and compliance audit notifications."
size="sm"
placement="top"
/>
</div>
<input
type="email"
{...orgForm.register('email')}
@@ -725,9 +862,17 @@ export const GovernancePage: React.FC = () => {
{editingOrg && (
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Operating Status *
</label>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">
Operating Status *
</label>
<HelpTooltip
title="Lifecycle State"
content="Active organizations are eligible for new contracts and disbursement releases."
size="sm"
placement="top"
/>
</div>
<select
{...orgForm.register('status')}
className="w-full px-3 py-2 text-xs border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 bg-[#090d16] text-white font-medium"
@@ -770,10 +915,19 @@ export const GovernancePage: React.FC = () => {
}}
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."
>
<form onSubmit={portfolioForm.handleSubmit((data) => portfolioMutation.mutate(data))} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Portfolio Title *</label>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">Portfolio Title *</label>
<HelpTooltip
title="Portfolio Nomenclature"
content="Clear strategic theme or national fund title (e.g. Energy Transition Fund, Biotechnology Thrust)."
size="sm"
placement="right"
/>
</div>
<input
type="text"
{...portfolioForm.register('name')}
@@ -786,7 +940,15 @@ export const GovernancePage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Parent Organisation *</label>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">Parent Organisation *</label>
<HelpTooltip
title="Funding Custodian"
content="The governing department, ministry, or endowment holding statutory financial authority."
size="sm"
placement="right"
/>
</div>
<select
{...portfolioForm.register('organisationId')}
className="w-full px-3 py-2 text-xs border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 bg-[#090d16] text-white font-medium"
@@ -800,7 +962,15 @@ export const GovernancePage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Owner Email *</label>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">Owner Email *</label>
<HelpTooltip
title="Portfolio Director"
content="Designated chief director or portfolio lead responsible for executive decisions and allocations."
size="sm"
placement="right"
/>
</div>
<input
type="text"
{...portfolioForm.register('ownerId')}
@@ -810,7 +980,15 @@ export const GovernancePage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Strategic Objective *</label>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">Strategic Objective *</label>
<HelpTooltip
title="Statutory Mandate"
content="High-level policy goal, target impact, or socioeconomic transformation objectives."
size="sm"
placement="right"
/>
</div>
<textarea
rows={3}
{...portfolioForm.register('description')}
@@ -853,10 +1031,19 @@ export const GovernancePage: React.FC = () => {
}}
title={editingProgramme ? 'Edit Strategic Programme' : 'Create Strategic Programme'}
subtitle="Programmatic operational entity funding distinct projects and awards"
helpText="Programmes operationalize portfolios into concrete funding instruments. Projects and grant awards are linked directly to programmes."
>
<form onSubmit={programmeForm.handleSubmit((data) => programmeMutation.mutate(data))} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Programme Title *</label>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">Programme Title *</label>
<HelpTooltip
title="Programme Identifier"
content="Official operational title under which funding calls and awards are issued."
size="sm"
placement="right"
/>
</div>
<input
type="text"
{...programmeForm.register('name')}
@@ -869,7 +1056,15 @@ export const GovernancePage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Assign to Portfolio *</label>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">Assign to Portfolio *</label>
<HelpTooltip
title="Parent Portfolio"
content="The portfolio funding umbrella to which this programme reports."
size="sm"
placement="right"
/>
</div>
<select
{...programmeForm.register('portfolioId')}
className="w-full px-3 py-2 text-xs border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 bg-[#090d16] text-white font-medium"
@@ -884,7 +1079,15 @@ export const GovernancePage: React.FC = () => {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Governance Status *</label>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">Governance Status *</label>
<HelpTooltip
title="Approval Gate"
content="Only Approved programmes can issue active grants and enter contract agreements."
size="sm"
placement="top"
/>
</div>
<select
{...programmeForm.register('status')}
className="w-full px-3 py-2 text-xs border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 bg-[#090d16] text-white font-medium"
@@ -898,7 +1101,15 @@ export const GovernancePage: React.FC = () => {
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Owner Email *</label>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">Owner Email *</label>
<HelpTooltip
title="Programme Manager"
content="Designated programme officer responsible for monitoring KPIs and milestone approvals."
size="sm"
placement="top"
/>
</div>
<input
type="text"
{...programmeForm.register('ownerId')}
@@ -1048,6 +1259,196 @@ export const GovernancePage: React.FC = () => {
</div>
</div>
</Modal>
{/* Modal: View Organisation Details (GET /v1/api/governance/organisations/{id} & /portfolio) */}
<Modal
isOpen={isOrgDetailModalOpen}
onClose={() => {
setIsOrgDetailModalOpen(false);
setSelectedOrgForDetail(null);
}}
title={orgFreshDetail?.name || selectedOrgForDetail?.name || 'Organisation Master Record'}
subtitle={`Authoritative profile for Organisation #${selectedOrgForDetail?.id || ''}`}
>
<div className="space-y-4 text-xs">
{loadingOrgFreshDetail ? (
<div className="py-8 text-center text-slate-400">
<RefreshCw className="w-5 h-5 animate-spin mx-auto mb-2 text-blue-400" />
Fetching canonical record (GET /v1/api/governance/organisations/{selectedOrgForDetail?.id})...
</div>
) : (
<>
<div className="p-4 bg-[#0d121f] rounded-xl border border-slate-800 space-y-2">
<div className="grid grid-cols-2 gap-3">
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Legal Name:</span>
<span className="text-white font-medium">{orgFreshDetail?.name || selectedOrgForDetail?.name}</span>
</div>
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Reg Number:</span>
<span className="font-mono text-slate-300">{orgFreshDetail?.registrationNo || selectedOrgForDetail?.registrationNo || '—'}</span>
</div>
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Official Email:</span>
<span className="font-mono text-slate-300">{orgFreshDetail?.email || selectedOrgForDetail?.email}</span>
</div>
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Entity Type:</span>
<span className="text-slate-300">{getOrganisationTypeLabel(orgFreshDetail?.type ?? selectedOrgForDetail?.type ?? OrganisationType.Enterprise)}</span>
</div>
</div>
</div>
<div>
<h4 className="font-semibold text-slate-300 text-xs mb-2 flex items-center justify-between">
<span>Linked Portfolios (GET /v1/api/governance/organisations/{selectedOrgForDetail?.id}/portfolio)</span>
<span className="text-[10px] font-mono text-slate-400">{orgPortfolios.length} linked</span>
</h4>
{loadingOrgPortfolios ? (
<p className="text-xs text-slate-500">Loading portfolios...</p>
) : orgPortfolios.length === 0 ? (
<p className="text-xs text-slate-500">No portfolios directly linked to this organisation.</p>
) : (
<div className="space-y-2 max-h-[220px] overflow-y-auto">
{orgPortfolios.map((port) => (
<div key={port.id} className="p-3 bg-slate-900/60 rounded-xl border border-slate-800 flex items-center justify-between">
<div>
<span className="text-white font-medium block">{port.name}</span>
<span className="text-[10px] text-slate-500 font-mono">ID: {port.id}</span>
</div>
<span className="text-[10px] text-blue-400 bg-blue-500/10 px-2 py-0.5 rounded border border-blue-500/20 font-mono">
Portfolio
</span>
</div>
))}
</div>
)}
</div>
</>
)}
<div className="pt-2 border-t border-slate-800 flex justify-end">
<button
type="button"
onClick={() => {
setIsOrgDetailModalOpen(false);
setSelectedOrgForDetail(null);
}}
className="px-4 py-2 text-xs font-semibold text-slate-300 bg-slate-800 hover:bg-slate-700 rounded-xl cursor-pointer"
>
Close
</button>
</div>
</div>
</Modal>
{/* Modal: View Portfolio Details (GET /v1/api/governance/portfolios/{id}) */}
<Modal
isOpen={isPortfolioDetailModalOpen}
onClose={() => {
setIsPortfolioDetailModalOpen(false);
setSelectedPortfolioForDetail(null);
}}
title={portfolioFreshDetail?.name || selectedPortfolioForDetail?.name || 'Portfolio Details'}
subtitle={`Authoritative record for Portfolio #${selectedPortfolioForDetail?.id || ''}`}
>
<div className="space-y-4 text-xs">
{loadingPortfolioFreshDetail ? (
<div className="py-8 text-center text-slate-400">
<RefreshCw className="w-5 h-5 animate-spin mx-auto mb-2 text-blue-400" />
Fetching canonical record (GET /v1/api/governance/portfolios/{selectedPortfolioForDetail?.id})...
</div>
) : (
<div className="p-4 bg-[#0d121f] rounded-xl border border-slate-800 space-y-3">
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Title:</span>
<span className="text-white font-medium text-sm">{portfolioFreshDetail?.name || selectedPortfolioForDetail?.name}</span>
</div>
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Description:</span>
<p className="text-slate-300 mt-0.5 leading-relaxed">{portfolioFreshDetail?.description || selectedPortfolioForDetail?.description || 'None provided'}</p>
</div>
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-slate-800/80">
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Owner:</span>
<span className="font-mono text-slate-300">{getUserEmail(portfolioFreshDetail?.ownedBy || selectedPortfolioForDetail?.ownedBy)}</span>
</div>
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Created:</span>
<span className="font-mono text-slate-300">
{formatDate(portfolioFreshDetail?.createdAt || selectedPortfolioForDetail?.createdAt || '')}
</span>
</div>
</div>
</div>
)}
<div className="pt-2 border-t border-slate-800 flex justify-end">
<button
type="button"
onClick={() => {
setIsPortfolioDetailModalOpen(false);
setSelectedPortfolioForDetail(null);
}}
className="px-4 py-2 text-xs font-semibold text-slate-300 bg-slate-800 hover:bg-slate-700 rounded-xl cursor-pointer"
>
Close
</button>
</div>
</div>
</Modal>
{/* Modal: View Programme Details (GET /v1/api/projects/programme/{id}) */}
<Modal
isOpen={isProgrammeDetailModalOpen}
onClose={() => {
setIsProgrammeDetailModalOpen(false);
setSelectedProgrammeForDetail(null);
}}
title={programmeFreshDetail?.name || selectedProgrammeForDetail?.name || 'Programme Details'}
subtitle={`Authoritative record for Programme #${selectedProgrammeForDetail?.id || ''}`}
>
<div className="space-y-4 text-xs">
{loadingProgrammeFreshDetail ? (
<div className="py-8 text-center text-slate-400">
<RefreshCw className="w-5 h-5 animate-spin mx-auto mb-2 text-blue-400" />
Fetching canonical record (GET /v1/api/projects/programme/{selectedProgrammeForDetail?.id})...
</div>
) : (
<div className="p-4 bg-[#0d121f] rounded-xl border border-slate-800 space-y-3">
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Programme Name:</span>
<span className="text-white font-medium text-sm">{programmeFreshDetail?.name || selectedProgrammeForDetail?.name}</span>
</div>
<div className="grid grid-cols-2 gap-2 pt-2">
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Approval Status:</span>
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${getApprovalStatusBadge(programmeFreshDetail?.status ?? selectedProgrammeForDetail?.status ?? ApprovalStatus.Approved).className}`}>
{getApprovalStatusBadge(programmeFreshDetail?.status ?? selectedProgrammeForDetail?.status ?? ApprovalStatus.Approved).label}
</span>
</div>
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Owner:</span>
<span className="font-mono text-slate-300">{getUserEmail(programmeFreshDetail?.ownedBy || selectedProgrammeForDetail?.ownedBy)}</span>
</div>
</div>
</div>
)}
<div className="pt-2 border-t border-slate-800 flex justify-end">
<button
type="button"
onClick={() => {
setIsProgrammeDetailModalOpen(false);
setSelectedProgrammeForDetail(null);
}}
className="px-4 py-2 text-xs font-semibold text-slate-300 bg-slate-800 hover:bg-slate-700 rounded-xl cursor-pointer"
>
Close
</button>
</div>
</div>
</Modal>
</div>
);
};