Implemented data export
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
updateOrganisation,
|
||||
getAllPortfolios,
|
||||
getPortfolios,
|
||||
getOrganisationPortfolios,
|
||||
getPortfolio,
|
||||
createPortfolio,
|
||||
updatePortfolio,
|
||||
@@ -39,8 +40,8 @@ import {
|
||||
updateProgramme,
|
||||
getInstruments,
|
||||
getInstructions,
|
||||
createInstruction,
|
||||
updateInstruction,
|
||||
createInstrument,
|
||||
updateInstrument,
|
||||
getRules,
|
||||
createRule,
|
||||
updateRule,
|
||||
@@ -51,8 +52,10 @@ import {
|
||||
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 { InstructionsSection } from '../components/governance/InstructionsSection';
|
||||
import { FundingInstrumentsSection, InstructionsSection } from '../components/governance/InstructionsSection';
|
||||
import { RulesSection } from '../components/governance/RulesSection';
|
||||
import {
|
||||
Organisation,
|
||||
@@ -133,26 +136,27 @@ export const GovernancePage: React.FC = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const tabParam = searchParams.get('tab');
|
||||
|
||||
const getValidTab = (val: string | null): 'organisations' | 'portfolios' | 'programmes' | 'instructions' | 'rules' => {
|
||||
if (val === 'portfolios' || val === 'programmes' || val === 'organisations' || val === 'instructions' || val === 'rules') {
|
||||
return val;
|
||||
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' | 'instructions' | 'rules'>(() => getValidTab(tabParam));
|
||||
const [activeTab, setActiveTab] = useState<'organisations' | 'portfolios' | 'programmes' | 'instruments' | 'instructions' | 'rules'>(() => getValidTab(tabParam));
|
||||
|
||||
useEffect(() => {
|
||||
if (tabParam && (tabParam === 'organisations' || tabParam === 'portfolios' || tabParam === 'programmes' || tabParam === 'instructions' || tabParam === 'rules')) {
|
||||
setActiveTab(tabParam);
|
||||
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' | 'instructions' | 'rules') => {
|
||||
setActiveTab(tab);
|
||||
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', tab);
|
||||
next.set('tab', canonicalTab);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
@@ -219,17 +223,32 @@ export const GovernancePage: React.FC = () => {
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const { data: organisations = [], isLoading: loadingOrgs } = useQuery({
|
||||
const {
|
||||
data: organisations = [],
|
||||
isLoading: loadingOrgs,
|
||||
isFetching: isFetchingOrgs,
|
||||
refetch: refetchOrgs,
|
||||
} = useQuery({
|
||||
queryKey: ['organisations'],
|
||||
queryFn: () => getOrganisations(),
|
||||
});
|
||||
|
||||
const { data: portfolios = [], isLoading: loadingPortfolios } = useQuery({
|
||||
const {
|
||||
data: portfolios = [],
|
||||
isLoading: loadingPortfolios,
|
||||
isFetching: isFetchingPortfolios,
|
||||
refetch: refetchPortfolios,
|
||||
} = useQuery({
|
||||
queryKey: ['portfolios'],
|
||||
queryFn: () => getAllPortfolios(),
|
||||
});
|
||||
|
||||
const { data: programmes = [], isLoading: loadingProgrammes } = useQuery({
|
||||
const {
|
||||
data: programmes = [],
|
||||
isLoading: loadingProgrammes,
|
||||
isFetching: isFetchingProgrammes,
|
||||
refetch: refetchProgrammes,
|
||||
} = useQuery({
|
||||
queryKey: ['programmes'],
|
||||
queryFn: () => getAllProgrammes(),
|
||||
});
|
||||
@@ -278,7 +297,7 @@ export const GovernancePage: React.FC = () => {
|
||||
|
||||
const { data: orgPortfolios = [], isLoading: loadingOrgPortfolios } = useQuery({
|
||||
queryKey: ['organisation-portfolios', selectedOrgForDetail?.id],
|
||||
queryFn: () => (selectedOrgForDetail ? getPortfolios(selectedOrgForDetail.id) : Promise.resolve([])),
|
||||
queryFn: () => (selectedOrgForDetail ? getOrganisationPortfolios(selectedOrgForDetail.id) : Promise.resolve([])),
|
||||
enabled: !!selectedOrgForDetail && isOrgDetailModalOpen,
|
||||
});
|
||||
|
||||
@@ -575,9 +594,81 @@ export const GovernancePage: React.FC = () => {
|
||||
(o) =>
|
||||
o.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
o.registrationNo?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
o.email?.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);
|
||||
});
|
||||
|
||||
// Export Columns Definitions
|
||||
const orgExportColumns: ExportColumn<Organisation>[] = [
|
||||
{ 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<Portfolio>[] = [
|
||||
{ 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<Programme>[] = [
|
||||
{ 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 (
|
||||
<div className="space-y-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
@@ -597,7 +688,7 @@ export const GovernancePage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Maintain Master Organisation Registry, Funder Portfolios, Strategic Programmes, Funding Instructions, and Programme Rules.
|
||||
Maintain Master Organisation Registry, Funder Portfolios, Strategic Programmes, Funding Instruments, and Programme Rules.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -630,13 +721,13 @@ export const GovernancePage: React.FC = () => {
|
||||
Create Programme
|
||||
</button>
|
||||
)}
|
||||
{activeTab === 'instructions' && (
|
||||
{(activeTab === 'instruments' || activeTab === 'instructions') && (
|
||||
<button
|
||||
onClick={() => setIsInstructionCreateTriggered(true)}
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-md shadow-blue-500/20 transition cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Instruction
|
||||
Add Funding Instrument
|
||||
</button>
|
||||
)}
|
||||
{activeTab === 'rules' && (
|
||||
@@ -705,18 +796,18 @@ export const GovernancePage: React.FC = () => {
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTabChange('instructions')}
|
||||
onClick={() => handleTabChange('instruments')}
|
||||
className={`py-3.5 px-4 text-xs font-semibold border-b-2 transition-colors flex items-center gap-2 cursor-pointer whitespace-nowrap ${
|
||||
activeTab === 'instructions'
|
||||
activeTab === 'instruments' || activeTab === 'instructions'
|
||||
? 'border-blue-500 text-blue-400'
|
||||
: 'border-transparent text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
<span>Funding Instructions</span>
|
||||
<Coins className="w-4 h-4" />
|
||||
<span>Funding Instruments</span>
|
||||
<HelpTooltip
|
||||
title="Funding Instructions"
|
||||
content="Operational directives and funding instrument guidelines attached to specific strategic programmes."
|
||||
title="Funding Instruments"
|
||||
content="Binding funding instruments and financing directives attached to specific strategic funding programmes."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
@@ -740,31 +831,41 @@ export const GovernancePage: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search Bar & Actions */}
|
||||
{/* Search Bar & Actions for Organisations */}
|
||||
{activeTab === 'organisations' && (
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||
<div className="relative max-w-md flex-1">
|
||||
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-3" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by organisation name, reg no, or email..."
|
||||
placeholder="Search by organisation name, reg no, email, or ID..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['organisations'] });
|
||||
success('Refreshed', 'Organisation directory refreshed from database.');
|
||||
}}
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 text-xs font-semibold text-slate-300 bg-[#0d121f] hover:bg-slate-800 border border-slate-700/80 rounded-xl transition cursor-pointer"
|
||||
title="Force refresh organisation directory from database"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5 text-blue-400" />
|
||||
Refresh Directory
|
||||
</button>
|
||||
<div className="flex items-center gap-2 self-end sm:self-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
refetchOrgs();
|
||||
success('Refreshed', 'Organisation directory refreshed from API.');
|
||||
}}
|
||||
disabled={isFetchingOrgs}
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 text-xs font-semibold text-slate-300 bg-[#0d121f] hover:bg-slate-800 border border-slate-700/80 rounded-xl transition cursor-pointer shadow-xs disabled:opacity-50"
|
||||
title="Force refresh organisation directory from API"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 text-blue-400 ${isFetchingOrgs ? 'animate-spin' : ''}`} />
|
||||
<span>Refresh Directory</span>
|
||||
</button>
|
||||
<ExportButton
|
||||
title="Organisation Master Registry"
|
||||
subtitle="Official directory of verified recipient and funder entities"
|
||||
filename={`pfm-organisations-${new Date().toISOString().split('T')[0]}`}
|
||||
columns={orgExportColumns}
|
||||
data={filteredOrgs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -855,13 +956,57 @@ export const GovernancePage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search Bar & Actions for Portfolios */}
|
||||
{activeTab === 'portfolios' && (
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||
<div className="relative max-w-md flex-1">
|
||||
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-3" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search portfolios by name, ID, organisation, or owner..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 self-end sm:self-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
refetchPortfolios();
|
||||
success('Refreshed', 'Funder portfolios refreshed from API.');
|
||||
}}
|
||||
disabled={isFetchingPortfolios}
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 text-xs font-semibold text-slate-300 bg-[#0d121f] hover:bg-slate-800 border border-slate-700/80 rounded-xl transition cursor-pointer shadow-xs disabled:opacity-50"
|
||||
title="Force refresh portfolios from API"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 text-blue-400 ${isFetchingPortfolios ? 'animate-spin' : ''}`} />
|
||||
<span>Refresh Portfolios</span>
|
||||
</button>
|
||||
<ExportButton
|
||||
title="Funder Portfolios Registry"
|
||||
subtitle="High-level funding groupings and thematic capital allocations"
|
||||
filename={`pfm-portfolios-${new Date().toISOString().split('T')[0]}`}
|
||||
columns={portfolioExportColumns}
|
||||
data={filteredPortfolios}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 2: Portfolios List */}
|
||||
{activeTab === 'portfolios' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{loadingPortfolios ? (
|
||||
<p className="text-xs text-slate-500 col-span-3">Loading portfolios...</p>
|
||||
<p className="text-xs text-slate-500 col-span-3 text-center py-8">Loading portfolios...</p>
|
||||
) : filteredPortfolios.length === 0 ? (
|
||||
<div className="col-span-3 py-12 text-center bg-[#0f1523]/80 rounded-2xl border border-slate-800/80 p-8">
|
||||
<FolderKanban className="w-8 h-8 text-slate-600 mx-auto mb-2" />
|
||||
<p className="text-sm font-semibold text-slate-300">No portfolios found matching query</p>
|
||||
<p className="text-xs text-slate-500 mt-1">Try refining your search keywords or create a new portfolio.</p>
|
||||
</div>
|
||||
) : (
|
||||
portfolios.map((p) => {
|
||||
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);
|
||||
@@ -942,6 +1087,44 @@ export const GovernancePage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search Bar & Actions for Programmes */}
|
||||
{activeTab === 'programmes' && (
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||
<div className="relative max-w-md flex-1">
|
||||
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-3" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search programmes by name, ID, parent portfolio, or owner..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 self-end sm:self-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
refetchProgrammes();
|
||||
success('Refreshed', 'Programmes hierarchy refreshed from API.');
|
||||
}}
|
||||
disabled={isFetchingProgrammes}
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 text-xs font-semibold text-slate-300 bg-[#0d121f] hover:bg-slate-800 border border-slate-700/80 rounded-xl transition cursor-pointer shadow-xs disabled:opacity-50"
|
||||
title="Force refresh programmes from API"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 text-blue-400 ${isFetchingProgrammes ? 'animate-spin' : ''}`} />
|
||||
<span>Refresh Programmes</span>
|
||||
</button>
|
||||
<ExportButton
|
||||
title="Strategic Programmes Hierarchy"
|
||||
subtitle="Operational funding vehicles linked to funder portfolios"
|
||||
filename={`pfm-programmes-${new Date().toISOString().split('T')[0]}`}
|
||||
columns={programmeExportColumns}
|
||||
data={filteredProgrammes}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 3: Programmes Hierarchy */}
|
||||
{activeTab === 'programmes' && (
|
||||
<div className="bg-[#0f1523]/80 rounded-2xl border border-slate-800/80 shadow-lg backdrop-blur-sm overflow-hidden">
|
||||
@@ -964,8 +1147,14 @@ export const GovernancePage: React.FC = () => {
|
||||
Loading programmes...
|
||||
</td>
|
||||
</tr>
|
||||
) : filteredProgrammes.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="py-8 text-center text-slate-500">
|
||||
No programmes found matching query.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
programmes.map((pr) => {
|
||||
filteredProgrammes.map((pr) => {
|
||||
const port = portfolios.find((p) => String(p.id) === String(pr.portfolioId));
|
||||
const statusBadge = getApprovalStatusBadge(pr.status);
|
||||
return (
|
||||
@@ -1007,10 +1196,10 @@ export const GovernancePage: React.FC = () => {
|
||||
setIsInstrumentsModalOpen(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 rounded-lg transition shadow-xs cursor-pointer"
|
||||
title="View Funding Instructions"
|
||||
title="View Funding Instruments"
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
Instructions
|
||||
<Coins className="w-3 h-3" />
|
||||
Instruments
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1044,9 +1233,9 @@ export const GovernancePage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 4: Funding Instructions */}
|
||||
{activeTab === 'instructions' && (
|
||||
<InstructionsSection
|
||||
{/* Tab 4: Funding Instruments */}
|
||||
{(activeTab === 'instruments' || activeTab === 'instructions') && (
|
||||
<FundingInstrumentsSection
|
||||
programmes={programmes}
|
||||
currentUserId={currentUserId}
|
||||
preselectedProgrammeId={preselectedProgForInstruction}
|
||||
@@ -1476,26 +1665,26 @@ export const GovernancePage: React.FC = () => {
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{/* Modal: View Funding Instructions */}
|
||||
{/* Modal: View Funding Instruments */}
|
||||
<Modal
|
||||
isOpen={isInstrumentsModalOpen}
|
||||
onClose={() => {
|
||||
setIsInstrumentsModalOpen(false);
|
||||
setSelectedProgrammeForInstruments(null);
|
||||
}}
|
||||
title={`Funding Instructions: ${selectedProgrammeForInstruments?.name || ''}`}
|
||||
subtitle={`Authoritative funding instructions linked to Programme #${selectedProgrammeForInstruments?.id || ''}`}
|
||||
title={`Funding Instruments: ${selectedProgrammeForInstruments?.name || ''}`}
|
||||
subtitle={`Authoritative funding instruments linked to Programme #${selectedProgrammeForInstruments?.id || ''}`}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{loadingInstruments ? (
|
||||
<div className="py-8 text-center text-xs text-slate-400 flex items-center justify-center gap-2">
|
||||
<RefreshCw className="w-4 h-4 animate-spin text-blue-400" />
|
||||
<span>Loading funding instructions...</span>
|
||||
<span>Loading funding instruments...</span>
|
||||
</div>
|
||||
) : programmeInstruments.length === 0 ? (
|
||||
<div className="py-8 text-center text-xs text-slate-400 space-y-3">
|
||||
<div className="text-slate-500">
|
||||
No funding instructions registered under this programme yet.
|
||||
No funding instruments registered under this programme yet.
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1505,12 +1694,12 @@ export const GovernancePage: React.FC = () => {
|
||||
setSelectedProgrammeForInstruments(null);
|
||||
setPreselectedProgForInstruction(pId || null);
|
||||
setIsInstructionCreateTriggered(true);
|
||||
handleTabChange('instructions');
|
||||
handleTabChange('instruments');
|
||||
}}
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-2 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-xl transition cursor-pointer shadow-md shadow-blue-500/20"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
Add Instruction to this Programme
|
||||
Add Instrument to this Programme
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -1540,10 +1729,10 @@ export const GovernancePage: React.FC = () => {
|
||||
setIsInstrumentsModalOpen(false);
|
||||
setSelectedProgrammeForInstruments(null);
|
||||
setPreselectedProgForInstruction(pId || null);
|
||||
handleTabChange('instructions');
|
||||
handleTabChange('instruments');
|
||||
}}
|
||||
className="inline-flex items-center gap-1 text-[11px] font-semibold text-slate-300 hover:text-white bg-slate-800 hover:bg-slate-700 border border-slate-700 px-2 py-0.5 rounded cursor-pointer"
|
||||
title="Manage in Instructions Tab"
|
||||
title="Manage in Funding Instruments Tab"
|
||||
>
|
||||
<Edit2 className="w-3 h-3" />
|
||||
Manage
|
||||
@@ -1571,12 +1760,12 @@ export const GovernancePage: React.FC = () => {
|
||||
setSelectedProgrammeForInstruments(null);
|
||||
setPreselectedProgForInstruction(pId || null);
|
||||
setIsInstructionCreateTriggered(true);
|
||||
handleTabChange('instructions');
|
||||
handleTabChange('instruments');
|
||||
}}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-blue-400 hover:text-white bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 rounded-xl transition cursor-pointer"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
Add Instruction
|
||||
Add Funding Instrument
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1765,10 +1954,10 @@ export const GovernancePage: React.FC = () => {
|
||||
setIsInstrumentsModalOpen(true);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 text-[11px] font-semibold text-blue-400 hover:text-white bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 px-2 py-1 rounded-lg cursor-pointer"
|
||||
title="View Funding Instructions"
|
||||
title="View Funding Instruments"
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
Instructions
|
||||
<Coins className="w-3 h-3" />
|
||||
Instruments
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -2067,12 +2256,12 @@ export const GovernancePage: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Total Instructions */}
|
||||
{/* Total Funding Instruments */}
|
||||
<div className="p-3 bg-gradient-to-br from-blue-500/10 to-blue-500/5 rounded-xl border border-blue-500/20 flex flex-col justify-between">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[11px] font-semibold text-blue-300 flex items-center gap-1.5">
|
||||
<FileText className="w-3.5 h-3.5 text-blue-400" />
|
||||
Total Instructions
|
||||
<Coins className="w-3.5 h-3.5 text-blue-400" />
|
||||
Total Funding Instruments
|
||||
</span>
|
||||
<span className="text-lg font-bold font-mono text-blue-400">
|
||||
{loadingDetailInstructions ? (
|
||||
@@ -2089,11 +2278,11 @@ export const GovernancePage: React.FC = () => {
|
||||
setIsProgrammeDetailModalOpen(false);
|
||||
setSelectedProgrammeForDetail(null);
|
||||
setPreselectedProgForInstruction(pId || null);
|
||||
handleTabChange('instructions');
|
||||
handleTabChange('instruments');
|
||||
}}
|
||||
className="text-[11px] font-semibold text-blue-400 hover:text-blue-300 inline-flex items-center gap-1 mt-1 transition cursor-pointer self-start"
|
||||
>
|
||||
View in Instructions Tab →
|
||||
View in Funding Instruments Tab →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user