PartnerUI evidence, notification, indicators and actuals fixes
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
X,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
AlertTriangle,
|
||||
Scale,
|
||||
FileCheck,
|
||||
Info,
|
||||
Calendar,
|
||||
User,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Layers,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { Project, Milestone, Activity, ComplianceItem, Risk, Rule, ComplianceStatus } from '../../services/api/models';
|
||||
import { getProjectComplianceItems, getProjectRisks, getRules } from '../../services/api';
|
||||
import { formatDate } from '../../lib/formatters';
|
||||
|
||||
interface GovernanceInfoModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
project: Project;
|
||||
milestone?: Milestone | null;
|
||||
activity?: Activity | null;
|
||||
initialTab?: 'compliance' | 'risks' | 'rules';
|
||||
}
|
||||
|
||||
export const GovernanceInfoModal: React.FC<GovernanceInfoModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
project,
|
||||
milestone,
|
||||
activity,
|
||||
initialTab = 'compliance',
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'compliance' | 'risks' | 'rules'>(initialTab);
|
||||
|
||||
const projectId = project?.id || 0;
|
||||
|
||||
// 1. Fetch compliance items for project
|
||||
const { data: complianceItems = [], isLoading: isLoadingCompliance } = useQuery<ComplianceItem[]>({
|
||||
queryKey: ['governance-compliance', String(projectId)],
|
||||
queryFn: () => getProjectComplianceItems(projectId),
|
||||
enabled: isOpen && Boolean(projectId),
|
||||
});
|
||||
|
||||
// 2. Fetch risks for project
|
||||
const { data: projectRisks = [], isLoading: isLoadingRisks } = useQuery<Risk[]>({
|
||||
queryKey: ['governance-risks', String(projectId)],
|
||||
queryFn: () => getProjectRisks(projectId),
|
||||
enabled: isOpen && Boolean(projectId),
|
||||
});
|
||||
|
||||
// 3. Fetch rules
|
||||
const { data: governanceRules = [], isLoading: isLoadingRules } = useQuery<Rule[]>({
|
||||
queryKey: ['governance-rules'],
|
||||
queryFn: () => getRules(1, 100),
|
||||
enabled: isOpen,
|
||||
});
|
||||
|
||||
if (!isOpen || typeof document === 'undefined') return null;
|
||||
|
||||
const renderComplianceStatus = (status?: number) => {
|
||||
switch (status) {
|
||||
case ComplianceStatus.Compliant:
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/15 text-emerald-300 border border-emerald-500/30">
|
||||
<CheckCircle2 className="w-3 h-3 text-emerald-400" />
|
||||
<span>Compliant</span>
|
||||
</span>
|
||||
);
|
||||
case ComplianceStatus.UnderReview:
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-amber-500/15 text-amber-300 border border-amber-500/30">
|
||||
<Clock className="w-3 h-3 text-amber-400" />
|
||||
<span>Under Review</span>
|
||||
</span>
|
||||
);
|
||||
case ComplianceStatus.NonCompliant:
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-rose-500/15 text-rose-300 border border-rose-500/30">
|
||||
<AlertTriangle className="w-3 h-3 text-rose-400" />
|
||||
<span>Non-Compliant</span>
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-800 text-slate-300 border border-white/10">
|
||||
<span>Pending Submission</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4 sm:p-6 bg-slate-950/85 backdrop-blur-md animate-in fade-in duration-200 overflow-y-auto">
|
||||
<div className="relative w-full max-w-4xl rounded-3xl border border-white/10 bg-slate-900 shadow-2xl p-6 sm:p-8 space-y-6 my-auto text-slate-100 max-h-[90vh] flex flex-col">
|
||||
{/* Modal Header */}
|
||||
<div className="flex items-start justify-between border-b border-white/[0.08] pb-4 shrink-0">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-md text-[10px] font-mono font-bold bg-emerald-500/15 text-emerald-300 border border-emerald-500/30">
|
||||
<ShieldAlert className="w-3 h-3 text-emerald-400" />
|
||||
<span>Governance & Compliance Guidance</span>
|
||||
</span>
|
||||
{activity ? (
|
||||
<span className="text-[11px] text-slate-400 font-mono">Activity: {activity.title}</span>
|
||||
) : milestone ? (
|
||||
<span className="text-[11px] text-slate-400 font-mono">Milestone: {milestone.name}</span>
|
||||
) : (
|
||||
<span className="text-[11px] text-slate-400 font-mono">Project: {project.name}</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-lg sm:text-xl font-bold text-white tracking-tight">
|
||||
Compliance Items, Risks & Institutional Rules
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400">
|
||||
Contractual obligations, risk mitigations, and execution requirements for grant fulfillment.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="p-1.5 rounded-xl text-slate-400 hover:text-white hover:bg-white/[0.06] transition-colors cursor-pointer shrink-0"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex items-center gap-2 border-b border-white/[0.08] pb-3 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('compliance')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-semibold transition-all cursor-pointer ${
|
||||
activeTab === 'compliance'
|
||||
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 shadow-sm'
|
||||
: 'text-slate-400 hover:text-white hover:bg-white/[0.04]'
|
||||
}`}
|
||||
>
|
||||
<FileCheck className="w-4 h-4" />
|
||||
<span>Compliance Items ({complianceItems.length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('risks')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-semibold transition-all cursor-pointer ${
|
||||
activeTab === 'risks'
|
||||
? 'bg-amber-500/20 text-amber-300 border border-amber-500/40 shadow-sm'
|
||||
: 'text-slate-400 hover:text-white hover:bg-white/[0.04]'
|
||||
}`}
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<span>Risk Register ({projectRisks.length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('rules')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-semibold transition-all cursor-pointer ${
|
||||
activeTab === 'rules'
|
||||
? 'bg-teal-500/20 text-teal-300 border border-teal-500/40 shadow-sm'
|
||||
: 'text-slate-400 hover:text-white hover:bg-white/[0.04]'
|
||||
}`}
|
||||
>
|
||||
<Scale className="w-4 h-4" />
|
||||
<span>Governance Rules ({governanceRules.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Content Panels */}
|
||||
<div className="flex-1 overflow-y-auto space-y-4 pr-1 [scrollbar-width:thin] [scrollbar-color:#334155_transparent]">
|
||||
{/* TAB 1: COMPLIANCE ITEMS */}
|
||||
{activeTab === 'compliance' && (
|
||||
<div className="space-y-3">
|
||||
{isLoadingCompliance ? (
|
||||
<div className="p-8 text-center text-xs text-slate-400">Loading compliance requirements...</div>
|
||||
) : complianceItems.length === 0 ? (
|
||||
<div className="p-8 rounded-2xl bg-slate-950/60 border border-white/5 text-center text-xs text-slate-400 space-y-2">
|
||||
<FileCheck className="w-8 h-8 text-slate-500 mx-auto" />
|
||||
<p>No specific compliance items registered for this project.</p>
|
||||
<p className="text-[11px] text-slate-500">
|
||||
Standard grant statutory terms apply as per your signed funding agreement.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
complianceItems.map((item, idx) => (
|
||||
<div
|
||||
key={item.id || idx}
|
||||
className="p-4 rounded-2xl bg-slate-950/70 border border-white/[0.08] space-y-2.5"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-semibold text-sm text-slate-100">
|
||||
{item.title || `Compliance Item #${idx + 1}`}
|
||||
</span>
|
||||
{renderComplianceStatus(item.status)}
|
||||
</div>
|
||||
{item.dueDate && (
|
||||
<div className="text-[11px] text-slate-400 flex items-center gap-1 font-mono">
|
||||
<Calendar className="w-3.5 h-3.5 text-slate-500" />
|
||||
<span>Due: {formatDate(item.dueDate)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{item.description && (
|
||||
<p className="text-xs text-slate-300 leading-relaxed">{item.description}</p>
|
||||
)}
|
||||
{item.notes && (
|
||||
<div className="text-[11px] text-emerald-400/90 bg-emerald-950/40 border border-emerald-500/20 p-2.5 rounded-xl font-mono">
|
||||
Note: {item.notes}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 2: RISK REGISTER */}
|
||||
{activeTab === 'risks' && (
|
||||
<div className="space-y-3">
|
||||
{isLoadingRisks ? (
|
||||
<div className="p-8 text-center text-xs text-slate-400">Loading risk register...</div>
|
||||
) : projectRisks.length === 0 ? (
|
||||
<div className="p-8 rounded-2xl bg-slate-950/60 border border-white/5 text-center text-xs text-slate-400 space-y-2">
|
||||
<ShieldCheck className="w-8 h-8 text-emerald-500 mx-auto" />
|
||||
<p>No high-severity risks recorded for this project.</p>
|
||||
<p className="text-[11px] text-slate-500">
|
||||
Maintain milestone delivery cadence to preserve low-risk standing.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
projectRisks.map((risk, idx) => (
|
||||
<div
|
||||
key={risk.id || idx}
|
||||
className="p-4 rounded-2xl bg-slate-950/70 border border-white/[0.08] space-y-2.5"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<h4 className="font-semibold text-sm text-slate-100">
|
||||
{risk.title || `Risk Item #${idx + 1}`}
|
||||
</h4>
|
||||
<div className="flex items-center gap-2 text-[10px] font-mono">
|
||||
<span className="px-2 py-0.5 rounded-md bg-amber-500/15 text-amber-300 border border-amber-500/30">
|
||||
Likelihood: {risk.likelihood ?? 'Medium'}
|
||||
</span>
|
||||
<span className="px-2 py-0.5 rounded-md bg-rose-500/15 text-rose-300 border border-rose-500/30">
|
||||
Impact: {risk.impact ?? 'Moderate'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{risk.description && (
|
||||
<p className="text-xs text-slate-300 leading-relaxed">{risk.description}</p>
|
||||
)}
|
||||
{risk.mitigation && (
|
||||
<div className="text-[11px] text-teal-300 bg-teal-950/40 border border-teal-500/20 p-2.5 rounded-xl">
|
||||
<strong>Mitigation Strategy:</strong> {risk.mitigation}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 3: GOVERNANCE & RULES */}
|
||||
{activeTab === 'rules' && (
|
||||
<div className="space-y-3">
|
||||
{isLoadingRules ? (
|
||||
<div className="p-8 text-center text-xs text-slate-400">Loading institutional rules...</div>
|
||||
) : governanceRules.length === 0 ? (
|
||||
<div className="p-8 rounded-2xl bg-slate-950/60 border border-white/5 text-center text-xs text-slate-400 space-y-2">
|
||||
<Scale className="w-8 h-8 text-slate-500 mx-auto" />
|
||||
<p>Standard PFM Funding Portfolio Governance Rules apply.</p>
|
||||
<p className="text-[11px] text-slate-500">
|
||||
All disbursements gate against verified milestone deliverables and evidence.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
governanceRules.map((rule, idx) => (
|
||||
<div
|
||||
key={rule.id || idx}
|
||||
className="p-4 rounded-2xl bg-slate-950/70 border border-white/[0.08] space-y-2"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h4 className="font-semibold text-sm text-slate-100">
|
||||
{rule.name || `Rule #${idx + 1}`}
|
||||
</h4>
|
||||
{rule.category && (
|
||||
<span className="text-[10px] font-mono px-2 py-0.5 rounded-md bg-slate-800 text-slate-300 border border-white/10">
|
||||
{rule.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{rule.description && (
|
||||
<p className="text-xs text-slate-300 leading-relaxed">{rule.description}</p>
|
||||
)}
|
||||
{rule.condition && (
|
||||
<div className="text-[11px] text-slate-400 bg-slate-900 border border-white/5 p-2 rounded-xl font-mono">
|
||||
Condition: {rule.condition}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-white/[0.08] shrink-0">
|
||||
<span className="text-[11px] text-slate-500 font-mono">
|
||||
PFM Governance Architecture • Clean Audit Policy
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-5 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-semibold transition-colors cursor-pointer"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user