Refactored project view into modal

Elevated toast message priority
This commit is contained in:
2026-10-22 09:41:41 +02:00
parent ccc49df720
commit 0261a6093e
6 changed files with 1454 additions and 441 deletions
@@ -21,6 +21,7 @@ import {
AlertCircle,
CheckCircle2,
SlidersHorizontal,
Eye,
} from 'lucide-react';
import {
getProjects,
@@ -57,7 +58,7 @@ import {
APPROVAL_STATUS_OPTIONS,
getUserEmail,
} from '../lib/formatters';
import { ProjectExpandedRow } from '../components/projects/ProjectExpandedRow';
import { ProjectViewModal } from '../components/projects/ProjectViewModal';
import { ProjectFormModal } from '../components/projects/ProjectFormModal';
import { ProjectMilestonesModal } from '../components/projects/ProjectMilestonesModal';
import { ProjectActivitiesModal } from '../components/projects/ProjectActivitiesModal';
@@ -158,8 +159,9 @@ export const ProjectsPage: React.FC = () => {
const [selectedSectorFilter, setSelectedSectorFilter] = useState<string | number | 'all'>('all');
const [selectedStatusFilter, setSelectedStatusFilter] = useState<string | number | 'all'>('all');
// Expanded Rows State (store project IDs)
const [expandedRowIds, setExpandedRowIds] = useState<Set<string | number>>(new Set());
// Active Project View Modal State
const [viewingProject, setViewingProject] = useState<Project | null>(null);
const [isProjectViewModalOpen, setIsProjectViewModalOpen] = useState(false);
// Modals Management State
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
@@ -369,26 +371,11 @@ export const ProjectsPage: React.FC = () => {
customExtractors
);
// Toggle Single Row Expansion
const toggleRow = (id: string | number) => {
setExpandedRowIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
// Expand / Collapse All Rows
const toggleAllRows = () => {
if (expandedRowIds.size === sortedProjects.length && sortedProjects.length > 0) {
setExpandedRowIds(new Set());
} else {
setExpandedRowIds(new Set(sortedProjects.map((p) => p.id)));
}
// Handler to open comprehensive Project View Modal
const handleOpenProjectView = (p: Project, e?: React.MouseEvent) => {
if (e) e.stopPropagation();
setViewingProject(p);
setIsProjectViewModalOpen(true);
};
// Handlers to open resource modals
@@ -447,7 +434,7 @@ export const ProjectsPage: React.FC = () => {
}, [projects, milestonesByProject]);
return (
<div className="space-y-6 max-w-7xl mx-auto pb-12">
<div className="space-y-6 w-full max-w-7xl 2xl:max-w-[1600px] mx-auto pb-12">
{/* Header Banner */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-gradient-to-r from-[#0d1322] via-[#0f172a] to-[#0d1322] p-6 sm:p-8 rounded-3xl border border-slate-800/80 shadow-2xl relative overflow-hidden">
<div className="absolute -right-10 -bottom-10 w-60 h-60 bg-blue-600/10 rounded-full blur-3xl pointer-events-none" />
@@ -703,43 +690,30 @@ export const ProjectsPage: React.FC = () => {
<h3 className="text-sm font-bold text-white uppercase tracking-wider">
Funded Projects Directory ({sortedProjects.length})
</h3>
<span className="text-xs text-slate-500 hidden sm:inline">
&bull; Click any row to expand the Gantt timeline & resource details
<span className="text-xs text-slate-400 hidden sm:inline">
&bull; Click "View Project" or project row to open the Project View modal & Gantt timeline
</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={toggleAllRows}
className="text-xs font-semibold text-slate-300 hover:text-white bg-slate-800/80 hover:bg-slate-700/80 border border-slate-700/80 px-3 py-1.5 rounded-xl transition cursor-pointer"
>
{expandedRowIds.size === sortedProjects.length && sortedProjects.length > 0
? 'Collapse All'
: 'Expand All'}
</button>
</div>
</div>
{/* Projects Table */}
<div className="overflow-x-auto">
<table className="w-full text-left text-xs text-slate-300 border-collapse min-w-[960px]">
<div className="overflow-x-auto scrollbar-thin">
<table className="w-full text-left text-xs text-slate-300 border-collapse min-w-full">
<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 min-w-[180px]">
<SortableTh column="name" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-3 min-w-[160px] max-w-[240px]">
Project Name & ID
</SortableTh>
<SortableTh column="programme" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4 min-w-[140px]">
<SortableTh column="programme" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-3 w-36">
Parent Programme
</SortableTh>
<SortableTh column="duration" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4 min-w-[140px] whitespace-nowrap">
<SortableTh column="duration" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-3 w-36 whitespace-nowrap">
Schedule (Timeline)
</SortableTh>
<SortableTh column="progress" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4 w-28 whitespace-nowrap">
<SortableTh column="progress" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-3 w-28 whitespace-nowrap">
WBS Progress
</SortableTh>
<SortableTh column="risk" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4 min-w-[125px] whitespace-nowrap">
<SortableTh column="risk" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-3 w-32 whitespace-nowrap">
<div className="flex items-center gap-1">
<span>Schedule Risk</span>
<HelpTooltip
@@ -750,16 +724,16 @@ export const ProjectsPage: React.FC = () => {
/>
</div>
</SortableTh>
<SortableTh column="status" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-4 w-32 whitespace-nowrap">
<SortableTh column="status" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3 px-3 w-32 whitespace-nowrap">
Status
</SortableTh>
<th className="py-3 px-4 text-right font-semibold whitespace-nowrap min-w-[170px]">Actions</th>
<th className="py-3 px-3 text-right font-semibold whitespace-nowrap w-44">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60 font-medium">
{loadingProjects ? (
<tr>
<td colSpan={8} className="p-12 text-center text-slate-400 text-xs">
<td colSpan={7} className="p-12 text-center text-slate-400 text-xs">
<div className="flex flex-col items-center justify-center space-y-2">
<div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
<span>Loading project directory...</span>
@@ -768,7 +742,7 @@ export const ProjectsPage: React.FC = () => {
</tr>
) : sortedProjects.length === 0 ? (
<tr>
<td colSpan={8} className="p-12 text-center text-slate-400">
<td colSpan={7} className="p-12 text-center text-slate-400">
<div className="flex flex-col items-center justify-center space-y-3">
<FolderKanban className="w-10 h-10 text-slate-600" />
<p className="text-sm font-semibold text-slate-300">No matching projects found</p>
@@ -789,7 +763,6 @@ export const ProjectsPage: React.FC = () => {
</tr>
) : (
sortedProjects.map((p) => {
const isExpanded = expandedRowIds.has(p.id);
const prog = programmeMap.get(p.programmeId);
const milestoneStats = milestonesByProject.get(p.id) || { total: 0, verified: 0 };
const progressPct =
@@ -799,58 +772,50 @@ export const ProjectsPage: React.FC = () => {
const scheduleRisk = evaluateProjectScheduleRisk(p, milestoneStats);
return (
<React.Fragment key={p.id}>
{/* Main Project Row */}
<tr
onClick={() => toggleRow(p.id)}
className={`transition-colors cursor-pointer group ${
isExpanded
? 'bg-[#0f172a]/90 text-white'
: 'hover:bg-slate-900/60 text-slate-300'
}`}
>
{/* Expand/Collapse Chevron */}
<td className="py-4 px-3 text-center">
<button
type="button"
aria-label="Expand project row"
className="p-1 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition"
>
{isExpanded ? (
<ChevronDown className="w-4 h-4 text-blue-400" />
) : (
<ChevronRight className="w-4 h-4 group-hover:text-white" />
)}
</button>
</td>
{/* Project Name & Meta */}
<td className="py-4 px-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="font-bold text-sm text-white group-hover:text-blue-300 transition">
{p.name}
</span>
</div>
<div className="flex items-center gap-2 flex-wrap text-[11px]">
<span className="text-[10px] font-mono text-blue-400 bg-blue-500/10 px-1.5 py-0.2 rounded border border-blue-500/25">
#{p.id}
</span>
<span className="bg-slate-900/90 border border-slate-700/80 px-2 py-0.5 rounded text-slate-300 text-[10px]">
{getIndustrySectorLabel(p.sector)}
</span>
<span className="text-slate-500">&bull;</span>
<span className="text-slate-400 text-[10px]">
{getProjectTypeLabel(p.projectType)}
</span>
</div>
<tr
key={p.id}
onClick={() => handleOpenProjectView(p)}
className="transition-colors cursor-pointer hover:bg-slate-900/60 text-slate-300 group"
>
{/* Project Name & Meta */}
<td className="py-3.5 px-3 max-w-[240px]">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span
className="font-bold text-sm text-white group-hover:text-blue-300 transition line-clamp-1"
title={p.name || `Project #${p.id}`}
>
{p.name || `Project #${p.id}`}
</span>
</div>
</td>
<div className="flex items-center gap-1.5 flex-wrap text-[11px]">
<span className="text-[10px] font-mono text-blue-400 bg-blue-500/10 px-1.5 py-0.2 rounded border border-blue-500/25 shrink-0">
#{p.id}
</span>
<span
className="bg-slate-900/90 border border-slate-700/80 px-2 py-0.5 rounded text-slate-300 text-[10px] truncate max-w-[120px]"
title={getIndustrySectorLabel(p.sector)}
>
{getIndustrySectorLabel(p.sector)}
</span>
<span className="text-slate-500">&bull;</span>
<span
className="text-slate-400 text-[10px] truncate max-w-[120px]"
title={getProjectTypeLabel(p.projectType)}
>
{getProjectTypeLabel(p.projectType)}
</span>
</div>
</div>
</td>
{/* Parent Programme */}
<td className="py-4 px-4">
<td className="py-3.5 px-3 w-36">
<div className="space-y-0.5">
<span className="text-xs font-semibold text-slate-200 block truncate max-w-[160px]">
<span
className="text-xs font-semibold text-slate-200 block truncate max-w-[130px]"
title={prog?.name || `Programme #${p.programmeId}`}
>
{prog?.name || `Prog #${p.programmeId}`}
</span>
<span className="text-[10px] text-slate-500 font-mono">
@@ -860,8 +825,8 @@ export const ProjectsPage: React.FC = () => {
</td>
{/* Schedule (Dates) */}
<td className="py-4 px-4">
<div className="space-y-0.5 font-mono text-[11px] text-slate-400">
<td className="py-3.5 px-3 w-36">
<div className="space-y-0.5 font-mono text-[11px] text-slate-400 whitespace-nowrap">
<div className="flex items-center gap-1.5 text-slate-300">
<Calendar className="w-3.5 h-3.5 text-slate-500 shrink-0" />
<span>{formatDate(p.startedAt)} &rarr; {formatDate(p.endedAt)}</span>
@@ -882,8 +847,8 @@ export const ProjectsPage: React.FC = () => {
</td>
{/* Milestone Progress */}
<td className="py-4 px-4">
<div className="space-y-1 w-32">
<td className="py-3.5 px-3 w-28">
<div className="space-y-1 w-24">
<div className="flex justify-between text-[11px] font-semibold">
<span className={progressPct >= 100 ? 'text-emerald-400' : 'text-blue-400'}>
{progressPct}%
@@ -906,7 +871,7 @@ export const ProjectsPage: React.FC = () => {
</td>
{/* Schedule Risk Column with Icon & Tooltip */}
<td className="py-4 px-4" onClick={(e) => e.stopPropagation()}>
<td className="py-3.5 px-3 w-32" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center gap-1.5">
{scheduleRisk.isOverdue ? (
<span
@@ -952,8 +917,8 @@ export const ProjectsPage: React.FC = () => {
</td>
{/* Governance Status */}
<td className="py-4 px-4 whitespace-nowrap" onClick={(e) => e.stopPropagation()}>
<div className="w-32">
<td className="py-3.5 px-3 whitespace-nowrap w-32" onClick={(e) => e.stopPropagation()}>
<div className="w-28">
{(() => {
const canApproveThis = canApproveRecord('projects_milestones', p.createdBy);
const isSelf = isMaker(p.createdBy);
@@ -969,7 +934,7 @@ export const ProjectsPage: React.FC = () => {
status: Number(e.target.value) as ApprovalStatus,
})
}
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"
className="w-full text-xs font-semibold bg-[#090d16] border border-slate-700/80 rounded-xl px-2 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;
@@ -992,51 +957,62 @@ export const ProjectsPage: React.FC = () => {
</div>
</td>
{/* Row Level Buttons: Milestones, Activities, Indicators, Risks, Edit */}
<td className="py-4 px-4 text-right whitespace-nowrap" onClick={(e) => e.stopPropagation()}>
{/* Row Level Buttons: View Project, Milestones, Activities, Indicators, Risks, Edit */}
<td className="py-3.5 px-3 text-right whitespace-nowrap w-44" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-end gap-1.5 flex-nowrap">
{/* Primary View Project Modal Button */}
<button
type="button"
onClick={(e) => handleOpenProjectView(p, e)}
className="inline-flex items-center gap-1.5 px-3 py-1.5 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 transition cursor-pointer shadow-sm shadow-blue-600/25 shrink-0"
title="Open Project View & Interactive Gantt Timeline"
>
<Eye className="w-3.5 h-3.5" />
<span>View Project</span>
</button>
{/* Milestones Modal Button */}
<button
type="button"
onClick={(e) => handleOpenMilestones(p, e)}
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"
className="inline-flex items-center gap-1 p-1.5 2xl:px-2.5 2xl: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.5 h-3.5 text-blue-400 shrink-0" />
<span className="hidden xl:inline">Milestones</span>
<span className="hidden 2xl:inline">Milestones</span>
</button>
{/* Activities Modal Button */}
<button
type="button"
onClick={(e) => handleOpenActivities(p, e)}
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"
className="inline-flex items-center gap-1 p-1.5 2xl:px-2.5 2xl: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.5 h-3.5 text-cyan-400 shrink-0" />
<span className="hidden xl:inline">Activities</span>
<span className="hidden 2xl:inline">Activities</span>
</button>
{/* Indicators Modal Button */}
<button
type="button"
onClick={(e) => handleOpenIndicators(p, e)}
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"
className="inline-flex items-center gap-1 p-1.5 2xl:px-2.5 2xl: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.5 h-3.5 text-indigo-400 shrink-0" />
<span className="hidden xl:inline">Indicators</span>
<span className="hidden 2xl:inline">Indicators</span>
</button>
{/* Risks Modal Button */}
<button
type="button"
onClick={(e) => handleOpenRisks(p, e)}
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"
className="inline-flex items-center gap-1 p-1.5 2xl:px-2.5 2xl: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.5 h-3.5 text-rose-400 shrink-0" />
<span className="hidden xl:inline">Risks</span>
<span className="hidden 2xl:inline">Risks</span>
</button>
{/* Edit Project Button */}
@@ -1051,24 +1027,7 @@ export const ProjectsPage: React.FC = () => {
</div>
</td>
</tr>
{/* Expanded View Content */}
{isExpanded && (
<tr>
<td colSpan={8} className="p-0 border-b border-blue-500/20">
<ProjectExpandedRow
project={p}
onOpenMilestonesModal={() => handleOpenMilestones(p)}
onOpenActivitiesModal={() => handleOpenActivities(p)}
onOpenIndicatorsModal={() => handleOpenIndicators(p)}
onOpenRisksModal={() => handleOpenRisks(p)}
onOpenEditProject={() => handleOpenEditProject(p)}
/>
</td>
</tr>
)}
</React.Fragment>
);
);
})
)}
</tbody>
@@ -1076,6 +1035,47 @@ export const ProjectsPage: React.FC = () => {
</div>
</div>
{/* Project View Modal (Comprehensive Console, Navigable Gantt Timeline & WBS) */}
<ProjectViewModal
isOpen={isProjectViewModalOpen}
onClose={() => {
setIsProjectViewModalOpen(false);
setViewingProject(null);
}}
project={viewingProject}
programmeMap={programmeMap}
onOpenMilestonesModal={() => {
if (viewingProject) {
setActiveModalProject(viewingProject);
setIsMilestonesModalOpen(true);
}
}}
onOpenActivitiesModal={() => {
if (viewingProject) {
setActiveModalProject(viewingProject);
setIsActivitiesModalOpen(true);
}
}}
onOpenIndicatorsModal={() => {
if (viewingProject) {
setActiveModalProject(viewingProject);
setIsIndicatorsModalOpen(true);
}
}}
onOpenRisksModal={() => {
if (viewingProject) {
setActiveModalProject(viewingProject);
setIsRisksModalOpen(true);
}
}}
onOpenEditProject={() => {
if (viewingProject) {
setEditingProject(viewingProject);
setIsProjectModalOpen(true);
}
}}
/>
{/* Project Form Modal (Create or Edit) */}
<ProjectFormModal
isOpen={isProjectModalOpen}