import React, { useState, useMemo, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Link, useSearchParams } from 'react-router-dom'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { FolderKanban, Plus, Search, RotateCcw, ChevronDown, ChevronRight, Flag, Briefcase, Target, ShieldAlert, Edit2, Calendar, Clock, TrendingUp, FileCheck, AlertTriangle, AlertCircle, CheckCircle2, SlidersHorizontal, Eye, RefreshCw, Filter, FileText, Activity, } from 'lucide-react'; import { getProjects, getAllProgrammes, getProgrammeProjects, getAllMilestones, getAllRisks, getAllIndicators, createIndicator, recordIndicatorActual, getBudgets, } from '../services/api'; import { Modal } from '../components/ui/Modal'; import { HelpTooltip } from '../components/ui/HelpTooltip'; import { ExportButton } from '../components/ui/ExportButton'; import { ExportColumn } from '../utils/exportUtils'; import { useToast } from '../components/ui/Toast'; import { useRbac } from '../auth/rbac'; import { useTableSort, SortableTh } from '../utils/tableSort'; import { Project, Programme, Milestone, Indicator, CreateIndicatorRequest, UnitOfMeasure, ApprovalStatus, IndustrySector, ProjectType, } from '../types'; import { getIndustrySectorLabel, getProjectTypeLabel, getApprovalStatusBadge, getUnitOfMeasureLabel, UNIT_OF_MEASURE_OPTIONS, formatDate, formatZAR, INDUSTRY_SECTOR_OPTIONS, PROJECT_TYPE_OPTIONS, APPROVAL_STATUS_OPTIONS, getUserEmail, } from '../lib/formatters'; import { ProjectViewModal } from '../components/projects/ProjectViewModal'; import { ProjectFormModal } from '../components/projects/ProjectFormModal'; import { ProjectMilestonesModal } from '../components/projects/ProjectMilestonesModal'; import { ProjectActivitiesModal } from '../components/projects/ProjectActivitiesModal'; import { ProjectIndicatorsModal } from '../components/projects/ProjectIndicatorsModal'; import { ProjectRisksModal } from '../components/projects/ProjectRisksModal'; const indicatorSchema = z.object({ projectId: z.coerce.number().min(1, 'Select project'), name: z.string().min(5, 'Indicator statement / definition is required'), unitOfMeasure: z.coerce.number().min(1, 'Unit of measure is required'), baselineAmount: z.coerce.number().min(0, 'Baseline must be >= 0'), targetAmount: z.coerce.number().min(1, 'Target benchmark must be > 0'), }); type IndicatorFormValues = z.infer; export interface ProjectScheduleRisk { isAtRisk: boolean; isOverdue: boolean; daysRemaining: number | null; reason: string; badgeClass: string; iconType: 'critical' | 'warning' | 'on_track' | 'pending'; } export const evaluateProjectScheduleRisk = ( project: Project, milestoneStats?: { total: number; verified: number } ): ProjectScheduleRisk => { const hasStarted = Boolean(project.startedAt); const targetDateStr = project.endedAt; if (!targetDateStr) { return { isAtRisk: false, isOverdue: false, daysRemaining: null, reason: 'No contractual target completion date configured yet.', badgeClass: 'bg-slate-800 text-slate-400 border-slate-700/80', iconType: 'pending', }; } const now = Date.now(); const targetTime = new Date(targetDateStr).getTime(); const diffDays = Math.ceil((targetTime - now) / (1000 * 60 * 60 * 24)); const totalMilestones = milestoneStats?.total || 0; const verifiedMilestones = milestoneStats?.verified || 0; const isMilestonesIncomplete = totalMilestones === 0 || verifiedMilestones < totalMilestones; if (hasStarted && isMilestonesIncomplete) { if (diffDays < 0) { return { isAtRisk: true, isOverdue: true, daysRemaining: diffDays, reason: `Target completion deadline passed ${Math.abs(diffDays)} day${ Math.abs(diffDays) === 1 ? '' : 's' } ago with ${totalMilestones - verifiedMilestones} unverified milestone deliverable(s) remaining.`, badgeClass: 'bg-rose-500/15 text-rose-400 border-rose-500/40 font-bold animate-pulse', iconType: 'critical', }; } if (diffDays <= 30) { return { isAtRisk: true, isOverdue: false, daysRemaining: diffDays, reason: `Project is ${diffDays} day${ diffDays === 1 ? '' : 's' } away from target due date with only ${verifiedMilestones} of ${totalMilestones} milestones verified.`, badgeClass: 'bg-amber-500/15 text-amber-300 border-amber-500/40 font-semibold', iconType: 'warning', }; } } if (totalMilestones > 0 && verifiedMilestones === totalMilestones) { return { isAtRisk: false, isOverdue: false, daysRemaining: diffDays, reason: `All ${totalMilestones} contractual milestone deliverables verified and approved.`, badgeClass: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30', iconType: 'on_track', }; } return { isAtRisk: false, isOverdue: false, daysRemaining: diffDays, reason: `${diffDays} days remaining until target completion deadline.`, badgeClass: 'bg-slate-800/80 text-slate-300 border-slate-700/80', iconType: 'on_track', }; }; export const ProjectsPage: React.FC = () => { const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); const { canAdd, canApprove, canExport, canApproveRecord, isMaker } = useRbac(); // Filter & Search States const [searchTerm, setSearchTerm] = useState(''); const [selectedProgrammeFilter, setSelectedProgrammeFilter] = useState('all'); const [selectedSectorFilter, setSelectedSectorFilter] = useState('all'); const [selectedStatusFilter, setSelectedStatusFilter] = useState('all'); // Active Project View Modal State const [viewingProject, setViewingProject] = useState(null); const [isProjectViewModalOpen, setIsProjectViewModalOpen] = useState(false); // Modals Management State const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); const [editingProject, setEditingProject] = useState(null); const [activeModalProject, setActiveModalProject] = useState(null); const [isMilestonesModalOpen, setIsMilestonesModalOpen] = useState(false); const [isActivitiesModalOpen, setIsActivitiesModalOpen] = useState(false); const [isIndicatorsModalOpen, setIsIndicatorsModalOpen] = useState(false); const [isRisksModalOpen, setIsRisksModalOpen] = useState(false); // Queries const { data: programmes = [] } = useQuery({ queryKey: ['programmes'], queryFn: () => getAllProgrammes(), }); const { data: projects = [], isLoading: loadingProjects, refetch: refetchProjects, isFetching: isFetchingProjects, } = useQuery({ queryKey: ['projects', selectedProgrammeFilter], queryFn: () => selectedProgrammeFilter !== 'all' ? getProgrammeProjects(selectedProgrammeFilter) : getProjects(), }); const { data: allMilestones = [] } = useQuery({ queryKey: ['milestones-all'], queryFn: () => getAllMilestones(), }); const { data: allRisks = [] } = useQuery({ queryKey: ['risks-all'], queryFn: () => getAllRisks(), }); const { data: allIndicators = [] } = useQuery({ queryKey: ['indicators-all'], queryFn: () => getAllIndicators(), }); const { data: allBudgets = [] } = useQuery({ queryKey: ['budgets'], queryFn: () => getBudgets(1, 100), }); // Programme lookup map const programmeMap = useMemo(() => { const map = new Map(); programmes.forEach((p) => map.set(p.id, p)); return map; }, [programmes]); // Project Milestones Count Map const milestonesByProject = useMemo(() => { const map = new Map(); allMilestones.forEach((m) => { const pId = m.projectId; if (pId !== undefined && pId !== null) { const current = map.get(pId) || { total: 0, verified: 0 }; current.total += 1; if (m.status === ApprovalStatus.Approved) { current.verified += 1; } map.set(pId, current); } }); return map; }, [allMilestones]); // Project Approved Budget Map const approvedBudgetsByProject = useMemo(() => { const map = new Map(); allBudgets.forEach((b) => { const pId = b.projectId; if (pId !== undefined && pId !== null) { if ( b.status === undefined || b.status === null || Number(b.status) === ApprovalStatus.Approved || Number(b.status) === 3 ) { const current = map.get(pId) || 0; map.set(pId, current + (Number(b.amount) || 0)); } } }); return map; }, [allBudgets]); // Filtered Projects const filteredProjects = useMemo(() => { return projects.filter((p) => { // Search term if (searchTerm.trim()) { const query = searchTerm.toLowerCase(); const matchesName = (p.name || '').toLowerCase().includes(query); const matchesDesc = (p.description || '').toLowerCase().includes(query); const matchesId = String(p.id).toLowerCase().includes(query); const matchesSector = getIndustrySectorLabel(p.sector).toLowerCase().includes(query); if (!matchesName && !matchesDesc && !matchesId && !matchesSector) { return false; } } // Sector filter if (selectedSectorFilter !== 'all' && Number(p.sector) !== Number(selectedSectorFilter)) { return false; } // Status filter if (selectedStatusFilter !== 'all' && Number(p.status) !== Number(selectedStatusFilter)) { return false; } return true; }); }, [projects, searchTerm, selectedSectorFilter, selectedStatusFilter]); // Export Columns Configuration const projectExportColumns: ExportColumn[] = useMemo(() => [ { header: 'Project ID', key: 'id', format: (val) => `#${val}` }, { header: 'Project Name', key: 'name', format: (val) => val || 'Untitled' }, { header: 'Parent Programme', key: 'programmeId', format: (val) => programmeMap.get(val)?.name || `Prog #${val}`, }, { header: 'Industry Sector', key: 'sector', format: (val) => getIndustrySectorLabel(val) }, { header: 'Project Type', key: 'projectType', format: (val) => getProjectTypeLabel(val) }, { header: 'Start Date', key: 'startedAt', format: (val) => formatDate(val) }, { header: 'Target End Date', key: 'endedAt', format: (val) => formatDate(val) }, { header: 'Schedule Risk Alert', key: 'id', format: (_, p) => { const stats = milestonesByProject.get(p.id); const risk = evaluateProjectScheduleRisk(p, stats); return risk.isAtRisk ? (risk.isOverdue ? `OVERDUE: ${risk.reason}` : `AT RISK: ${risk.reason}`) : 'ON TRACK'; }, }, { header: 'Milestone Progress', key: 'id', format: (_, p) => { const stats = milestonesByProject.get(p.id); if (!stats || stats.total === 0) return '0 / 0 (0%)'; const pct = Math.round((stats.verified / stats.total) * 100); return `${stats.verified} / ${stats.total} (${pct}%)`; }, }, { header: 'Approved Budget (ZAR)', key: 'id', format: (_, p) => { const amt = approvedBudgetsByProject.get(p.id) || 0; return amt > 0 ? formatZAR(amt) : 'R 0'; }, }, { header: 'Approval Status', key: 'status', format: (val) => getApprovalStatusBadge(val).label }, { header: 'Created By', key: 'createdBy', format: (val) => getUserEmail(val) }, ], [programmeMap, milestonesByProject, approvedBudgetsByProject]); // Table Sorting const customExtractors = { name: (p: Project) => (p.name || '').toLowerCase(), programme: (p: Project) => (programmeMap.get(p.programmeId)?.name || '').toLowerCase(), status: (p: Project) => Number(p.status) || 0, progress: (p: Project) => { const stats = milestonesByProject.get(p.id); return stats && stats.total > 0 ? (stats.verified / stats.total) * 100 : 0; }, duration: (p: Project) => { if (!p.startedAt || !p.endedAt) return 0; return new Date(p.endedAt).getTime() - new Date(p.startedAt).getTime(); }, risk: (p: Project) => { const stats = milestonesByProject.get(p.id); const risk = evaluateProjectScheduleRisk(p, stats); if (risk.isOverdue) return 1; if (risk.isAtRisk) return 2; return 3; }, }; const { sortedData: sortedProjects, sortColumn, sortDirection, handleSort } = useTableSort( filteredProjects, 'risk', 'asc', customExtractors ); // 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 const handleOpenMilestones = (p: Project, e?: React.MouseEvent) => { if (e) e.stopPropagation(); setActiveModalProject(p); setIsMilestonesModalOpen(true); }; const handleOpenActivities = (p: Project, e?: React.MouseEvent) => { if (e) e.stopPropagation(); setActiveModalProject(p); setIsActivitiesModalOpen(true); }; const handleOpenIndicators = (p: Project, e?: React.MouseEvent) => { if (e) e.stopPropagation(); setActiveModalProject(p); setIsIndicatorsModalOpen(true); }; const handleOpenRisks = (p: Project, e?: React.MouseEvent) => { if (e) e.stopPropagation(); setActiveModalProject(p); setIsRisksModalOpen(true); }; const handleOpenEditProject = (p: Project, e?: React.MouseEvent) => { if (e) e.stopPropagation(); setEditingProject(p); setIsProjectModalOpen(true); }; const handleOpenCreateProject = () => { setEditingProject(null); setIsProjectModalOpen(true); }; const resetFilters = () => { setSearchTerm(''); setSelectedProgrammeFilter('all'); setSelectedSectorFilter('all'); setSelectedStatusFilter('all'); }; // Key KPI Aggregations const totalApprovedProjects = projects.filter((p) => p.status === ApprovalStatus.Approved).length; const verifiedMilestonesTotal = allMilestones.filter((m) => m.status === ApprovalStatus.Approved).length; const milestoneCompletionRate = allMilestones.length > 0 ? Math.round((verifiedMilestonesTotal / allMilestones.length) * 100) : 0; const atRiskProjectsCount = useMemo(() => { return projects.filter((p) => { const stats = milestonesByProject.get(p.id); return evaluateProjectScheduleRisk(p, stats).isAtRisk; }).length; }, [projects, milestonesByProject]); const [searchParams, setSearchParams] = useSearchParams(); const tabParam = searchParams.get('tab'); const [activeTab, setActiveTab] = useState<'projects' | 'indicators' | 'actuals'>( tabParam === 'indicators' || tabParam === 'actuals' ? tabParam : 'projects' ); useEffect(() => { const t = searchParams.get('tab'); if (t === 'projects' || t === 'indicators' || t === 'actuals') { setActiveTab(t); } }, [searchParams]); const handleTabChange = (tab: 'projects' | 'indicators' | 'actuals') => { setActiveTab(tab); setSearchParams((prev) => { const next = new URLSearchParams(prev); next.set('tab', tab); return next; }); }; // Indicators & Actuals Modals & States const [isCreateIndicatorModalOpen, setIsCreateIndicatorModalOpen] = useState(false); const [selectedIndicatorForActual, setSelectedIndicatorForActual] = useState(null); const [actualInput, setActualInput] = useState(0); // Indicators Tab Filter States const [indicatorSearchTerm, setIndicatorSearchTerm] = useState(''); const [indicatorProjectFilter, setIndicatorProjectFilter] = useState('all'); const [indicatorUnitFilter, setIndicatorUnitFilter] = useState('all'); // Actuals Tab Filter States const [actualSearchTerm, setActualSearchTerm] = useState(''); const [actualProjectFilter, setActualProjectFilter] = useState('all'); const [actualStatusFilter, setActualStatusFilter] = useState('all'); // Indicator Status Thresholds (BR-008 & BR-009: >=90% On Track, 70-89% At Risk, <70% Off Track) const getIndicatorStatus = (actual: number, target: number) => { if (!target) return { label: 'Pending Target', badge: 'bg-slate-800/60 text-slate-400 border-slate-700/60' }; const pct = (actual / target) * 100; if (pct >= 90) return { label: '≥90% (On Track)', badge: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20' }; if (pct >= 70) return { label: '70–89% (At Risk)', badge: 'bg-amber-500/10 text-amber-400 border-amber-500/20' }; return { label: '<70% (Underperforming)', badge: 'bg-rose-500/10 text-rose-400 border-rose-500/20' }; }; const indicatorForm = useForm({ resolver: zodResolver(indicatorSchema), defaultValues: { projectId: projects[0]?.id ? Number(projects[0].id) : 401, name: '', unitOfMeasure: UnitOfMeasure.Count, baselineAmount: 0, targetAmount: 100, }, }); const indicatorMutation = useMutation({ mutationFn: (data: CreateIndicatorRequest) => createIndicator(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['indicators-all'] }); queryClient.invalidateQueries({ queryKey: ['indicators'] }); success('Indicator Defined', 'Performance indicator added to project monitoring plan (M08)'); setIsCreateIndicatorModalOpen(false); indicatorForm.reset(); }, onError: (err: unknown) => { toastError('Failed to create indicator', err); }, }); const actualMutation = useMutation({ mutationFn: ({ id, amount }: { id: string | number; amount: number }) => recordIndicatorActual(id, amount), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['indicators-all'] }); queryClient.invalidateQueries({ queryKey: ['indicators'] }); success('Actual Value Recorded', 'M&E variance and indicator status recalculated (BR-009)'); setSelectedIndicatorForActual(null); }, onError: (err: unknown) => { toastError('Failed to record actual', err); }, }); // Filtered Indicators const filteredIndicatorsList = useMemo(() => { return allIndicators.filter((ind) => { if (indicatorProjectFilter !== 'all' && String(ind.projectId) !== String(indicatorProjectFilter)) { return false; } if (indicatorUnitFilter !== 'all' && String(ind.unitOfMeasure) !== String(indicatorUnitFilter)) { return false; } if (!indicatorSearchTerm.trim()) return true; const q = indicatorSearchTerm.toLowerCase(); const name = (ind.name || '').toLowerCase(); const projId = String(ind.projectId || '').toLowerCase(); const unit = getUnitOfMeasureLabel(ind.unitOfMeasure).toLowerCase(); return name.includes(q) || projId.includes(q) || unit.includes(q); }); }, [allIndicators, indicatorProjectFilter, indicatorUnitFilter, indicatorSearchTerm]); // Filtered Actuals const filteredActualsList = useMemo(() => { return allIndicators.filter((ind) => { if (actualProjectFilter !== 'all' && String(ind.projectId) !== String(actualProjectFilter)) { return false; } if (actualStatusFilter !== 'all') { const statusObj = getIndicatorStatus(ind.actualAmount, ind.targetAmount); if (actualStatusFilter === 'green' && !statusObj.label.startsWith('≥90%')) return false; if (actualStatusFilter === 'amber' && !statusObj.label.startsWith('70–89%')) return false; if (actualStatusFilter === 'red' && !statusObj.label.startsWith('<70%')) return false; if (actualStatusFilter === 'pending' && statusObj.label !== 'Pending Target') return false; } if (!actualSearchTerm.trim()) return true; const q = actualSearchTerm.toLowerCase(); const name = (ind.name || '').toLowerCase(); const projId = String(ind.projectId || '').toLowerCase(); const unit = getUnitOfMeasureLabel(ind.unitOfMeasure).toLowerCase(); return name.includes(q) || projId.includes(q) || unit.includes(q); }); }, [allIndicators, actualProjectFilter, actualStatusFilter, actualSearchTerm]); // Exports const indicatorExportColumns: ExportColumn[] = [ { header: 'Indicator ID', key: 'id' }, { header: 'Indicator Statement', key: 'name' }, { header: 'Project ID', key: 'projectId', format: (val) => `Project #${val}` }, { header: 'Unit of Measure', key: 'unitOfMeasure', format: (val) => getUnitOfMeasureLabel(val) }, { header: 'Baseline Value', key: 'baselineAmount' }, { header: 'Target Value', key: 'targetAmount' }, ]; const actualExportColumns: ExportColumn[] = [ { header: 'Indicator Statement', key: 'name' }, { header: 'Project ID', key: 'projectId', format: (val) => `Project #${val}` }, { header: 'Unit of Measure', key: 'unitOfMeasure', format: (val) => getUnitOfMeasureLabel(val) }, { header: 'Baseline', key: 'baselineAmount' }, { header: 'Target Benchmark', key: 'targetAmount' }, { header: 'Actual Recorded', key: 'actualAmount' }, { header: 'Variance', key: 'actualAmount', format: (val, item) => `${Number(item.targetAmount) - Number(val)}`, }, { header: 'Achievement %', key: 'actualAmount', format: (val, item) => (item.targetAmount ? `${Math.round((Number(val) / Number(item.targetAmount)) * 100)}%` : '—'), }, { header: 'Status', key: 'actualAmount', format: (val, item) => getIndicatorStatus(Number(val), Number(item.targetAmount)).label, }, ]; // Table Sort Hooks const { sortedData: sortedIndicatorsData, sortColumn: indSortCol, sortDirection: indSortDir, handleSort: handleIndSort, } = useTableSort(filteredIndicatorsList, null, null, { name: (ind) => ind.name, projectId: (ind) => ind.projectId, unitOfMeasure: (ind) => getUnitOfMeasureLabel(ind.unitOfMeasure), baselineAmount: (ind) => ind.baselineAmount, targetAmount: (ind) => ind.targetAmount, }); const { sortedData: sortedActualsData, sortColumn: actSortCol, sortDirection: actSortDir, handleSort: handleActSort, } = useTableSort(filteredActualsList, null, null, { name: (ind) => ind.name, projectId: (ind) => ind.projectId, unitOfMeasure: (ind) => getUnitOfMeasureLabel(ind.unitOfMeasure), baselineAmount: (ind) => ind.baselineAmount, targetAmount: (ind) => ind.targetAmount, actualAmount: (ind) => ind.actualAmount, status: (ind) => getIndicatorStatus(ind.actualAmount, ind.targetAmount).label, }); return (
{/* Header Banner */}

Project & Milestone Management

Interactive WBS Console

Manage operational activities, verified contractual milestone deliverables, risk matrices, and M&E performance indicator targets.

{activeTab === 'projects' && canAdd('projects_milestones') && ( )} {activeTab === 'indicators' && canAdd('projects_milestones') && ( )}
{/* 3 Navigation Tabs: Projects, Indicators, Actuals */}
{activeTab === 'projects' && (
{/* KPI Overview Summary Cards */}
Total Projects
{projects.length} ({totalApprovedProjects} Approved)

Under {programmes.length} parent programmes

Milestone Progress
{milestoneCompletionRate}% ({verifiedMilestonesTotal}/{allMilestones.length})
Schedule Risk Alerts 0 ? 'text-amber-400 animate-pulse' : 'text-slate-500'}`} />
0 ? 'text-amber-400' : 'text-slate-300'}`}> {atRiskProjectsCount} Imminent / Overdue

Due with unverified deliverables

M&E Indicators
{allIndicators.length} Tracked KPIs

Results & impact metrics

{/* Filter and Search Controls Bar */}
{/* Search Input */}
setSearchTerm(e.target.value)} placeholder="Search projects by name, ID, sector, or keywords..." className="w-full pl-9 pr-3 py-2 bg-slate-900/90 border border-slate-700/80 rounded-xl text-xs text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40" />
{/* Quick Filters */}
{/* Programme Filter */}
Programme:
{/* Sector Filter */}
Sector:
{/* Status Filter */}
Status:
{/* Clear Filter Button */} {(searchTerm || selectedProgrammeFilter !== 'all' || selectedSectorFilter !== 'all' || selectedStatusFilter !== 'all') && ( )} {/* Export Button */} {/* Single Refresh Button */}
{/* Active Filter Tags */} {(searchTerm || selectedProgrammeFilter !== 'all' || selectedSectorFilter !== 'all' || selectedStatusFilter !== 'all') && (
Active Filters: {searchTerm && ( Search: "{searchTerm}" )} {selectedProgrammeFilter !== 'all' && ( Prog: {programmeMap.get(selectedProgrammeFilter)?.name || `#${selectedProgrammeFilter}`} )} {selectedSectorFilter !== 'all' && ( Sector: {getIndustrySectorLabel(Number(selectedSectorFilter))} )} {selectedStatusFilter !== 'all' && ( Status: {getApprovalStatusBadge(Number(selectedStatusFilter)).label} )}
)}
{/* Main Projects Table Container */}
{/* Table Header Bar */}

Funded Projects Directory ({sortedProjects.length})

• Click "View Project" or project row to open the Project View modal & Gantt timeline
{/* Projects Table */}
Project Name & ID Parent Programme Schedule (Timeline) WBS Progress
Schedule Risk
{loadingProjects ? ( ) : sortedProjects.length === 0 ? ( ) : ( sortedProjects.map((p) => { const prog = programmeMap.get(p.programmeId); const milestoneStats = milestonesByProject.get(p.id) || { total: 0, verified: 0 }; const progressPct = milestoneStats.total > 0 ? Math.round((milestoneStats.verified / milestoneStats.total) * 100) : 0; const scheduleRisk = evaluateProjectScheduleRisk(p, milestoneStats); return ( handleOpenProjectView(p)} className="transition-colors cursor-pointer hover:bg-slate-900/60 text-slate-300 group" > {/* Project Name & Meta */} {/* Parent Programme */} {/* Schedule (Dates) */} {/* Milestone Progress */} {/* Schedule Risk Column with Icon & Tooltip */} {/* Row Action: View Project (Opens Large WBS & Gantt Console) */} ); }) )}
Actions
Loading project directory...

No matching projects found

{searchTerm || selectedProgrammeFilter !== 'all' || selectedSectorFilter !== 'all' ? 'Try adjusting your search query or filters.' : 'Register a project to begin building your work breakdown structure.'}

{p.name || `Project #${p.id}`}
#{p.id} {getIndustrySectorLabel(p.sector)} • {getProjectTypeLabel(p.projectType)}
{prog?.name || `Prog #${p.programmeId}`} Programme #{p.programmeId}
{formatDate(p.startedAt)} → {formatDate(p.endedAt)}
{p.startedAt && p.endedAt && ( {Math.max( 1, Math.round( (new Date(p.endedAt).getTime() - new Date(p.startedAt).getTime()) / (1000 * 60 * 60 * 24 * 30) ) )}{' '} Months Estimated Duration )}
= 100 ? 'text-emerald-400' : 'text-blue-400'}> {progressPct}% ({milestoneStats.verified}/{milestoneStats.total})
= 100 ? 'bg-emerald-500' : 'bg-gradient-to-r from-blue-500 to-indigo-500' }`} style={{ width: `${progressPct}%` }} />
e.stopPropagation()}>
{scheduleRisk.isOverdue ? ( Overdue ) : scheduleRisk.isAtRisk ? ( At Risk ({scheduleRisk.daysRemaining}d) ) : scheduleRisk.iconType === 'on_track' ? ( On Track ) : ( Scheduled )}
e.stopPropagation()}>
)} {/* TAB 2: Performance Indicators (M08) */} {activeTab === 'indicators' && (
{/* Indicators KPI Summary Cards */}
Total Indicators
{allIndicators.length} Defined metrics

Across all active projects

Targets Set
{allIndicators.filter((i) => Number(i.targetAmount) > 0).length} Contracted milestones

M&E logframe benchmarks

Baselines Recorded
{allIndicators.filter((i) => Number(i.baselineAmount) >= 0).length} Pre-grant baselines

Groundwork observations

Measurement Units
{new Set(allIndicators.map((i) => i.unitOfMeasure)).size} Active dimensions

Standardized measurement scales

{/* Indicators Toolbar */}
setIndicatorSearchTerm(e.target.value)} className="w-full pl-9 pr-4 py-2 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 text-slate-200 placeholder:text-slate-500 transition shadow-xs" />
{(indicatorSearchTerm || indicatorProjectFilter !== 'all' || indicatorUnitFilter !== 'all') && ( )}
{/* Indicators Table Card */}

Performance Indicators & Target Benchmarks

Showing {sortedIndicatorsData.length} of {allIndicators.length} indicators
Indicator Statement Project Unit of Measure Baseline Target {sortedIndicatorsData.length === 0 ? ( ) : ( sortedIndicatorsData.map((ind) => ( )) )}
Action
{indicatorSearchTerm || indicatorProjectFilter !== 'all' || indicatorUnitFilter !== 'all' ? 'No indicators match your filter criteria.' : 'No performance indicators defined yet. Click "Define Indicator" above to begin.'}
{ind.name} Project #{ind.projectId} {getUnitOfMeasureLabel(ind.unitOfMeasure)} {ind.baselineAmount} {ind.targetAmount}
)} {/* TAB 3: Realized Actuals & Variance Ledger (M08 / BR-009) */} {activeTab === 'actuals' && (
{/* Actuals RAG Summary Cards */}
On Track (≥90%)
{allIndicators.filter((i) => i.targetAmount > 0 && (i.actualAmount / i.targetAmount) >= 0.9).length} Satisfactory progress

≥90% of target benchmark achieved

At Risk (70–89%)
{allIndicators.filter((i) => { if (!i.targetAmount) return false; const r = i.actualAmount / i.targetAmount; return r >= 0.7 && r < 0.9; }).length} Requires monitoring

Moderately behind milestones

Underperforming (<70%)
{allIndicators.filter((i) => i.targetAmount > 0 && i.actualAmount > 0 && (i.actualAmount / i.targetAmount) < 0.7).length} Corrective action

<70% of target achieved

Pending Actuals
{allIndicators.filter((i) => !i.actualAmount || Number(i.actualAmount) === 0).length} Unrecorded

Awaiting field monitoring returns

{/* Actuals Toolbar */}
setActualSearchTerm(e.target.value)} className="w-full pl-9 pr-4 py-2 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 text-slate-200 placeholder:text-slate-500 transition shadow-xs" />
{(actualSearchTerm || actualProjectFilter !== 'all' || actualStatusFilter !== 'all') && ( )}
{/* Actuals Table Card */}

Realized Actuals & Variance Ledger

Showing {sortedActualsData.length} of {allIndicators.length} returns
Indicator Statement Project Unit Baseline Target Actual RAG Status {sortedActualsData.length === 0 ? ( ) : ( sortedActualsData.map((ind) => { const statusObj = getIndicatorStatus(ind.actualAmount, ind.targetAmount); const achievementPct = ind.targetAmount ? Math.round((Number(ind.actualAmount) / Number(ind.targetAmount)) * 100) : 0; return ( ); }) )}
Achievement % Action
{actualSearchTerm || actualProjectFilter !== 'all' || actualStatusFilter !== 'all' ? 'No indicator returns match your filter criteria.' : 'No indicator returns recorded yet. Click "Record Actual" to log field returns.'}
{ind.name} Project #{ind.projectId} {getUnitOfMeasureLabel(ind.unitOfMeasure)} {ind.baselineAmount} {ind.targetAmount} {ind.actualAmount} = 90 ? 'text-emerald-400 font-semibold' : achievementPct >= 70 ? 'text-amber-400 font-semibold' : 'text-rose-400 font-semibold'}> {ind.targetAmount ? `${achievementPct}%` : '—'} {statusObj.label}
)} {/* Project View Modal (Comprehensive Console, Navigable Gantt Timeline & WBS) */} { 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) */} { setIsProjectModalOpen(false); setEditingProject(null); }} editingProject={editingProject} programmes={programmes} defaultProgrammeId={selectedProgrammeFilter !== 'all' ? selectedProgrammeFilter : undefined} /> {/* Milestones Management Modal */} { setIsMilestonesModalOpen(false); setActiveModalProject(null); }} project={activeModalProject} /> {/* Activities Management Modal */} { setIsActivitiesModalOpen(false); setActiveModalProject(null); }} project={activeModalProject} /> {/* Indicators Management Modal */} { setIsIndicatorsModalOpen(false); setActiveModalProject(null); }} project={activeModalProject} /> {/* Risks Management Modal */} { setIsRisksModalOpen(false); setActiveModalProject(null); }} project={activeModalProject} /> {/* Modal: Define M&E Performance Indicator */} setIsCreateIndicatorModalOpen(false)} title="Define M&E Performance Indicator" subtitle="Establish quantitative output, outcome, or impact metric for project logframe (M08)" helpText="Every performance indicator strictly enforces: Baseline -> Target -> Actual -> Variance -> Evidence (BR-009)." >
indicatorMutation.mutate(data as CreateIndicatorRequest))} className="space-y-4">
{/* Modal: Record Actual Amount */} setSelectedIndicatorForActual(null)} title="Record Indicator Actual Value" subtitle={selectedIndicatorForActual?.name || ''} helpText="Logs a certified field or audited measurement against the target, recalculating the achievement percentage and RAG rating (BR-009)." >
Baseline: {selectedIndicatorForActual?.baselineAmount} Target Benchmark: {selectedIndicatorForActual?.targetAmount}
setActualInput(Number(e.target.value))} className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-mono transition" />
); };