diff --git a/PostFundManagement.ConsoleUI/src/components/projects/ProjectExpandedRow.tsx b/PostFundManagement.ConsoleUI/src/components/projects/ProjectExpandedRow.tsx index 196a20b..4ab53a5 100644 --- a/PostFundManagement.ConsoleUI/src/components/projects/ProjectExpandedRow.tsx +++ b/PostFundManagement.ConsoleUI/src/components/projects/ProjectExpandedRow.tsx @@ -19,8 +19,9 @@ import { getMilestones, getProjectIndicators, getProjectRisks, + getBudgets, } from '../../services/api'; -import { Project, ApprovalStatus } from '../../types'; +import { Project, ApprovalStatus, Budget } from '../../types'; import { formatDate, formatZAR, @@ -70,6 +71,21 @@ export const ProjectExpandedRow: React.FC = ({ queryFn: () => getProjectRisks(projectId), }); + const { data: allBudgets = [] } = useQuery({ + queryKey: ['budgets'], + queryFn: () => getBudgets(1, 100), + }); + + const projectBudgets = allBudgets.filter((b) => String(b.projectId) === String(projectId)); + const approvedBudgets = projectBudgets.filter( + (b) => + b.status === undefined || + b.status === null || + Number(b.status) === ApprovalStatus.Approved || + Number(b.status) === 3 + ); + const approvedBudgetTotal = approvedBudgets.reduce((sum, b) => sum + (Number(b.amount) || 0), 0); + const verifiedMilestonesCount = milestones.filter((m) => m.status === ApprovalStatus.Approved).length; const milestoneProgress = milestones.length > 0 ? Math.round((verifiedMilestonesCount / milestones.length) * 100) : 0; const totalActivityBudget = activities.reduce((sum, a) => sum + (Number(a.amount) || 0), 0); @@ -86,6 +102,14 @@ export const ProjectExpandedRow: React.FC = ({ Owner / Created by: {getUserEmail(project.createdBy)} + {approvedBudgetTotal > 0 && ( + <> + • + + Approved Budget: {formatZAR(approvedBudgetTotal)} + + + )}

{project.description}

@@ -114,6 +138,8 @@ export const ProjectExpandedRow: React.FC = ({ project={project} milestones={milestones} activities={activities} + approvedBudgetTotal={approvedBudgetTotal} + approvedBudgetsCount={approvedBudgets.length} onManageMilestones={onOpenMilestonesModal} onManageActivities={onOpenActivitiesModal} /> diff --git a/PostFundManagement.ConsoleUI/src/components/projects/ProjectTimelineGantt.tsx b/PostFundManagement.ConsoleUI/src/components/projects/ProjectTimelineGantt.tsx index a3b18f8..740b8c7 100644 --- a/PostFundManagement.ConsoleUI/src/components/projects/ProjectTimelineGantt.tsx +++ b/PostFundManagement.ConsoleUI/src/components/projects/ProjectTimelineGantt.tsx @@ -17,6 +17,8 @@ interface ProjectTimelineGanttProps { project: Project; milestones: Milestone[]; activities: Activity[]; + approvedBudgetTotal?: number; + approvedBudgetsCount?: number; onManageMilestones: () => void; onManageActivities: () => void; } @@ -25,6 +27,8 @@ export const ProjectTimelineGantt: React.FC = ({ project, milestones, activities, + approvedBudgetTotal = 0, + approvedBudgetsCount = 0, onManageMilestones, onManageActivities, }) => { @@ -362,7 +366,7 @@ export const ProjectTimelineGantt: React.FC = ({ {/* Bottom Summary KPI Bar */} -
+
Project Duration @@ -379,7 +383,7 @@ export const ProjectTimelineGantt: React.FC = ({ {milestoneProgress}% ({verifiedMilestonesCount}/{milestones.length})
-
+
= ({
Statutory Clearance
+ + {/* Total Value of Approved Project Budgets Widget */} +
+ Approved Budget Value + + {formatZAR(approvedBudgetTotal)} + + + {approvedBudgetsCount > 0 + ? `${approvedBudgetsCount} approved budget line${approvedBudgetsCount === 1 ? '' : 's'}` + : 'Allocated Project Budget'} + +
); diff --git a/PostFundManagement.ConsoleUI/src/pages/ProjectsPage.tsx b/PostFundManagement.ConsoleUI/src/pages/ProjectsPage.tsx index 16cab21..c94fff2 100644 --- a/PostFundManagement.ConsoleUI/src/pages/ProjectsPage.tsx +++ b/PostFundManagement.ConsoleUI/src/pages/ProjectsPage.tsx @@ -29,6 +29,7 @@ import { getAllMilestones, getAllRisks, getAllIndicators, + getBudgets, updateProjectStatus, } from '../services/api'; import { HelpTooltip } from '../components/ui/HelpTooltip'; @@ -49,6 +50,7 @@ import { getProjectTypeLabel, getApprovalStatusBadge, formatDate, + formatZAR, INDUSTRY_SECTOR_OPTIONS, PROJECT_TYPE_OPTIONS, APPROVAL_STATUS_OPTIONS, @@ -201,6 +203,11 @@ export const ProjectsPage: React.FC = () => { queryFn: () => getAllIndicators(), }); + const { data: allBudgets = [] } = useQuery({ + queryKey: ['budgets'], + queryFn: () => getBudgets(1, 100), + }); + // Programme lookup map const programmeMap = useMemo(() => { const map = new Map(); @@ -225,6 +232,26 @@ export const ProjectsPage: React.FC = () => { 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]); + // Status Mutation const statusMutation = useMutation({ mutationFn: ({ projectId, status }: { projectId: number | string; status: ApprovalStatus }) => @@ -269,19 +296,21 @@ export const ProjectsPage: React.FC = () => { // Export Columns Configuration const projectExportColumns: ExportColumn[] = useMemo(() => [ - { header: 'Project ID', accessor: (p) => `#${p.id}` }, - { header: 'Project Name', accessor: (p) => p.name || 'Untitled' }, + { header: 'Project ID', key: 'id', format: (val) => `#${val}` }, + { header: 'Project Name', key: 'name', format: (val) => val || 'Untitled' }, { header: 'Parent Programme', - accessor: (p) => programmeMap.get(p.programmeId)?.name || `Prog #${p.programmeId}`, + key: 'programmeId', + format: (val) => programmeMap.get(val)?.name || `Prog #${val}`, }, - { header: 'Industry Sector', accessor: (p) => getIndustrySectorLabel(p.sector) }, - { header: 'Project Type', accessor: (p) => getProjectTypeLabel(p.projectType) }, - { header: 'Start Date', accessor: (p) => formatDate(p.startedAt) }, - { header: 'Target End Date', accessor: (p) => formatDate(p.endedAt) }, + { 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', - accessor: (p) => { + 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'; @@ -289,16 +318,25 @@ export const ProjectsPage: React.FC = () => { }, { header: 'Milestone Progress', - accessor: (p) => { + 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: 'Approval Status', accessor: (p) => getApprovalStatusBadge(p.status).label }, - { header: 'Created By', accessor: (p) => getUserEmail(p.createdBy) }, - ], [programmeMap, milestonesByProject]); + { + 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 = { @@ -439,14 +477,6 @@ export const ProjectsPage: React.FC = () => {
- - )} + {/* Export Button */} + + {/* Single Refresh Button */}