Refactored projects page, fixed security user modal

This commit is contained in:
2026-10-22 07:39:33 +02:00
parent c24e809b88
commit ccc49df720
7 changed files with 288 additions and 337 deletions
@@ -108,7 +108,6 @@ export const MonitoringPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'contracts' | 'evidence' | 'indicators' | 'risks'>(
tabParam === 'evidence' || tabParam === 'indicators' || tabParam === 'risks' ? tabParam : 'contracts'
);
const [isCreateContractModalOpen, setIsCreateContractModalOpen] = useState(false);
useEffect(() => {
const t = searchParams.get('tab');
@@ -437,15 +436,6 @@ export const MonitoringPage: React.FC = () => {
</div>
<div className="flex items-center gap-2 relative z-10 self-start md:self-center flex-wrap">
{activeTab === 'contracts' && canAdd('grant_financials') && (
<button
onClick={() => setIsCreateContractModalOpen(true)}
className="inline-flex items-center gap-1.5 px-4 py-2.5 text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 rounded-xl shadow-lg shadow-indigo-600/25 transition cursor-pointer"
>
<Plus className="w-4 h-4" />
<span>Create Contract &amp; Upload PDF</span>
</button>
)}
{activeTab === 'indicators' && canAdd('evidence_risks') && (
<button
onClick={() => setIsIndicatorModalOpen(true)}
@@ -541,10 +531,7 @@ export const MonitoringPage: React.FC = () => {
{/* Tab 0: Contracts & Legal Documents with PDF Upload */}
{activeTab === 'contracts' && (
<ContractsManagement
isCreateModalOpenExternal={isCreateContractModalOpen}
onCloseCreateModalExternal={() => setIsCreateContractModalOpen(false)}
/>
<ContractsManagement />
)}
{/* Tab 1: Project & Milestone Evidence Review Hub */}
@@ -723,23 +723,23 @@ export const ProjectsPage: React.FC = () => {
{/* Projects Table */}
<div className="overflow-x-auto">
<table className="w-full text-left text-xs text-slate-300 border-collapse">
<table className="w-full text-left text-xs text-slate-300 border-collapse min-w-[960px]">
<thead>
<tr className="border-b border-slate-800/80 bg-[#090d16] text-[11px] font-semibold text-slate-400 select-none">
<th className="py-3 px-3 w-10 text-center"></th>
<SortableTh column="name" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4">
<SortableTh column="name" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4 min-w-[180px]">
Project Name & ID
</SortableTh>
<SortableTh column="programme" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4">
<SortableTh column="programme" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4 min-w-[140px]">
Parent Programme
</SortableTh>
<SortableTh column="duration" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4">
<SortableTh column="duration" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4 min-w-[140px] whitespace-nowrap">
Schedule (Timeline)
</SortableTh>
<SortableTh column="progress" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4">
<SortableTh column="progress" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4 w-28 whitespace-nowrap">
WBS Progress
</SortableTh>
<SortableTh column="risk" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4">
<SortableTh column="risk" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4 min-w-[125px] whitespace-nowrap">
<div className="flex items-center gap-1">
<span>Schedule Risk</span>
<HelpTooltip
@@ -750,10 +750,10 @@ export const ProjectsPage: React.FC = () => {
/>
</div>
</SortableTh>
<SortableTh column="status" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4">
<SortableTh column="status" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4 w-32 whitespace-nowrap">
Status
</SortableTh>
<th className="py-3 px-4 text-right font-semibold">Resource Management Hub</th>
<th className="py-3 px-4 text-right font-semibold whitespace-nowrap min-w-[170px]">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60 font-medium">
@@ -952,8 +952,8 @@ export const ProjectsPage: React.FC = () => {
</td>
{/* Governance Status */}
<td className="py-4 px-4" onClick={(e) => e.stopPropagation()}>
<div className="space-y-1">
<td className="py-4 px-4 whitespace-nowrap" onClick={(e) => e.stopPropagation()}>
<div className="w-32">
{(() => {
const canApproveThis = canApproveRecord('projects_milestones', p.createdBy);
const isSelf = isMaker(p.createdBy);
@@ -969,16 +969,16 @@ export const ProjectsPage: React.FC = () => {
status: Number(e.target.value) as ApprovalStatus,
})
}
className="text-xs font-semibold bg-[#090d16] border border-slate-700/80 rounded-xl px-2.5 py-1 text-slate-200 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed"
className="w-full text-xs font-semibold bg-[#090d16] border border-slate-700/80 rounded-xl px-2.5 py-1 text-slate-200 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed truncate"
>
{APPROVAL_STATUS_OPTIONS.map((opt) => {
const isApprovedOpt = opt.value === ApprovalStatus.Approved;
const isOptDisabled = isApprovedOpt && !canApproveThis;
let reasonText = '';
if (isApprovedOpt && !canApprove('projects_milestones')) {
reasonText = ' (Requires Update Permission)';
reasonText = ' (Requires Update)';
} else if (isApprovedOpt && isSelf) {
reasonText = ' (Maker-Checker: Cannot approve own record)';
reasonText = ' (Maker-Checker)';
}
return (
<option key={opt.value} value={opt.value} disabled={isOptDisabled}>
@@ -993,57 +993,57 @@ export const ProjectsPage: React.FC = () => {
</td>
{/* Row Level Buttons: Milestones, Activities, Indicators, Risks, Edit */}
<td className="py-4 px-4 text-right" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-end gap-1.5 flex-wrap">
<td className="py-4 px-4 text-right whitespace-nowrap" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-end gap-1.5 flex-nowrap">
{/* Milestones Modal Button */}
<button
type="button"
onClick={(e) => handleOpenMilestones(p, e)}
className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-semibold text-blue-300 hover:text-white bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 rounded-lg transition cursor-pointer shadow-sm"
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-[11px] font-semibold text-blue-300 hover:text-white bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 rounded-xl transition cursor-pointer shadow-xs shrink-0"
title="Manage Contractual Milestones"
>
<Flag className="w-3 h-3 text-blue-400" />
<span>Milestones</span>
<Flag className="w-3.5 h-3.5 text-blue-400 shrink-0" />
<span className="hidden xl:inline">Milestones</span>
</button>
{/* Activities Modal Button */}
<button
type="button"
onClick={(e) => handleOpenActivities(p, e)}
className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-semibold text-cyan-300 hover:text-white bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/30 rounded-lg transition cursor-pointer shadow-sm"
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-[11px] font-semibold text-cyan-300 hover:text-white bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/30 rounded-xl transition cursor-pointer shadow-xs shrink-0"
title="Manage Work Breakdown Activities"
>
<Briefcase className="w-3 h-3 text-cyan-400" />
<span>Activities</span>
<Briefcase className="w-3.5 h-3.5 text-cyan-400 shrink-0" />
<span className="hidden xl:inline">Activities</span>
</button>
{/* Indicators Modal Button */}
<button
type="button"
onClick={(e) => handleOpenIndicators(p, e)}
className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-semibold text-indigo-300 hover:text-white bg-indigo-500/10 hover:bg-indigo-500/20 border border-indigo-500/30 rounded-lg transition cursor-pointer shadow-sm"
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-[11px] font-semibold text-indigo-300 hover:text-white bg-indigo-500/10 hover:bg-indigo-500/20 border border-indigo-500/30 rounded-xl transition cursor-pointer shadow-xs shrink-0"
title="Manage M&E Performance Indicators"
>
<Target className="w-3 h-3 text-indigo-400" />
<span>Indicators</span>
<Target className="w-3.5 h-3.5 text-indigo-400 shrink-0" />
<span className="hidden xl:inline">Indicators</span>
</button>
{/* Risks Modal Button */}
<button
type="button"
onClick={(e) => handleOpenRisks(p, e)}
className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-semibold text-rose-300 hover:text-white bg-rose-500/10 hover:bg-rose-500/20 border border-rose-500/30 rounded-lg transition cursor-pointer shadow-sm"
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-[11px] font-semibold text-rose-300 hover:text-white bg-rose-500/10 hover:bg-rose-500/20 border border-rose-500/30 rounded-xl transition cursor-pointer shadow-xs shrink-0"
title="Manage Risk Register (ISO 31000)"
>
<ShieldAlert className="w-3 h-3 text-rose-400" />
<span>Risks</span>
<ShieldAlert className="w-3.5 h-3.5 text-rose-400 shrink-0" />
<span className="hidden xl:inline">Risks</span>
</button>
{/* Edit Project Button */}
<button
type="button"
onClick={(e) => handleOpenEditProject(p, e)}
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
className="p-1.5 rounded-xl text-slate-400 hover:text-white hover:bg-slate-800 border border-transparent hover:border-slate-700/60 transition cursor-pointer shrink-0"
title="Edit Project Scope & Schedule"
>
<Edit2 className="w-3.5 h-3.5" />
@@ -1640,7 +1640,7 @@ export const SettingsPage: React.FC = () => {
</div>
<div className="overflow-x-auto min-w-0 w-full">
<table className="w-full text-left text-xs border-collapse min-w-[850px]">
<table className="w-full text-left text-xs border-collapse min-w-[680px]">
<thead>
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
<th className="py-3.5 px-3 w-10 text-center"></th>
@@ -1653,29 +1653,23 @@ export const SettingsPage: React.FC = () => {
<SortableTh column="action" currentColumn={auditSortCol} direction={auditSortDir} onSort={handleAuditSort} className="py-3.5 px-4">
Action
</SortableTh>
<SortableTh column="entityName" currentColumn={auditSortCol} direction={auditSortDir} onSort={handleAuditSort} className="py-3.5 px-4">
Entity
</SortableTh>
<SortableTh column="performedBy" currentColumn={auditSortCol} direction={auditSortDir} onSort={handleAuditSort} className="py-3.5 px-4">
Performed By
</SortableTh>
<SortableTh column="changes" currentColumn={auditSortCol} direction={auditSortDir} onSort={handleAuditSort} className="py-3.5 px-4">
Changes
</SortableTh>
<th className="py-3.5 px-5 text-right font-medium">Inspect</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{loadingAuditLogs ? (
<tr>
<td colSpan={8} className="py-12 text-center text-slate-500">
<td colSpan={6} className="py-12 text-center text-slate-500">
<RefreshCw className="w-6 h-6 animate-spin mx-auto mb-2 text-blue-400" />
Loading governance audit records...
</td>
</tr>
) : sortedAuditLogs.length === 0 ? (
<tr>
<td colSpan={8} className="py-12 text-center text-slate-500">
<td colSpan={6} className="py-12 text-center text-slate-500">
<History className="w-8 h-8 text-slate-600 mx-auto mb-2" />
{auditSearchTerm || auditEntityFilter !== 'all' || auditActionFilter !== 'all'
? 'No audit log entries match your filter criteria.'
@@ -1727,12 +1721,6 @@ export const SettingsPage: React.FC = () => {
{badge.label}
</span>
</td>
<td className="py-3.5 px-4 whitespace-nowrap">
<span className="font-semibold text-slate-200 block">{log.entityName || 'General'}</span>
{log.entityId && (
<span className="text-[10px] text-slate-500 font-mono">#{log.entityId}</span>
)}
</td>
<td
className="py-3.5 px-4"
onClick={(e) => e.stopPropagation()}
@@ -1760,25 +1748,6 @@ export const SettingsPage: React.FC = () => {
)}
</div>
</td>
<td className="py-3.5 px-4">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
toggleExpandAuditLog(log.id);
}}
className="inline-flex items-center gap-1.5 px-2 py-1 rounded-lg bg-slate-800/90 hover:bg-slate-700 border border-slate-700/80 text-[11px] text-slate-300 hover:text-white transition cursor-pointer"
title="Click to toggle changes shelf"
>
<Layers className="w-3 h-3 text-blue-400" />
<span>{changesCount} {changesCount === 1 ? 'change' : 'changes'}</span>
<ChevronDown
className={`w-3 h-3 text-slate-400 transition-transform duration-200 ${
isExpanded ? 'rotate-180 text-blue-400' : ''
}`}
/>
</button>
</td>
<td className="py-3.5 px-5 text-right whitespace-nowrap">
<button
type="button"
@@ -1798,7 +1767,7 @@ export const SettingsPage: React.FC = () => {
{/* Expanding Shelf for Changes Content */}
{isExpanded && (
<tr className="bg-[#080c14] border-b border-slate-800/80">
<td colSpan={8} className="p-4 sm:p-5 bg-gradient-to-r from-[#0d121f] via-[#090d16] to-[#0d121f]">
<td colSpan={6} className="p-4 sm:p-5 bg-gradient-to-r from-[#0d121f] via-[#090d16] to-[#0d121f]">
<div className="rounded-xl border border-slate-800/90 bg-[#070a12]/90 p-4 space-y-3 shadow-inner">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-slate-800/80 pb-3 flex-wrap">
<div className="flex items-center gap-2">