diff --git a/backend/src/modules/board-planning/board-planning-projection.service.ts b/backend/src/modules/board-planning/board-planning-projection.service.ts index bddba51..521e884 100644 --- a/backend/src/modules/board-planning/board-planning-projection.service.ts +++ b/backend/src/modules/board-planning/board-planning-projection.service.ts @@ -287,7 +287,7 @@ export class BoardPlanningProjectionService { else maturityIndex[key].reserve += maturityTotal; } - // Capital project expenses + // Capital project expenses (from unified projects table) const projectExpenses = await this.tenant.query(` SELECT estimated_cost, target_year, target_month, fund_source FROM projects WHERE is_active = true AND status IN ('planned', 'in_progress') AND target_year IS NOT NULL AND estimated_cost > 0 @@ -303,6 +303,25 @@ export class BoardPlanningProjectionService { else projectIndex[key].reserve += cost; } + // Also include capital_projects table (Capital Planning page) + try { + const capitalProjectExpenses = await this.tenant.query(` + SELECT estimated_cost, target_year, target_month, fund_source + FROM capital_projects WHERE status IN ('planned', 'approved', 'in_progress') AND target_year IS NOT NULL AND estimated_cost > 0 + `); + for (const p of capitalProjectExpenses) { + const yr = parseInt(p.target_year); + const mo = parseInt(p.target_month) || 6; + const key = `${yr}-${mo}`; + if (!projectIndex[key]) projectIndex[key] = { operating: 0, reserve: 0 }; + const cost = parseFloat(p.estimated_cost) || 0; + if (p.fund_source === 'operating') projectIndex[key].operating += cost; + else projectIndex[key].reserve += cost; + } + } catch { + // capital_projects table may not exist in all tenants + } + return { openingBalances: { opCash: parseFloat(openingOp[0]?.total || '0'), @@ -464,15 +483,18 @@ export class BoardPlanningProjectionService { const minLiquidity = Math.min(...allLiquidity); const endLiquidity = allLiquidity[allLiquidity.length - 1]; - // Monthly reserve expense from budgets (approximate average) - let totalResExpense = 0; - let budgetMonths = 0; - for (const key of Object.keys(baseline.budgetsByYearMonth)) { - const b = baseline.budgetsByYearMonth[key]; - if (b.resExpense > 0) { totalResExpense += b.resExpense; budgetMonths++; } + // Reserve coverage: reserve balance / avg monthly reserve expenditure from planned capital projects + let totalReserveProjectCost = 0; + const projectionYears = Math.max(1, Math.ceil(datapoints.length / 12)); + for (const key of Object.keys(baseline.projectIndex)) { + totalReserveProjectCost += baseline.projectIndex[key].reserve || 0; } - const avgMonthlyResExpense = budgetMonths > 0 ? totalResExpense / budgetMonths : 1; - const reserveCoverageMonths = (last.reserve_cash + last.reserve_investments) / Math.max(avgMonthlyResExpense, 1); + const avgMonthlyReserveExpenditure = totalReserveProjectCost > 0 + ? totalReserveProjectCost / (projectionYears * 12) + : 0; + const reserveCoverageMonths = avgMonthlyReserveExpenditure > 0 + ? (last.reserve_cash + last.reserve_investments) / avgMonthlyReserveExpenditure + : 0; // No planned projects = show 0 (N/A) // Estimate total investment income from scenario investments const totalInterestEarned = datapoints.reduce((sum, d, i) => { diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index a17c425..6903d8d 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -67,8 +67,8 @@ const navSections = [ label: 'Board Planning', items: [ { label: 'Budget Planning', icon: IconReportAnalytics, path: '/board-planning/budgets' }, - { label: 'Investment Scenarios', icon: IconScale, path: '/board-planning/investments' }, { label: 'Assessment Scenarios', icon: IconCalculator, path: '/board-planning/assessments' }, + { label: 'Investment Scenarios', icon: IconScale, path: '/board-planning/investments' }, { label: 'Compare Scenarios', icon: IconGitCompare, path: '/board-planning/compare' }, ], }, diff --git a/frontend/src/pages/board-planning/AssessmentScenarioDetailPage.tsx b/frontend/src/pages/board-planning/AssessmentScenarioDetailPage.tsx index 0a08dd7..d7837ca 100644 --- a/frontend/src/pages/board-planning/AssessmentScenarioDetailPage.tsx +++ b/frontend/src/pages/board-planning/AssessmentScenarioDetailPage.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; import { Title, Text, Stack, Group, Button, Table, Badge, Card, ActionIcon, - Loader, Center, Menu, Select, SimpleGrid, + Loader, Center, Select, SimpleGrid, Tooltip, } from '@mantine/core'; import { - IconPlus, IconArrowLeft, IconDots, IconTrash, IconEdit, IconRefresh, + IconPlus, IconArrowLeft, IconTrash, IconEdit, } from '@tabler/icons-react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useParams, useNavigate } from 'react-router-dom'; @@ -92,14 +92,6 @@ export function AssessmentScenarioDetailPage() { }, }); - const refreshProjection = useMutation({ - mutationFn: () => api.post(`/board-planning/scenarios/${id}/projection/refresh`), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['board-planning-projection', id] }); - notifications.show({ message: 'Projection refreshed', color: 'green' }); - }, - }); - if (isLoading) return