Compare commits
8 Commits
claude/ecs
...
8e2456dcae
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e2456dcae | |||
| 1acd8c3bff | |||
| 2de0cde94c | |||
| 94c7c90b91 | |||
| f47fbfcf93 | |||
| 04771f370c | |||
| 208c1dd7bc | |||
| a047144922 |
@@ -220,12 +220,12 @@ export class HealthScoresService {
|
|||||||
missing.push(`No budget found for ${year}. Upload or create an annual budget.`);
|
missing.push(`No budget found for ${year}. Upload or create an annual budget.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should have capital projects (warn but don't block)
|
// Should have reserve-funded projects with estimated costs (warn but don't block)
|
||||||
const projects = await qr.query(
|
const projects = await qr.query(
|
||||||
`SELECT COUNT(*) as cnt FROM projects WHERE is_active = true`,
|
`SELECT COUNT(*) as cnt FROM projects WHERE is_active = true AND fund_source = 'reserve'`,
|
||||||
);
|
);
|
||||||
if (parseInt(projects[0].cnt) === 0) {
|
if (parseInt(projects[0].cnt) === 0) {
|
||||||
missing.push('No capital projects found. Add planned capital projects for a more accurate reserve health assessment.');
|
missing.push('No reserve-funded projects found. Add projects with estimated costs for an accurate funded-ratio calculation.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,10 +558,12 @@ export class HealthScoresService {
|
|||||||
FROM reserve_components
|
FROM reserve_components
|
||||||
ORDER BY remaining_life_years ASC NULLS LAST
|
ORDER BY remaining_life_years ASC NULLS LAST
|
||||||
`),
|
`),
|
||||||
// Capital projects
|
// Capital projects (include component-level fields for funded ratio when reserve_components is empty)
|
||||||
qr.query(`
|
qr.query(`
|
||||||
SELECT name, estimated_cost, target_year, target_month, fund_source,
|
SELECT name, estimated_cost, actual_cost, target_year, target_month, fund_source,
|
||||||
status, priority, current_fund_balance, funded_percentage
|
status, priority, current_fund_balance, funded_percentage,
|
||||||
|
category, useful_life_years, remaining_life_years, condition_rating,
|
||||||
|
annual_contribution
|
||||||
FROM projects
|
FROM projects
|
||||||
WHERE is_active = true AND status IN ('planned', 'approved', 'in_progress')
|
WHERE is_active = true AND status IN ('planned', 'approved', 'in_progress')
|
||||||
ORDER BY target_year, target_month NULLS LAST
|
ORDER BY target_year, target_month NULLS LAST
|
||||||
@@ -596,11 +598,19 @@ export class HealthScoresService {
|
|||||||
|
|
||||||
const totalReserveFund = reserveCash + totalInvestments;
|
const totalReserveFund = reserveCash + totalInvestments;
|
||||||
|
|
||||||
const totalReplacementCost = reserveComponents
|
// Use reserve_components for funded ratio when available; fall back to
|
||||||
.reduce((s: number, c: any) => s + parseFloat(c.replacement_cost || '0'), 0);
|
// reserve-funded projects (which carry the same estimated_cost / lifecycle
|
||||||
|
// fields that users actually populate on the Projects page).
|
||||||
|
const reserveProjects = projects.filter((p: any) => p.fund_source === 'reserve');
|
||||||
|
const useComponentsTable = reserveComponents.length > 0;
|
||||||
|
|
||||||
const totalComponentFunded = reserveComponents
|
const totalReplacementCost = useComponentsTable
|
||||||
.reduce((s: number, c: any) => s + parseFloat(c.current_fund_balance || '0'), 0);
|
? reserveComponents.reduce((s: number, c: any) => s + parseFloat(c.replacement_cost || '0'), 0)
|
||||||
|
: reserveProjects.reduce((s: number, p: any) => s + parseFloat(p.estimated_cost || '0'), 0);
|
||||||
|
|
||||||
|
const totalComponentFunded = useComponentsTable
|
||||||
|
? reserveComponents.reduce((s: number, c: any) => s + parseFloat(c.current_fund_balance || '0'), 0)
|
||||||
|
: reserveProjects.reduce((s: number, p: any) => s + parseFloat(p.current_fund_balance || '0'), 0);
|
||||||
|
|
||||||
const percentFunded = totalReplacementCost > 0 ? (totalReserveFund / totalReplacementCost) * 100 : 0;
|
const percentFunded = totalReplacementCost > 0 ? (totalReserveFund / totalReplacementCost) * 100 : 0;
|
||||||
|
|
||||||
@@ -615,9 +625,13 @@ export class HealthScoresService {
|
|||||||
.filter((b: any) => b.account_type === 'expense')
|
.filter((b: any) => b.account_type === 'expense')
|
||||||
.reduce((s: number, b: any) => s + parseFloat(b.annual_total || '0'), 0);
|
.reduce((s: number, b: any) => s + parseFloat(b.annual_total || '0'), 0);
|
||||||
|
|
||||||
// Components needing replacement within 5 years
|
// Components needing replacement within 5 years — use whichever source has data
|
||||||
const urgentComponents = reserveComponents.filter(
|
const urgentComponents = useComponentsTable
|
||||||
|
? reserveComponents.filter(
|
||||||
(c: any) => c.remaining_life_years !== null && parseFloat(c.remaining_life_years) <= 5,
|
(c: any) => c.remaining_life_years !== null && parseFloat(c.remaining_life_years) <= 5,
|
||||||
|
)
|
||||||
|
: reserveProjects.filter(
|
||||||
|
(p: any) => p.remaining_life_years !== null && parseFloat(p.remaining_life_years) <= 5,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Build 12-month forward reserve cash flow projection ──
|
// ── Build 12-month forward reserve cash flow projection ──
|
||||||
@@ -749,6 +763,7 @@ export class HealthScoresService {
|
|||||||
accounts,
|
accounts,
|
||||||
investments,
|
investments,
|
||||||
reserveComponents,
|
reserveComponents,
|
||||||
|
reserveProjects,
|
||||||
projects,
|
projects,
|
||||||
budgets,
|
budgets,
|
||||||
assessments,
|
assessments,
|
||||||
@@ -959,13 +974,15 @@ Provide 3-5 factors and 1-3 actionable recommendations. Be specific with dollar
|
|||||||
`- ${i.name} | ${i.investment_type} @ ${i.institution} | $${parseFloat(i.current_value || i.principal || '0').toFixed(2)} | Rate: ${parseFloat(i.interest_rate || '0').toFixed(2)}% | Maturity: ${i.maturity_date ? new Date(i.maturity_date).toLocaleDateString() : 'N/A'}`,
|
`- ${i.name} | ${i.investment_type} @ ${i.institution} | $${parseFloat(i.current_value || i.principal || '0').toFixed(2)} | Rate: ${parseFloat(i.interest_rate || '0').toFixed(2)}% | Maturity: ${i.maturity_date ? new Date(i.maturity_date).toLocaleDateString() : 'N/A'}`,
|
||||||
).join('\n');
|
).join('\n');
|
||||||
|
|
||||||
const componentLines = data.reserveComponents.length === 0
|
// Build component lines from reserve_components if available, otherwise from reserve-funded projects
|
||||||
? 'No reserve components tracked.'
|
const componentSource = data.reserveComponents.length > 0 ? data.reserveComponents : data.reserveProjects;
|
||||||
: data.reserveComponents.map((c: any) => {
|
const componentLines = componentSource.length === 0
|
||||||
const cost = parseFloat(c.replacement_cost || '0');
|
? 'No reserve components or reserve projects tracked.'
|
||||||
|
: componentSource.map((c: any) => {
|
||||||
|
const cost = parseFloat(c.replacement_cost || c.estimated_cost || '0');
|
||||||
const funded = parseFloat(c.current_fund_balance || '0');
|
const funded = parseFloat(c.current_fund_balance || '0');
|
||||||
const pct = cost > 0 ? ((funded / cost) * 100).toFixed(0) : '0';
|
const pct = cost > 0 ? ((funded / cost) * 100).toFixed(0) : '0';
|
||||||
return `- ${c.name} [${c.category}] | Life: ${c.useful_life_years}yr, Remaining: ${c.remaining_life_years}yr | Cost: $${cost.toFixed(0)} | Funded: $${funded.toFixed(0)} (${pct}%) | Condition: ${c.condition_rating}/10 | Annual Contribution: $${parseFloat(c.annual_contribution || '0').toFixed(0)}`;
|
return `- ${c.name} [${c.category || 'N/A'}] | Life: ${c.useful_life_years || '?'}yr, Remaining: ${c.remaining_life_years || '?'}yr | Cost: $${cost.toFixed(0)} | Funded: $${funded.toFixed(0)} (${pct}%) | Condition: ${c.condition_rating || '?'}/10 | Annual Contribution: $${parseFloat(c.annual_contribution || '0').toFixed(0)}`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
|
||||||
const projectLines = data.projects.length === 0
|
const projectLines = data.projects.length === 0
|
||||||
@@ -981,7 +998,7 @@ Provide 3-5 factors and 1-3 actionable recommendations. Be specific with dollar
|
|||||||
const urgentLines = data.urgentComponents.length === 0
|
const urgentLines = data.urgentComponents.length === 0
|
||||||
? 'None — no components due within 5 years.'
|
? 'None — no components due within 5 years.'
|
||||||
: data.urgentComponents.map((c: any) => {
|
: data.urgentComponents.map((c: any) => {
|
||||||
const cost = parseFloat(c.replacement_cost || '0');
|
const cost = parseFloat(c.replacement_cost || c.estimated_cost || '0');
|
||||||
const funded = parseFloat(c.current_fund_balance || '0');
|
const funded = parseFloat(c.current_fund_balance || '0');
|
||||||
const gap = cost - funded;
|
const gap = cost - funded;
|
||||||
return `- ${c.name}: ${c.remaining_life_years} years remaining, $${gap.toFixed(0)} funding gap`;
|
return `- ${c.name}: ${c.remaining_life_years} years remaining, $${gap.toFixed(0)} funding gap`;
|
||||||
@@ -997,8 +1014,8 @@ Reserve Cash (bank accounts): $${data.reserveCash.toFixed(2)}
|
|||||||
Reserve Investments: $${data.totalInvestments.toFixed(2)}
|
Reserve Investments: $${data.totalInvestments.toFixed(2)}
|
||||||
Total Reserve Fund: $${data.totalReserveFund.toFixed(2)}
|
Total Reserve Fund: $${data.totalReserveFund.toFixed(2)}
|
||||||
|
|
||||||
Total Replacement Cost (all components): $${data.totalReplacementCost.toFixed(2)}
|
Total Replacement Cost (all components): ${data.totalReplacementCost > 0 ? '$' + data.totalReplacementCost.toFixed(2) : '$0.00 (no reserve components entered — funded ratio cannot be calculated)'}
|
||||||
Percent Funded: ${data.percentFunded.toFixed(1)}%
|
Percent Funded: ${data.totalReplacementCost > 0 ? data.percentFunded.toFixed(1) + '%' : 'N/A — no reserve components with replacement costs have been entered. Do NOT report a 0% funded ratio; instead note that funded ratio is unavailable due to missing component data.'}
|
||||||
|
|
||||||
Annual Reserve Contribution (budgeted income): $${data.annualReserveContribution.toFixed(2)}
|
Annual Reserve Contribution (budgeted income): $${data.annualReserveContribution.toFixed(2)}
|
||||||
Annual Reserve Expenses (budgeted): $${data.annualReserveExpenses.toFixed(2)}
|
Annual Reserve Expenses (budgeted): $${data.annualReserveExpenses.toFixed(2)}
|
||||||
|
|||||||
Reference in New Issue
Block a user