Revised all UIs
This commit is contained in:
@@ -46,6 +46,8 @@ import {
|
||||
createRule,
|
||||
updateRule,
|
||||
getPortfolioProgrammes,
|
||||
getPortfolioValue,
|
||||
getProgrammeValue,
|
||||
getUsers,
|
||||
getUser,
|
||||
} from '../services/api';
|
||||
@@ -77,6 +79,7 @@ import {
|
||||
getOrganisationStatusBadge,
|
||||
getApprovalStatusBadge,
|
||||
formatDate,
|
||||
formatZAR,
|
||||
getUserEmail,
|
||||
registerUserEmail,
|
||||
getUserIdByEmail,
|
||||
@@ -307,12 +310,24 @@ export const GovernancePage: React.FC = () => {
|
||||
enabled: !!selectedPortfolioForDetail && isPortfolioDetailModalOpen,
|
||||
});
|
||||
|
||||
const { data: portfolioValue = 0, isLoading: loadingPortfolioValue } = useQuery({
|
||||
queryKey: ['portfolio-value', selectedPortfolioForDetail?.id],
|
||||
queryFn: () => (selectedPortfolioForDetail ? getPortfolioValue(selectedPortfolioForDetail.id) : Promise.resolve(0)),
|
||||
enabled: !!selectedPortfolioForDetail && isPortfolioDetailModalOpen,
|
||||
});
|
||||
|
||||
const { data: programmeFreshDetail, isLoading: loadingProgrammeFreshDetail } = useQuery({
|
||||
queryKey: ['programme', selectedProgrammeForDetail?.id],
|
||||
queryFn: () => (selectedProgrammeForDetail ? getProgramme(selectedProgrammeForDetail.id) : null),
|
||||
enabled: !!selectedProgrammeForDetail && isProgrammeDetailModalOpen,
|
||||
});
|
||||
|
||||
const { data: programmeValue = 0, isLoading: loadingProgrammeValue } = useQuery({
|
||||
queryKey: ['programme-value', selectedProgrammeForDetail?.id],
|
||||
queryFn: () => (selectedProgrammeForDetail ? getProgrammeValue(selectedProgrammeForDetail.id) : Promise.resolve(0)),
|
||||
enabled: !!selectedProgrammeForDetail && isProgrammeDetailModalOpen,
|
||||
});
|
||||
|
||||
// Owner resolution helpers
|
||||
const resolveOwnerId = (emailOrId: string, fallbackOwnerId?: string | number | null): string => {
|
||||
const trimmed = (emailOrId || '').trim();
|
||||
@@ -2130,27 +2145,71 @@ export const GovernancePage: React.FC = () => {
|
||||
Fetching portfolio details...
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 bg-[#0d121f] rounded-xl border border-slate-800 space-y-3">
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Title:</span>
|
||||
<span className="text-white font-medium text-sm">{portfolioFreshDetail?.name || selectedPortfolioForDetail?.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Description:</span>
|
||||
<p className="text-slate-300 mt-0.5 leading-relaxed">{portfolioFreshDetail?.description || selectedPortfolioForDetail?.description || 'None provided'}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-3 border-t border-slate-800/80 items-center">
|
||||
<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(portfolioFreshDetail?.ownedBy || selectedPortfolioForDetail?.ownedBy)}>
|
||||
{getUserEmail(portfolioFreshDetail?.ownedBy || selectedPortfolioForDetail?.ownedBy)}
|
||||
<div className="space-y-3.5">
|
||||
{/* Prominent Hero Card: Total Portfolio Budget Value */}
|
||||
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-blue-950/80 via-[#0d172a] to-indigo-950/80 border border-blue-500/40 p-4 sm:p-5 shadow-xl">
|
||||
<div className="absolute top-0 right-0 -mr-6 -mt-6 w-32 h-32 bg-blue-500/15 rounded-full blur-2xl pointer-events-none" />
|
||||
<div className="flex items-center justify-between gap-3 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-xl bg-blue-500/20 border border-blue-500/40 flex items-center justify-center shrink-0">
|
||||
<Coins className="w-4 h-4 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-blue-400 block">
|
||||
Portfolio Total Budget Value
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-400">
|
||||
Aggregated budget across all linked strategic programmes
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono font-semibold px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-300 border border-blue-500/30">
|
||||
Live API Total
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-400 font-semibold text-[11px] uppercase tracking-wider shrink-0">Created:</span>
|
||||
<span className="font-mono text-slate-300 text-xs">
|
||||
{formatDate(portfolioFreshDetail?.createdAt || selectedPortfolioForDetail?.createdAt || '')}
|
||||
</span>
|
||||
|
||||
<div className="mt-3 flex items-baseline gap-2">
|
||||
{loadingPortfolioValue ? (
|
||||
<div className="flex items-center gap-2 text-blue-300 py-1">
|
||||
<RefreshCw className="w-4 h-4 animate-spin text-blue-400" />
|
||||
<span className="font-mono text-xs">Calculating portfolio budget value...</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-2xl sm:text-3xl font-extrabold font-mono tracking-tight text-white drop-shadow-sm">
|
||||
{formatZAR(portfolioValue)}
|
||||
</span>
|
||||
<span className="text-xs font-semibold text-blue-400 font-mono">
|
||||
ZAR
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Portfolio Master Info Card */}
|
||||
<div className="p-4 bg-[#0d121f] rounded-xl border border-slate-800 space-y-3">
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase tracking-wider">Portfolio Title:</span>
|
||||
<span className="text-white font-semibold text-sm block mt-0.5">{portfolioFreshDetail?.name || selectedPortfolioForDetail?.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase tracking-wider">Description:</span>
|
||||
<p className="text-slate-300 mt-1 leading-relaxed">{portfolioFreshDetail?.description || selectedPortfolioForDetail?.description || 'None provided'}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-3 border-t border-slate-800/80 items-center">
|
||||
<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(portfolioFreshDetail?.ownedBy || selectedPortfolioForDetail?.ownedBy)}>
|
||||
{getUserEmail(portfolioFreshDetail?.ownedBy || selectedPortfolioForDetail?.ownedBy)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-400 font-semibold text-[11px] uppercase tracking-wider shrink-0">Created:</span>
|
||||
<span className="font-mono text-slate-300 text-xs">
|
||||
{formatDate(portfolioFreshDetail?.createdAt || selectedPortfolioForDetail?.createdAt || '')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2188,102 +2247,146 @@ export const GovernancePage: React.FC = () => {
|
||||
Fetching programme details...
|
||||
</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 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 className="space-y-3.5">
|
||||
{/* Prominent Hero Card: Total Programme Budget Value */}
|
||||
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-emerald-950/80 via-[#0a1b24] to-teal-950/80 border border-emerald-500/40 p-4 sm:p-5 shadow-xl">
|
||||
<div className="absolute top-0 right-0 -mr-6 -mt-6 w-32 h-32 bg-emerald-500/15 rounded-full blur-2xl pointer-events-none" />
|
||||
<div className="flex items-center justify-between gap-3 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-xl bg-emerald-500/20 border border-emerald-500/40 flex items-center justify-center shrink-0">
|
||||
<Coins className="w-4 h-4 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-emerald-400 block">
|
||||
Programme Total Budget Value
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-400">
|
||||
Calculated across all constituent project budgets
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono font-semibold px-2 py-0.5 rounded-full bg-emerald-500/20 text-emerald-300 border border-emerald-500/30">
|
||||
Live API Total
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-baseline gap-2">
|
||||
{loadingProgrammeValue ? (
|
||||
<div className="flex items-center gap-2 text-emerald-300 py-1">
|
||||
<RefreshCw className="w-4 h-4 animate-spin text-emerald-400" />
|
||||
<span className="font-mono text-xs">Calculating programme budget value...</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-2xl sm:text-3xl font-extrabold font-mono tracking-tight text-white drop-shadow-sm">
|
||||
{formatZAR(programmeValue)}
|
||||
</span>
|
||||
<span className="text-xs font-semibold text-emerald-400 font-mono">
|
||||
ZAR
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<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" />
|
||||
{/* Programme Details Card */}
|
||||
<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 flex items-start justify-between gap-4">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
{/* 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>
|
||||
</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>
|
||||
{/* 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">
|
||||
<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 ? (
|
||||
<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('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 Funding Instruments Tab →
|
||||
</button>
|
||||
</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 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">
|
||||
<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 ? (
|
||||
<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('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 Funding Instruments Tab →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user