fix: assessment scenarios UX tweaks and projection improvements
- Reorder sidebar: Assessment Scenarios now directly under Budget Planning - Simplify special assessment form: remove Total Amount, keep Per Unit only - Replace Duration field from free-text installments to dropdown (one-time/quarterly/6mo/annual) - Update Change column display to show total per-unit with duration label - Fix Reserve Coverage to use planned capital project costs instead of budget expenses - Include capital_projects table in projection engine alongside projects table - Replace actions dropdown menu with inline Edit/Remove icon buttons - Remove Refresh Projection button (projection auto-refreshes on changes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user