Fixed API calls on governance page

Enforced multitenancy
This commit is contained in:
2026-09-26 10:28:43 +02:00
parent b2688867f3
commit f3a8f78144
16 changed files with 3552 additions and 503 deletions
@@ -18,6 +18,9 @@ import {
Eye,
RefreshCw,
ArrowRight,
FileText,
Scale,
Calendar,
} from 'lucide-react';
import {
getOrganisations,
@@ -35,6 +38,12 @@ import {
createProgramme,
updateProgramme,
getInstruments,
getInstructions,
createInstruction,
updateInstruction,
getRules,
createRule,
updateRule,
getPortfolioProgrammes,
getUsers,
getUser,
@@ -43,11 +52,15 @@ import { Modal } from '../components/ui/Modal';
import { HelpTooltip } from '../components/ui/HelpTooltip';
import { useToast } from '../components/ui/Toast';
import { useAuth } from '../auth/AuthProvider';
import { InstructionsSection } from '../components/governance/InstructionsSection';
import { RulesSection } from '../components/governance/RulesSection';
import {
Organisation,
Portfolio,
Programme,
Instrument,
Instruction,
Rule,
OrganisationType,
ActivityStatus,
ApprovalStatus,
@@ -120,22 +133,22 @@ export const GovernancePage: React.FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
const tabParam = searchParams.get('tab');
const getValidTab = (val: string | null): 'organisations' | 'portfolios' | 'programmes' => {
if (val === 'portfolios' || val === 'programmes' || val === 'organisations') {
const getValidTab = (val: string | null): 'organisations' | 'portfolios' | 'programmes' | 'instructions' | 'rules' => {
if (val === 'portfolios' || val === 'programmes' || val === 'organisations' || val === 'instructions' || val === 'rules') {
return val;
}
return 'organisations';
};
const [activeTab, setActiveTab] = useState<'organisations' | 'portfolios' | 'programmes'>(() => getValidTab(tabParam));
const [activeTab, setActiveTab] = useState<'organisations' | 'portfolios' | 'programmes' | 'instructions' | 'rules'>(() => getValidTab(tabParam));
useEffect(() => {
if (tabParam && (tabParam === 'organisations' || tabParam === 'portfolios' || tabParam === 'programmes')) {
if (tabParam && (tabParam === 'organisations' || tabParam === 'portfolios' || tabParam === 'programmes' || tabParam === 'instructions' || tabParam === 'rules')) {
setActiveTab(tabParam);
}
}, [tabParam]);
const handleTabChange = (tab: 'organisations' | 'portfolios' | 'programmes') => {
const handleTabChange = (tab: 'organisations' | 'portfolios' | 'programmes' | 'instructions' | 'rules') => {
setActiveTab(tab);
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
@@ -144,6 +157,11 @@ export const GovernancePage: React.FC = () => {
});
};
const [preselectedProgForInstruction, setPreselectedProgForInstruction] = useState<string | number | null>(null);
const [preselectedProgForRule, setPreselectedProgForRule] = useState<string | number | null>(null);
const [isInstructionCreateTriggered, setIsInstructionCreateTriggered] = useState(false);
const [isRuleCreateTriggered, setIsRuleCreateTriggered] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [highlightedPortfolioId, setHighlightedPortfolioId] = useState<number | string | null>(null);
@@ -179,6 +197,9 @@ export const GovernancePage: React.FC = () => {
const [selectedProgrammeForInstruments, setSelectedProgrammeForInstruments] = useState<Programme | null>(null);
const [isInstrumentsModalOpen, setIsInstrumentsModalOpen] = useState(false);
const [selectedProgrammeForRules, setSelectedProgrammeForRules] = useState<Programme | null>(null);
const [isRulesModalOpen, setIsRulesModalOpen] = useState(false);
const [selectedPortfolioForProgrammes, setSelectedPortfolioForProgrammes] = useState<Portfolio | null>(null);
const [isPortfolioProgrammesModalOpen, setIsPortfolioProgrammesModalOpen] = useState(false);
@@ -215,10 +236,28 @@ export const GovernancePage: React.FC = () => {
const { data: programmeInstruments = [], isLoading: loadingInstruments } = useQuery({
queryKey: ['instruments', selectedProgrammeForInstruments?.id],
queryFn: () => (selectedProgrammeForInstruments ? getInstruments(selectedProgrammeForInstruments.id) : Promise.resolve([])),
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([])),
@@ -558,7 +597,7 @@ export const GovernancePage: React.FC = () => {
/>
</div>
<p className="text-xs text-slate-400 mt-1">
Maintain Master Organisation Registry, Funder Portfolios, and Strategic Programmes hierarchy.
Maintain Master Organisation Registry, Funder Portfolios, Strategic Programmes, Funding Instructions, and Programme Rules.
</p>
</div>
@@ -591,14 +630,32 @@ export const GovernancePage: React.FC = () => {
Create Programme
</button>
)}
{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
</button>
)}
{activeTab === 'rules' && (
<button
onClick={() => setIsRuleCreateTriggered(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 Rule
</button>
)}
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-slate-800/80 bg-[#0d121f]/80 px-4 rounded-2xl backdrop-blur-sm">
<div className="flex border-b border-slate-800/80 bg-[#0d121f]/80 px-4 rounded-2xl backdrop-blur-sm overflow-x-auto">
<button
onClick={() => handleTabChange('organisations')}
className={`py-3.5 px-4 text-xs font-semibold border-b-2 transition-colors flex items-center gap-2 cursor-pointer ${
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 === 'organisations'
? 'border-blue-500 text-blue-400'
: 'border-transparent text-slate-400 hover:text-slate-200'
@@ -615,7 +672,7 @@ export const GovernancePage: React.FC = () => {
</button>
<button
onClick={() => handleTabChange('portfolios')}
className={`py-3.5 px-4 text-xs font-semibold border-b-2 transition-colors flex items-center gap-2 cursor-pointer ${
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 === 'portfolios'
? 'border-blue-500 text-blue-400'
: 'border-transparent text-slate-400 hover:text-slate-200'
@@ -632,7 +689,7 @@ export const GovernancePage: React.FC = () => {
</button>
<button
onClick={() => handleTabChange('programmes')}
className={`py-3.5 px-4 text-xs font-semibold border-b-2 transition-colors flex items-center gap-2 cursor-pointer ${
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 === 'programmes'
? 'border-blue-500 text-blue-400'
: 'border-transparent text-slate-400 hover:text-slate-200'
@@ -647,6 +704,40 @@ export const GovernancePage: React.FC = () => {
placement="top"
/>
</button>
<button
onClick={() => handleTabChange('instructions')}
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'
? '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>
<HelpTooltip
title="Funding Instructions"
content="Operational directives and funding instrument guidelines attached to specific strategic programmes."
size="sm"
placement="top"
/>
</button>
<button
onClick={() => handleTabChange('rules')}
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 === 'rules'
? 'border-blue-500 text-blue-400'
: 'border-transparent text-slate-400 hover:text-slate-200'
}`}
>
<Scale className="w-4 h-4" />
<span>Programme Rules</span>
<HelpTooltip
title="Programme Rules"
content="Statutory, regulatory, and eligibility rules associated with specific funding programmes."
size="sm"
placement="top"
/>
</button>
</div>
{/* Search Bar & Actions */}
@@ -916,10 +1007,22 @@ 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 Instruments"
title="View Funding Instructions"
>
<Coins className="w-3 h-3" />
Instruments
<FileText className="w-3 h-3" />
Instructions
</button>
<button
type="button"
onClick={() => {
setSelectedProgrammeForRules(pr);
setIsRulesModalOpen(true);
}}
className="inline-flex items-center gap-1 text-xs font-semibold text-amber-400 hover:text-white bg-amber-500/10 hover:bg-amber-500/20 border border-amber-500/30 px-2.5 py-1 rounded-lg transition shadow-xs cursor-pointer"
title="View Programme Rules"
>
<Scale className="w-3 h-3" />
Rules
</button>
<button
type="button"
@@ -941,6 +1044,30 @@ export const GovernancePage: React.FC = () => {
</div>
)}
{/* Tab 4: Funding Instructions */}
{activeTab === 'instructions' && (
<InstructionsSection
programmes={programmes}
currentUserId={currentUserId}
preselectedProgrammeId={preselectedProgForInstruction}
onClearPreselectedProgramme={() => setPreselectedProgForInstruction(null)}
isCreateModalOpenExternal={isInstructionCreateTriggered}
onCloseCreateModalExternal={() => setIsInstructionCreateTriggered(false)}
/>
)}
{/* Tab 5: Programme Rules */}
{activeTab === 'rules' && (
<RulesSection
programmes={programmes}
currentUserId={currentUserId}
preselectedProgrammeId={preselectedProgForRule}
onClearPreselectedProgramme={() => setPreselectedProgForRule(null)}
isCreateModalOpenExternal={isRuleCreateTriggered}
onCloseCreateModalExternal={() => setIsRuleCreateTriggered(false)}
/>
)}
{/* Modal: Register / Edit Organisation */}
<Modal
isOpen={isOrgModalOpen}
@@ -1349,22 +1476,42 @@ export const GovernancePage: React.FC = () => {
</form>
</Modal>
{/* Modal: View Funding Instruments */}
{/* Modal: View Funding Instructions */}
<Modal
isOpen={isInstrumentsModalOpen}
onClose={() => {
setIsInstrumentsModalOpen(false);
setSelectedProgrammeForInstruments(null);
}}
title={`Funding Instruments: ${selectedProgrammeForInstruments?.name || ''}`}
subtitle={`Authoritative funding instruments linked to Programme #${selectedProgrammeForInstruments?.id || ''}`}
title={`Funding Instructions: ${selectedProgrammeForInstruments?.name || ''}`}
subtitle={`Authoritative funding instructions linked to Programme #${selectedProgrammeForInstruments?.id || ''}`}
>
<div className="space-y-4">
{loadingInstruments ? (
<div className="py-8 text-center text-xs text-slate-400">Loading funding instruments...</div>
<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>
</div>
) : programmeInstruments.length === 0 ? (
<div className="py-8 text-center text-xs text-slate-500">
No funding instruments registered under this programme.
<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.
</div>
<button
type="button"
onClick={() => {
const pId = selectedProgrammeForInstruments?.id;
setIsInstrumentsModalOpen(false);
setSelectedProgrammeForInstruments(null);
setPreselectedProgForInstruction(pId || null);
setIsInstructionCreateTriggered(true);
handleTabChange('instructions');
}}
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
</button>
</div>
) : (
<div className="space-y-2.5 max-h-[400px] overflow-y-auto">
@@ -1382,9 +1529,26 @@ export const GovernancePage: React.FC = () => {
</span>
<span className="font-semibold text-xs text-white">{inst.name}</span>
</div>
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${statusBadge.className}`}>
{statusBadge.label}
</span>
<div className="flex items-center gap-2">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${statusBadge.className}`}>
{statusBadge.label}
</span>
<button
type="button"
onClick={() => {
const pId = selectedProgrammeForInstruments?.id;
setIsInstrumentsModalOpen(false);
setSelectedProgrammeForInstruments(null);
setPreselectedProgForInstruction(pId || null);
handleTabChange('instructions');
}}
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"
>
<Edit2 className="w-3 h-3" />
Manage
</button>
</div>
</div>
{inst.description && (
<p className="text-xs text-slate-400 leading-relaxed">{inst.description}</p>
@@ -1398,7 +1562,22 @@ export const GovernancePage: React.FC = () => {
})}
</div>
)}
<div className="pt-2 border-t border-slate-800 flex justify-end">
<div className="pt-2 border-t border-slate-800 flex justify-between items-center">
<button
type="button"
onClick={() => {
const pId = selectedProgrammeForInstruments?.id;
setIsInstrumentsModalOpen(false);
setSelectedProgrammeForInstruments(null);
setPreselectedProgForInstruction(pId || null);
setIsInstructionCreateTriggered(true);
handleTabChange('instructions');
}}
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
</button>
<button
type="button"
onClick={() => {
@@ -1413,6 +1592,122 @@ export const GovernancePage: React.FC = () => {
</div>
</Modal>
{/* Modal: View Programme Rules */}
<Modal
isOpen={isRulesModalOpen}
onClose={() => {
setIsRulesModalOpen(false);
setSelectedProgrammeForRules(null);
}}
title={`Programme Rules: ${selectedProgrammeForRules?.name || ''}`}
subtitle={`Authoritative governance and statutory rules linked to Programme #${selectedProgrammeForRules?.id || ''}`}
>
<div className="space-y-4">
{loadingProgrammeRules ? (
<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-amber-400" />
<span>Loading programme rules...</span>
</div>
) : programmeRulesList.length === 0 ? (
<div className="py-8 text-center text-xs text-slate-400 space-y-3">
<div className="text-slate-500">
No programme rules registered under this programme yet.
</div>
<button
type="button"
onClick={() => {
const pId = selectedProgrammeForRules?.id;
setIsRulesModalOpen(false);
setSelectedProgrammeForRules(null);
setPreselectedProgForRule(pId || null);
setIsRuleCreateTriggered(true);
handleTabChange('rules');
}}
className="inline-flex items-center gap-1.5 px-3.5 py-2 text-xs font-semibold text-white bg-amber-600 hover:bg-amber-500 rounded-xl transition cursor-pointer shadow-md shadow-amber-500/20"
>
<Plus className="w-3.5 h-3.5" />
Add Rule to this Programme
</button>
</div>
) : (
<div className="space-y-2.5 max-h-[400px] overflow-y-auto">
{programmeRulesList.map((r: Rule) => {
const statusBadge = getActivityStatusBadge(r.status);
return (
<div
key={r.id}
className="p-3.5 rounded-xl border border-slate-800 bg-[#0d121f] flex flex-col gap-1.5"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="font-mono text-xs font-bold text-amber-400 bg-amber-500/10 px-2 py-0.5 rounded border border-amber-500/20">
v{r.version || 1}
</span>
<span className="font-semibold text-xs text-white">{r.name}</span>
</div>
<div className="flex items-center gap-2">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${statusBadge.className}`}>
{statusBadge.label}
</span>
<button
type="button"
onClick={() => {
const pId = selectedProgrammeForRules?.id;
setIsRulesModalOpen(false);
setSelectedProgrammeForRules(null);
setPreselectedProgForRule(pId || null);
handleTabChange('rules');
}}
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 Rules Tab"
>
<Edit2 className="w-3 h-3" />
Manage
</button>
</div>
</div>
{r.description && (
<p className="text-xs text-slate-400 leading-relaxed">{r.description}</p>
)}
<div className="flex items-center justify-between pt-2 border-t border-slate-800/80 text-[10px] text-slate-500 font-mono">
<span>Created by: {getUserEmail(r.createdBy)}</span>
<span>{formatDate(r.createdAt)}</span>
</div>
</div>
);
})}
</div>
)}
<div className="pt-2 border-t border-slate-800 flex justify-between items-center">
<button
type="button"
onClick={() => {
const pId = selectedProgrammeForRules?.id;
setIsRulesModalOpen(false);
setSelectedProgrammeForRules(null);
setPreselectedProgForRule(pId || null);
setIsRuleCreateTriggered(true);
handleTabChange('rules');
}}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-amber-400 hover:text-white bg-amber-500/10 hover:bg-amber-500/20 border border-amber-500/30 rounded-xl transition cursor-pointer"
>
<Plus className="w-3.5 h-3.5" />
Add Rule
</button>
<button
type="button"
onClick={() => {
setIsRulesModalOpen(false);
setSelectedProgrammeForRules(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 Programmes */}
<Modal
isOpen={isPortfolioProgrammesModalOpen}
@@ -1437,7 +1732,7 @@ export const GovernancePage: React.FC = () => {
return (
<div
key={pr.id}
className="p-3.5 rounded-xl border border-slate-800 bg-[#0d121f] flex items-center justify-between"
className="p-3.5 rounded-xl border border-slate-800 bg-[#0d121f] flex flex-col sm:flex-row sm:items-center justify-between gap-3"
>
<div>
<span className="font-semibold text-xs text-white block">{pr.name}</span>
@@ -1445,9 +1740,50 @@ export const GovernancePage: React.FC = () => {
ID: {pr.id} &bull; Owner: {getUserEmail(pr.ownedBy)} &bull; {formatDate(pr.createdAt)}
</span>
</div>
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${statusBadge.className}`}>
{statusBadge.label}
</span>
<div className="flex items-center gap-1.5 flex-wrap">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${statusBadge.className}`}>
{statusBadge.label}
</span>
<button
type="button"
onClick={() => {
setIsPortfolioProgrammesModalOpen(false);
setSelectedProgrammeForDetail(pr);
setIsProgrammeDetailModalOpen(true);
}}
className="inline-flex items-center gap-1 text-[11px] font-semibold text-cyan-400 hover:text-white bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/30 px-2 py-1 rounded-lg cursor-pointer"
title="Inspect Programme"
>
<Eye className="w-3 h-3" />
Inspect
</button>
<button
type="button"
onClick={() => {
setIsPortfolioProgrammesModalOpen(false);
setSelectedProgrammeForInstruments(pr);
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"
>
<FileText className="w-3 h-3" />
Instructions
</button>
<button
type="button"
onClick={() => {
setIsPortfolioProgrammesModalOpen(false);
setSelectedProgrammeForRules(pr);
setIsRulesModalOpen(true);
}}
className="inline-flex items-center gap-1 text-[11px] font-semibold text-amber-400 hover:text-white bg-amber-500/10 hover:bg-amber-500/20 border border-amber-500/30 px-2 py-1 rounded-lg cursor-pointer"
title="View Programme Rules"
>
<Scale className="w-3 h-3" />
Rules
</button>
</div>
</div>
);
})}
@@ -1664,22 +2000,101 @@ export const GovernancePage: React.FC = () => {
</div>
) : (
<div className="p-4 bg-[#0d121f] rounded-xl border border-slate-800/80 space-y-3.5">
<div className="pb-3 border-b border-slate-800/80">
<span className="text-slate-400 font-semibold block text-[11px] uppercase tracking-wider mb-1">Programme Name</span>
<span className="text-white font-semibold text-sm block">{programmeFreshDetail?.name || selectedProgrammeForDetail?.name}</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 items-center">
<div className="flex items-center gap-2">
<span className="text-slate-400 font-semibold text-[11px] uppercase tracking-wider shrink-0">Approval Status:</span>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold border whitespace-nowrap ${getApprovalStatusBadge(programmeFreshDetail?.status ?? selectedProgrammeForDetail?.status ?? ApprovalStatus.Approved).className}`}>
{getApprovalStatusBadge(programmeFreshDetail?.status ?? selectedProgrammeForDetail?.status ?? ApprovalStatus.Approved).label}
</span>
<div className="pb-3 border-b border-slate-800/80 flex items-start justify-between gap-4">
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase tracking-wider mb-1">Programme Title</span>
<span className="text-white font-semibold text-sm block">{programmeFreshDetail?.name || selectedProgrammeForDetail?.name}</span>
</div>
<div className="flex items-center gap-2 min-w-0">
<span className="text-slate-400 font-semibold text-[11px] uppercase tracking-wider shrink-0">Owner:</span>
<span className="font-mono text-slate-300 text-xs truncate" title={getUserEmail(programmeFreshDetail?.ownedBy || selectedProgrammeForDetail?.ownedBy)}>
{getUserEmail(programmeFreshDetail?.ownedBy || selectedProgrammeForDetail?.ownedBy)}
</span>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold border whitespace-nowrap ${getApprovalStatusBadge(programmeFreshDetail?.status ?? selectedProgrammeForDetail?.status ?? ApprovalStatus.Approved).className}`}>
{getApprovalStatusBadge(programmeFreshDetail?.status ?? selectedProgrammeForDetail?.status ?? ApprovalStatus.Approved).label}
</span>
</div>
{/* Core Details Grid: Owner & Creation Date */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
{/* Creation Date */}
<div className="p-3 bg-[#080d1a] rounded-lg border border-slate-800 flex items-center gap-2.5">
<Calendar className="w-4 h-4 text-slate-400 shrink-0" />
<div>
<span className="text-slate-500 font-medium block text-[10px] uppercase tracking-wider">Creation Date</span>
<span className="text-slate-200 font-mono text-xs font-semibold">
{formatDate(programmeFreshDetail?.createdAt || selectedProgrammeForDetail?.createdAt)}
</span>
</div>
</div>
{/* Owner */}
<div className="p-3 bg-[#080d1a] rounded-lg border border-slate-800 flex items-center gap-2.5">
<Mail className="w-4 h-4 text-slate-400 shrink-0" />
<div className="min-w-0 flex-1">
<span className="text-slate-500 font-medium block text-[10px] uppercase tracking-wider">Programme Lead</span>
<span className="font-mono text-slate-200 text-xs truncate block" title={getUserEmail(programmeFreshDetail?.ownedBy || selectedProgrammeForDetail?.ownedBy)}>
{getUserEmail(programmeFreshDetail?.ownedBy || selectedProgrammeForDetail?.ownedBy)}
</span>
</div>
</div>
</div>
{/* Metrics: Total Rules and Total Instructions */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-2">
{/* Total Rules */}
<div className="p-3 bg-gradient-to-br from-amber-500/10 to-amber-500/5 rounded-xl border border-amber-500/20 flex flex-col justify-between">
<div className="flex items-center justify-between mb-1">
<span className="text-[11px] font-semibold text-amber-300 flex items-center gap-1.5">
<Scale className="w-3.5 h-3.5 text-amber-400" />
Total Rules
</span>
<span className="text-lg font-bold font-mono text-amber-400">
{loadingDetailRules ? (
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
) : (
programmeDetailRules.length
)}
</span>
</div>
<button
type="button"
onClick={() => {
const pId = selectedProgrammeForDetail?.id;
setIsProgrammeDetailModalOpen(false);
setSelectedProgrammeForDetail(null);
setPreselectedProgForRule(pId || null);
handleTabChange('rules');
}}
className="text-[11px] font-semibold text-amber-400 hover:text-amber-300 inline-flex items-center gap-1 mt-1 transition cursor-pointer self-start"
>
View in Rules Tab &rarr;
</button>
</div>
{/* Total Instructions */}
<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
</span>
<span className="text-lg font-bold font-mono text-blue-400">
{loadingDetailInstructions ? (
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
) : (
programmeDetailInstructions.length
)}
</span>
</div>
<button
type="button"
onClick={() => {
const pId = selectedProgrammeForDetail?.id;
setIsProgrammeDetailModalOpen(false);
setSelectedProgrammeForDetail(null);
setPreselectedProgForInstruction(pId || null);
handleTabChange('instructions');
}}
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 &rarr;
</button>
</div>
</div>
</div>
@@ -75,6 +75,8 @@ const projectSchema = z.object({
programmeId: z.coerce.number().min(1, 'Select a programme'),
sector: z.coerce.number().min(1),
projectType: z.coerce.number().min(1),
startedAt: z.string().optional().nullable(),
endedAt: z.string().optional().nullable(),
});
const activitySchema = z.object({
@@ -83,6 +85,7 @@ const activitySchema = z.object({
category: z.string().min(2, 'Category required'),
amount: z.coerce.number().min(1, 'Amount must be positive'),
milestoneId: z.string().optional().nullable(),
performedAt: z.string().optional().nullable(),
});
const milestoneSchema = z.object({
@@ -164,6 +167,8 @@ export const ProjectsPage: React.FC = () => {
programmeId: programmes[0]?.id ? Number(programmes[0].id) : 0,
sector: IndustrySector.AgriTech,
projectType: ProjectType.Innovation,
startedAt: '',
endedAt: '',
},
});
@@ -175,6 +180,7 @@ export const ProjectsPage: React.FC = () => {
category: '',
amount: 0,
milestoneId: '',
performedAt: '',
},
});
@@ -197,6 +203,8 @@ export const ProjectsPage: React.FC = () => {
programmeId: programmes[0]?.id ? Number(programmes[0].id) : 0,
sector: IndustrySector.AgriTech,
projectType: ProjectType.Innovation,
startedAt: '',
endedAt: '',
});
setIsProjectModalOpen(true);
};
@@ -209,6 +217,8 @@ export const ProjectsPage: React.FC = () => {
programmeId: p.programmeId ? Number(p.programmeId) : (programmes[0]?.id ? Number(programmes[0].id) : 0),
sector: p.sector || IndustrySector.AgriTech,
projectType: p.projectType || ProjectType.Innovation,
startedAt: p.startedAt ? p.startedAt.slice(0, 10) : '',
endedAt: p.endedAt ? p.endedAt.slice(0, 10) : '',
});
setIsProjectModalOpen(true);
};
@@ -221,6 +231,7 @@ export const ProjectsPage: React.FC = () => {
category: '',
amount: 0,
milestoneId: '',
performedAt: new Date().toISOString().slice(0, 10),
});
setIsActivityModalOpen(true);
};
@@ -233,6 +244,7 @@ export const ProjectsPage: React.FC = () => {
category: a.category || '',
amount: a.amount || 0,
milestoneId: a.milestoneId ? String(a.milestoneId) : '',
performedAt: a.performedAt ? a.performedAt.slice(0, 10) : '',
});
setIsActivityModalOpen(true);
};
@@ -267,6 +279,8 @@ export const ProjectsPage: React.FC = () => {
projectId: editingProject.id,
name: data.name,
description: data.description,
startedAt: data.startedAt ? new Date(data.startedAt).toISOString() : null,
endedAt: data.endedAt ? new Date(data.endedAt).toISOString() : null,
sector: Number(data.sector),
projectType: Number(data.projectType),
});
@@ -310,6 +324,7 @@ export const ProjectsPage: React.FC = () => {
data.milestoneId && String(data.milestoneId).trim() !== ''
? Number(data.milestoneId)
: null;
const performedAtIso = data.performedAt ? new Date(data.performedAt).toISOString() : null;
if (editingActivity) {
return updateActivity({
@@ -320,6 +335,7 @@ export const ProjectsPage: React.FC = () => {
category: data.category,
amount: Number(data.amount),
milestoneId: parsedMilestoneId,
performedAt: performedAtIso,
});
}
return createActivity({
@@ -329,6 +345,7 @@ export const ProjectsPage: React.FC = () => {
category: data.category,
amount: Number(data.amount),
milestoneId: parsedMilestoneId,
performedAt: performedAtIso,
});
},
onSuccess: () => {
@@ -985,6 +1002,41 @@ export const ProjectsPage: React.FC = () => {
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">Start Date</label>
<HelpTooltip
title="Project Start Date"
content="Scheduled or actual project commencement date."
size="sm"
placement="top"
/>
</div>
<input
type="date"
{...projectForm.register('startedAt')}
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 text-white bg-[#090d16]"
/>
</div>
<div>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">Target Completion Date</label>
<HelpTooltip
title="Target Completion Date"
content="Projected or contractual conclusion date for statutory reporting."
size="sm"
placement="top"
/>
</div>
<input
type="date"
{...projectForm.register('endedAt')}
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 text-white bg-[#090d16]"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Project Scope & Objectives *</label>
<textarea
@@ -1112,6 +1164,23 @@ export const ProjectsPage: React.FC = () => {
</div>
</div>
<div>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">Execution / Performed Date</label>
<HelpTooltip
title="Activity Execution Date"
content="Scheduled or actual date on which the activity was performed."
size="sm"
placement="right"
/>
</div>
<input
type="date"
{...activityForm.register('performedAt')}
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 text-white bg-[#090d16]"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Description *</label>
<textarea