Refactored UI endpoint usage
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user