Improved compliance monitoring
This commit is contained in:
@@ -0,0 +1,510 @@
|
||||
import React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
ShieldCheck,
|
||||
ShieldAlert,
|
||||
AlertTriangle,
|
||||
Users,
|
||||
CheckCircle2,
|
||||
TrendingUp,
|
||||
Target,
|
||||
Clock,
|
||||
Coins,
|
||||
AlertCircle,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { Modal } from '../ui/Modal';
|
||||
import { HelpTooltip } from '../ui/HelpTooltip';
|
||||
import { ComplianceStatusBadge } from '../ui/StatusBadge';
|
||||
import {
|
||||
Project,
|
||||
Programme,
|
||||
ComplianceItem,
|
||||
Beneficiary,
|
||||
Outcome,
|
||||
ComplianceStatus,
|
||||
ApprovalStatus,
|
||||
} from '../../types';
|
||||
import {
|
||||
getProjectComplianceItems,
|
||||
getProjectBeneficiaries,
|
||||
getBeneficiaryOutcomes,
|
||||
getProjectRisks,
|
||||
getMilestones,
|
||||
getActivities,
|
||||
getBudgets,
|
||||
} from '../../services/api';
|
||||
import {
|
||||
formatDate,
|
||||
formatZAR,
|
||||
getRiskScoreColor,
|
||||
getIndustrySectorLabel,
|
||||
getProjectTypeLabel,
|
||||
} from '../../lib/formatters';
|
||||
|
||||
interface ProjectComplianceModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
project: Project | null;
|
||||
programme: Programme | null;
|
||||
}
|
||||
|
||||
export const ProjectComplianceModal: React.FC<ProjectComplianceModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
project,
|
||||
programme,
|
||||
}) => {
|
||||
const projectId = project?.id;
|
||||
|
||||
// Queries
|
||||
const { data: complianceItems = [], isLoading: loadingCompliance } = useQuery({
|
||||
queryKey: ['complianceItems', projectId],
|
||||
queryFn: () => (projectId ? getProjectComplianceItems(projectId) : Promise.resolve([])),
|
||||
enabled: isOpen && !!projectId,
|
||||
});
|
||||
|
||||
const { data: projectBeneficiaries = [], isLoading: loadingBeneficiaries } = useQuery({
|
||||
queryKey: ['projectBeneficiaries', projectId],
|
||||
queryFn: () => (projectId ? getProjectBeneficiaries(projectId) : Promise.resolve([])),
|
||||
enabled: isOpen && !!projectId,
|
||||
});
|
||||
|
||||
const { data: risks = [], isLoading: loadingRisks } = useQuery({
|
||||
queryKey: ['risks', projectId],
|
||||
queryFn: () => (projectId ? getProjectRisks(projectId) : Promise.resolve([])),
|
||||
enabled: isOpen && !!projectId,
|
||||
});
|
||||
|
||||
const { data: milestones = [] } = useQuery({
|
||||
queryKey: ['milestones', projectId],
|
||||
queryFn: () => (projectId ? getMilestones(projectId) : Promise.resolve([])),
|
||||
enabled: isOpen && !!projectId,
|
||||
});
|
||||
|
||||
const { data: activities = [] } = useQuery({
|
||||
queryKey: ['activities', projectId],
|
||||
queryFn: () => (projectId ? getActivities(projectId) : Promise.resolve([])),
|
||||
enabled: isOpen && !!projectId,
|
||||
});
|
||||
|
||||
const { data: allBudgets = [] } = useQuery({
|
||||
queryKey: ['budgets'],
|
||||
queryFn: () => getBudgets(1, 100),
|
||||
enabled: isOpen && !!projectId,
|
||||
});
|
||||
|
||||
// Beneficiaries already filtered by project
|
||||
|
||||
// Beneficiary outcomes query
|
||||
const { data: outcomes = [], isLoading: loadingOutcomes } = useQuery({
|
||||
queryKey: ['beneficiaryOutcomes', projectBeneficiaries.map((b) => b.id)],
|
||||
queryFn: async (): Promise<Outcome[]> => {
|
||||
if (projectBeneficiaries.length === 0) return [];
|
||||
const results = await Promise.all(
|
||||
projectBeneficiaries.map(async (b) => {
|
||||
try {
|
||||
return await getBeneficiaryOutcomes(b.id);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
);
|
||||
return results.flat();
|
||||
},
|
||||
enabled: isOpen && projectBeneficiaries.length > 0,
|
||||
});
|
||||
|
||||
if (!project) return null;
|
||||
|
||||
// Compliance calculations
|
||||
const totalCompCount = complianceItems.length;
|
||||
const metCompCount = complianceItems.filter(
|
||||
(c) => Number(c.status) === ComplianceStatus.Met || Number(c.status) === ComplianceStatus.Exceeded
|
||||
).length;
|
||||
const unmetCompCount = totalCompCount - metCompCount;
|
||||
|
||||
// Outcomes calculations
|
||||
const totalOutcomesCount = outcomes.length;
|
||||
const metOutcomesCount = outcomes.filter(
|
||||
(o) => Number(o.status) === ComplianceStatus.Met || Number(o.status) === ComplianceStatus.Exceeded
|
||||
).length;
|
||||
const unmetOutcomesCount = totalOutcomesCount - metOutcomesCount;
|
||||
|
||||
// Overall compliance verdict (Invariant: 100% met required for approval)
|
||||
const is100PercentCompliant =
|
||||
unmetCompCount === 0 && unmetOutcomesCount === 0;
|
||||
|
||||
// Financial calculations
|
||||
const projectBudgets = allBudgets.filter((b) => String(b.projectId) === String(projectId));
|
||||
const approvedBudgetTotal = projectBudgets
|
||||
.filter((b) => Number(b.status) === ApprovalStatus.Approved || b.status === undefined)
|
||||
.reduce((sum, b) => sum + (Number(b.amount) || 0), 0);
|
||||
const totalActivityBudget = activities.reduce((sum, a) => sum + (Number(a.amount) || 0), 0);
|
||||
const financialVariance = approvedBudgetTotal - totalActivityBudget;
|
||||
const isWithinBudget = financialVariance >= 0;
|
||||
|
||||
// Milestone Progress
|
||||
const verifiedMilestonesCount = milestones.filter((m) => m.status === ApprovalStatus.Approved).length;
|
||||
const milestoneProgress = milestones.length > 0 ? Math.round((verifiedMilestonesCount / milestones.length) * 100) : 0;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={`Risk & Compliance Status: ${project.name}`}
|
||||
subtitle={`Project #${project.id} • ${programme?.name || 'Programme'} • Statutory Governance Audit`}
|
||||
helpText="Comprehensive audit of project compliance items, beneficiary outcomes fulfillment, ISO 31000 risk register, and statutory approval eligibility."
|
||||
maxWidth="5xl"
|
||||
>
|
||||
<div className="space-y-6 text-slate-200">
|
||||
{/* Top Verdict Banner */}
|
||||
<div
|
||||
className={`p-4 sm:p-5 rounded-2xl border flex flex-col sm:flex-row sm:items-center justify-between gap-4 ${
|
||||
is100PercentCompliant
|
||||
? 'bg-emerald-950/30 border-emerald-500/40'
|
||||
: 'bg-rose-950/30 border-rose-500/40'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className={`p-2.5 rounded-xl border mt-0.5 ${
|
||||
is100PercentCompliant
|
||||
? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30'
|
||||
: 'bg-rose-500/20 text-rose-400 border-rose-500/30'
|
||||
}`}
|
||||
>
|
||||
{is100PercentCompliant ? (
|
||||
<ShieldCheck className="w-6 h-6" />
|
||||
) : (
|
||||
<ShieldAlert className="w-6 h-6" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-bold text-white">
|
||||
{is100PercentCompliant
|
||||
? '100% Fully Compliant — Approved for Project Governance'
|
||||
: 'Action Required: Project is Noncompliant'}
|
||||
</h4>
|
||||
<span
|
||||
className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded-full border ${
|
||||
is100PercentCompliant
|
||||
? 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40'
|
||||
: 'bg-rose-500/20 text-rose-300 border-rose-500/40 animate-pulse'
|
||||
}`}
|
||||
>
|
||||
{is100PercentCompliant ? 'Eligible for Approval' : 'Approvals Blocked'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-300 mt-1">
|
||||
{is100PercentCompliant
|
||||
? 'All statutory compliance requirements and beneficiary impact outcomes have met or exceeded threshold requirements.'
|
||||
: `This project has ${unmetCompCount} unmet compliance item(s) and ${unmetOutcomesCount} unmet beneficiary outcome(s). Per governance policy, projects cannot be approved until all compliance items and outcomes are 100% met.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 flex items-center gap-3">
|
||||
<div className="text-right">
|
||||
<span className="text-[10px] text-slate-400 block uppercase font-semibold">Budget At Risk</span>
|
||||
<span className={`text-base font-bold font-mono ${is100PercentCompliant ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||
{is100PercentCompliant ? 'R 0.00' : formatZAR(approvedBudgetTotal)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4 Summary Metric Cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3.5">
|
||||
<div className="bg-[#0b101c] p-3.5 rounded-xl border border-slate-800">
|
||||
<span className="text-[10px] uppercase font-bold text-slate-400 flex items-center gap-1.5">
|
||||
<ShieldCheck className="w-3.5 h-3.5 text-cyan-400" />
|
||||
Compliance Requirements
|
||||
</span>
|
||||
<div className="flex items-baseline gap-2 mt-1">
|
||||
<span className="text-xl font-bold font-mono text-cyan-400">
|
||||
{metCompCount}/{totalCompCount}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400 font-semibold">
|
||||
({totalCompCount > 0 ? Math.round((metCompCount / totalCompCount) * 100) : 100}%)
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-500">
|
||||
{unmetCompCount === 0 ? 'All items satisfied' : `${unmetCompCount} pending fulfillment`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0b101c] p-3.5 rounded-xl border border-slate-800">
|
||||
<span className="text-[10px] uppercase font-bold text-slate-400 flex items-center gap-1.5">
|
||||
<Target className="w-3.5 h-3.5 text-purple-400" />
|
||||
Beneficiary Outcomes
|
||||
</span>
|
||||
<div className="flex items-baseline gap-2 mt-1">
|
||||
<span className="text-xl font-bold font-mono text-purple-400">
|
||||
{metOutcomesCount}/{totalOutcomesCount}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400 font-semibold">
|
||||
({totalOutcomesCount > 0 ? Math.round((metOutcomesCount / totalOutcomesCount) * 100) : 100}%)
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-500">
|
||||
Across {projectBeneficiaries.length} beneficiary group{projectBeneficiaries.length === 1 ? '' : 's'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0b101c] p-3.5 rounded-xl border border-slate-800">
|
||||
<span className="text-[10px] uppercase font-bold text-slate-400 flex items-center gap-1.5">
|
||||
<Coins className="w-3.5 h-3.5 text-emerald-400" />
|
||||
Financial Variance
|
||||
</span>
|
||||
<div className="flex items-baseline gap-2 mt-1">
|
||||
<span
|
||||
className={`text-xl font-bold font-mono ${
|
||||
isWithinBudget ? 'text-emerald-400' : 'text-rose-400'
|
||||
}`}
|
||||
>
|
||||
{isWithinBudget ? `+${formatZAR(financialVariance)}` : `-${formatZAR(Math.abs(financialVariance))}`}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-500">
|
||||
Budget: {formatZAR(approvedBudgetTotal)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0b101c] p-3.5 rounded-xl border border-slate-800">
|
||||
<span className="text-[10px] uppercase font-bold text-slate-400 flex items-center gap-1.5">
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-amber-400" />
|
||||
Active Project Risks
|
||||
</span>
|
||||
<div className="flex items-baseline gap-2 mt-1">
|
||||
<span className="text-xl font-bold font-mono text-amber-400">{risks.length}</span>
|
||||
<span className="text-xs text-slate-400">
|
||||
({risks.filter((r) => (Number(r.likelihood) || 1) * (Number(r.impact) || 1) >= 12).length} High)
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-500">ISO 31000 risk register</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 1: Statutory Compliance Requirements */}
|
||||
<div className="bg-[#0b101c] rounded-2xl border border-slate-800 overflow-hidden shadow-sm">
|
||||
<div className="p-4 border-b border-slate-800 flex items-center justify-between bg-[#0d1322]">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="w-4 h-4 text-cyan-400" />
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-white">
|
||||
Statutory Compliance Requirements ({complianceItems.length})
|
||||
</h4>
|
||||
</div>
|
||||
<span className="text-[11px] text-slate-400">
|
||||
{metCompCount} of {complianceItems.length} Met / Exceeded
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{loadingCompliance ? (
|
||||
<div className="p-8 text-center text-xs text-slate-400">Loading compliance register...</div>
|
||||
) : complianceItems.length === 0 ? (
|
||||
<div className="p-6 text-center text-xs text-slate-500">
|
||||
No specific statutory compliance requirements captured for this project yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-900/60 border-b border-slate-800 text-slate-400 text-[11px] font-semibold">
|
||||
<th className="py-2.5 px-4">Title</th>
|
||||
<th className="py-2.5 px-4">Requirements Specification</th>
|
||||
<th className="py-2.5 px-4">Linked Beneficiary</th>
|
||||
<th className="py-2.5 px-4">Status</th>
|
||||
<th className="py-2.5 px-4 text-right">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/60">
|
||||
{complianceItems.map((ci) => {
|
||||
const linkedBen = projectBeneficiaries.find(
|
||||
(b) => String(b.id) === String(ci.beneficiaryId)
|
||||
);
|
||||
return (
|
||||
<tr key={ci.id} className="hover:bg-slate-900/30 transition">
|
||||
<td className="py-2.5 px-4 font-semibold text-white">{ci.title}</td>
|
||||
<td className="py-2.5 px-4 text-slate-300 max-w-sm truncate" title={ci.requirements || ''}>
|
||||
{ci.requirements || 'N/A'}
|
||||
</td>
|
||||
<td className="py-2.5 px-4 text-slate-300">
|
||||
{linkedBen ? (
|
||||
<span className="px-2 py-0.5 rounded bg-blue-500/10 border border-blue-500/20 text-blue-300 text-[11px]">
|
||||
{linkedBen.name}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-500 italic">Project-Wide</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2.5 px-4">
|
||||
<ComplianceStatusBadge status={ci.status} />
|
||||
</td>
|
||||
<td className="py-2.5 px-4 text-right text-slate-400 font-mono text-[11px]">
|
||||
{formatDate(ci.createdAt)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Section 2: Affected Beneficiaries & Mapped Outcomes */}
|
||||
<div className="bg-[#0b101c] rounded-2xl border border-slate-800 overflow-hidden shadow-sm">
|
||||
<div className="p-4 border-b border-slate-800 flex items-center justify-between bg-[#0d1322]">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-blue-400" />
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-white">
|
||||
Affected Beneficiaries & Mapped Impact Outcomes ({projectBeneficiaries.length})
|
||||
</h4>
|
||||
</div>
|
||||
<span className="text-[11px] text-slate-400">
|
||||
Total Target: {projectBeneficiaries.reduce((sum, b) => sum + (Number(b.targetCount) || 0), 0)} Reached:{' '}
|
||||
{projectBeneficiaries.reduce((sum, b) => sum + (Number(b.reachedCount) || 0), 0)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{loadingBeneficiaries || loadingOutcomes ? (
|
||||
<div className="p-8 text-center text-xs text-slate-400">Loading beneficiaries & outcomes...</div>
|
||||
) : projectBeneficiaries.length === 0 ? (
|
||||
<div className="p-6 text-center text-xs text-slate-500">
|
||||
No target beneficiaries mapped to this project yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 space-y-4">
|
||||
{projectBeneficiaries.map((b) => {
|
||||
const benOutcomes = outcomes.filter((o) => String(o.beneficiaryId) === String(b.id));
|
||||
return (
|
||||
<div
|
||||
key={b.id}
|
||||
className="p-3.5 rounded-xl bg-slate-900/60 border border-slate-800 space-y-3"
|
||||
>
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<div>
|
||||
<span className="font-bold text-white text-xs mr-2">{b.name}</span>
|
||||
<span className="text-[11px] text-slate-400 bg-slate-800 px-2 py-0.5 rounded border border-slate-700/60">
|
||||
{b.category} • {b.location}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs font-mono">
|
||||
<span className="text-slate-400">
|
||||
Target: <strong className="text-white">{b.targetCount}</strong>
|
||||
</span>
|
||||
<span className="text-slate-600">•</span>
|
||||
<span className="text-slate-400">
|
||||
Reached: <strong className="text-emerald-400">{b.reachedCount}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mapped Outcomes */}
|
||||
<div className="pt-2 border-t border-slate-800/80">
|
||||
<span className="text-[10px] uppercase font-bold text-slate-400 block mb-1.5">
|
||||
Mapped Expected Outcomes ({benOutcomes.length})
|
||||
</span>
|
||||
{benOutcomes.length === 0 ? (
|
||||
<p className="text-[11px] text-slate-500 italic">No expected outcomes recorded for this group.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{benOutcomes.map((o) => (
|
||||
<div
|
||||
key={o.id}
|
||||
className="p-2.5 rounded-lg bg-slate-950/60 border border-slate-800/80 flex items-start justify-between gap-2"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<span className="font-semibold text-slate-200 text-xs block truncate">{o.name}</span>
|
||||
<span className="text-[10px] text-slate-400 line-clamp-1">{o.description}</span>
|
||||
</div>
|
||||
<ComplianceStatusBadge status={o.status} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Section 3: Risk Profile (ISO 31000) */}
|
||||
<div className="bg-[#0b101c] rounded-2xl border border-slate-800 overflow-hidden shadow-sm">
|
||||
<div className="p-4 border-b border-slate-800 flex items-center justify-between bg-[#0d1322]">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-amber-400" />
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-white">
|
||||
Active Project Risks Register ({risks.length})
|
||||
</h4>
|
||||
</div>
|
||||
<span className="text-[11px] text-slate-400">ISO 31000 Risk Management</span>
|
||||
</div>
|
||||
|
||||
{loadingRisks ? (
|
||||
<div className="p-8 text-center text-xs text-slate-400">Loading risk register...</div>
|
||||
) : risks.length === 0 ? (
|
||||
<div className="p-6 text-center text-xs text-slate-500">
|
||||
No active risks logged on this project register.
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-900/60 border-b border-slate-800 text-slate-400 text-[11px] font-semibold">
|
||||
<th className="py-2.5 px-4">Risk Title</th>
|
||||
<th className="py-2.5 px-4">Category</th>
|
||||
<th className="py-2.5 px-4 text-center">Score</th>
|
||||
<th className="py-2.5 px-4">Mitigation Strategy</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/60">
|
||||
{risks.map((r) => {
|
||||
const score = (Number(r.likelihood) || 1) * (Number(r.impact) || 1);
|
||||
return (
|
||||
<tr key={r.id} className="hover:bg-slate-900/30 transition">
|
||||
<td className="py-2.5 px-4 font-semibold text-white">{r.title}</td>
|
||||
<td className="py-2.5 px-4 text-slate-300">{r.category || 'Operational'}</td>
|
||||
<td className="py-2.5 px-4 text-center">
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded text-[10px] font-bold ${
|
||||
score >= 12
|
||||
? 'bg-rose-500/20 text-rose-300 border border-rose-500/30'
|
||||
: score >= 8
|
||||
? 'bg-amber-500/20 text-amber-300 border border-amber-500/30'
|
||||
: 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'
|
||||
}`}
|
||||
>
|
||||
Score: {score}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2.5 px-4 text-slate-300 max-w-sm truncate" title={r.mitigation || ''}>
|
||||
{r.mitigation || 'No mitigation recorded'}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="flex justify-end pt-3 border-t border-slate-800">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-xl text-xs font-semibold text-slate-300 hover:text-white bg-slate-800 hover:bg-slate-700 transition"
|
||||
>
|
||||
Close Audit Inspector
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user