Fixed API calls on governance page
Enforced multitenancy
This commit is contained in:
@@ -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} • Owner: {getUserEmail(pr.ownedBy)} • {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 →
|
||||
</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 →
|
||||
</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
|
||||
|
||||
Reference in New Issue
Block a user