From c68a7e21c32d4ebcd0925ebb502e79b4fb3b5e8e Mon Sep 17 00:00:00 2001 From: olsch01 Date: Fri, 20 Feb 2026 08:33:00 -0500 Subject: [PATCH] Show investments under Operating/Reserve tabs and include in fund totals - Investment accounts now appear under their respective Operating/Reserve tabs in the Accounts page, with a compact sub-table showing name, type, institution, principal/value, rate, interest earned, and maturity info - Investment values (current_value) are included in dashboard Total Cash KPI - Reserve investment values are added to Reserve Fund Balance KPI and project funded percentage calculations - Year-end report reserve status now includes reserve investment values - Tab counts updated to include investment accounts per fund type - Summary cards show separate "asset (investments)" total for visibility Co-Authored-By: Claude Opus 4.6 --- .../src/modules/projects/projects.service.ts | 20 ++- .../src/modules/reports/reports.service.ts | 26 +++- frontend/src/pages/accounts/AccountsPage.tsx | 141 +++++++++++++++--- 3 files changed, 163 insertions(+), 24 deletions(-) diff --git a/backend/src/modules/projects/projects.service.ts b/backend/src/modules/projects/projects.service.ts index c74d1ee..90f412e 100644 --- a/backend/src/modules/projects/projects.service.ts +++ b/backend/src/modules/projects/projects.service.ts @@ -10,7 +10,7 @@ export class ProjectsService { // The total reserve fund balance (from reserve EQUITY accounts = fund balance) is distributed // proportionally across reserve projects based on their estimated_cost return this.tenant.query(` - WITH reserve_balance AS ( + WITH reserve_equity AS ( SELECT COALESCE(SUM(sub.balance), 0) as total FROM ( SELECT COALESCE(SUM(jel.credit), 0) - COALESCE(SUM(jel.debit), 0) as balance FROM accounts a @@ -20,6 +20,14 @@ export class ProjectsService { GROUP BY a.id ) sub ), + reserve_investments AS ( + SELECT COALESCE(SUM(current_value), 0) as total + FROM investment_accounts WHERE fund_type = 'reserve' AND is_active = true + ), + reserve_balance AS ( + SELECT re.total + ri.total as total + FROM reserve_equity re, reserve_investments ri + ), reserve_total_cost AS ( SELECT COALESCE(SUM(estimated_cost), 0) as total FROM projects @@ -54,7 +62,7 @@ export class ProjectsService { // Only return projects that have target_year set (for the Capital Planning kanban) // Uses the same dynamic reserve fund balance computation as findAll() return this.tenant.query(` - WITH reserve_balance AS ( + WITH reserve_equity AS ( SELECT COALESCE(SUM(sub.balance), 0) as total FROM ( SELECT COALESCE(SUM(jel.credit), 0) - COALESCE(SUM(jel.debit), 0) as balance FROM accounts a @@ -64,6 +72,14 @@ export class ProjectsService { GROUP BY a.id ) sub ), + reserve_investments AS ( + SELECT COALESCE(SUM(current_value), 0) as total + FROM investment_accounts WHERE fund_type = 'reserve' AND is_active = true + ), + reserve_balance AS ( + SELECT re.total + ri.total as total + FROM reserve_equity re, reserve_investments ri + ), reserve_total_cost AS ( SELECT COALESCE(SUM(estimated_cost), 0) as total FROM projects diff --git a/backend/src/modules/reports/reports.service.ts b/backend/src/modules/reports/reports.service.ts index c04d672..45f7646 100644 --- a/backend/src/modules/reports/reports.service.ts +++ b/backend/src/modules/reports/reports.service.ts @@ -374,9 +374,9 @@ export class ReportsService { `, [year]); // Reserve fund status from unified projects table - // Uses dynamic reserve balance computation (reserve equity accounts = fund balance) + // Uses dynamic reserve balance computation (reserve equity accounts + reserve investments) const reserveStatus = await this.tenant.query(` - WITH reserve_balance AS ( + WITH reserve_equity AS ( SELECT COALESCE(SUM(sub.balance), 0) as total FROM ( SELECT COALESCE(SUM(jel.credit), 0) - COALESCE(SUM(jel.debit), 0) as balance FROM accounts a @@ -386,6 +386,14 @@ export class ReportsService { GROUP BY a.id ) sub ), + reserve_investments AS ( + SELECT COALESCE(SUM(current_value), 0) as total + FROM investment_accounts WHERE fund_type = 'reserve' AND is_active = true + ), + reserve_balance AS ( + SELECT re.total + ri.total as total + FROM reserve_equity re, reserve_investments ri + ), reserve_total_cost AS ( SELECT COALESCE(SUM(estimated_cost), 0) as total FROM projects @@ -432,7 +440,12 @@ export class ReportsService { GROUP BY a.id ) sub `); - const totalCash = parseFloat(cash[0]?.total || '0'); + // Also include investment account current_value in total cash + const investmentCash = await this.tenant.query(` + SELECT COALESCE(SUM(current_value), 0) as total + FROM investment_accounts WHERE is_active = true + `); + const totalCash = parseFloat(cash[0]?.total || '0') + parseFloat(investmentCash[0]?.total || '0'); // Receivables: sum of unpaid invoices const ar = await this.tenant.query(` @@ -453,6 +466,11 @@ export class ReportsService { GROUP BY a.id ) sub `); + // Add reserve investment account values to the reserve fund total + const reserveInvestments = await this.tenant.query(` + SELECT COALESCE(SUM(current_value), 0) as total + FROM investment_accounts WHERE fund_type = 'reserve' AND is_active = true + `); // Delinquent count (overdue invoices) const delinquent = await this.tenant.query(` @@ -470,7 +488,7 @@ export class ReportsService { return { total_cash: totalCash.toFixed(2), total_receivables: ar[0]?.total || '0.00', - reserve_fund_balance: parseFloat(reserves[0]?.total || '0').toFixed(2), + reserve_fund_balance: (parseFloat(reserves[0]?.total || '0') + parseFloat(reserveInvestments[0]?.total || '0')).toFixed(2), delinquent_units: parseInt(delinquent[0]?.count || '0'), recent_transactions: recentTx, }; diff --git a/frontend/src/pages/accounts/AccountsPage.tsx b/frontend/src/pages/accounts/AccountsPage.tsx index 954073c..5cc9d7c 100644 --- a/frontend/src/pages/accounts/AccountsPage.tsx +++ b/frontend/src/pages/accounts/AccountsPage.tsx @@ -104,6 +104,7 @@ interface TrialBalanceEntry { const accountTypeColors: Record = { asset: 'green', + 'asset (investments)': 'teal', liability: 'red', equity: 'violet', income: 'blue', @@ -329,6 +330,10 @@ export function AccountsPage() { const activeAccounts = filtered.filter((a) => a.is_active); const archivedAccounts = filtered.filter((a) => !a.is_active); + // ── Investments split by fund type ── + const operatingInvestments = investments.filter((i) => i.fund_type === 'operating' && i.is_active); + const reserveInvestments = investments.filter((i) => i.fund_type === 'reserve' && i.is_active); + // ── Summary cards ── const totalsByType = accounts.reduce( (acc, a) => { @@ -340,6 +345,14 @@ export function AccountsPage() { {} as Record, ); + // Include investment principal in the asset totals + const investmentTotal = investments + .filter((i) => i.is_active) + .reduce((s, i) => s + parseFloat(i.current_value || i.principal || '0'), 0); + if (investmentTotal > 0) { + totalsByType['asset (investments)'] = investmentTotal; + } + // ── Adjust modal: current balance from trial balance ── const adjustCurrentBalance = adjustingAccount ? parseFloat( @@ -416,10 +429,14 @@ export function AccountsPage() { - All ({activeAccounts.length}) - Operating - Reserve - Investments + All ({activeAccounts.length + investments.filter(i => i.is_active).length}) + + Operating ({activeAccounts.filter(a => a.fund_type === 'operating').length + operatingInvestments.length}) + + + Reserve ({activeAccounts.filter(a => a.fund_type === 'reserve').length + reserveInvestments.length}) + + Investments ({investments.filter(i => i.is_active).length}) {showArchived && archivedAccounts.length > 0 && ( Archived ({archivedAccounts.length}) @@ -437,22 +454,38 @@ export function AccountsPage() { /> - a.fund_type === 'operating')} - onEdit={handleEdit} - onArchive={archiveMutation.mutate} - onSetPrimary={(id) => setPrimaryMutation.mutate(id)} - onAdjustBalance={handleAdjustBalance} - /> + + a.fund_type === 'operating')} + onEdit={handleEdit} + onArchive={archiveMutation.mutate} + onSetPrimary={(id) => setPrimaryMutation.mutate(id)} + onAdjustBalance={handleAdjustBalance} + /> + {operatingInvestments.length > 0 && ( + <> + + + + )} + - a.fund_type === 'reserve')} - onEdit={handleEdit} - onArchive={archiveMutation.mutate} - onSetPrimary={(id) => setPrimaryMutation.mutate(id)} - onAdjustBalance={handleAdjustBalance} - /> + + a.fund_type === 'reserve')} + onEdit={handleEdit} + onArchive={archiveMutation.mutate} + onSetPrimary={(id) => setPrimaryMutation.mutate(id)} + onAdjustBalance={handleAdjustBalance} + /> + {reserveInvestments.length > 0 && ( + <> + + + + )} + @@ -916,3 +949,75 @@ function InvestmentsTab({ ); } + +// ── Compact Investment Table for Operating/Reserve tabs ── + +function InvestmentMiniTable({ investments }: { investments: Investment[] }) { + const totalValue = investments.reduce( + (s, i) => s + parseFloat(i.current_value || i.principal || '0'), + 0, + ); + + return ( + + + + {investments.length} investment{investments.length !== 1 ? 's' : ''} — Total: {fmt(totalValue)} + + + + + + Name + Type + Institution + Principal / Value + Rate + Interest Earned + Maturity + + + + {investments.map((inv) => ( + + {inv.name} + + + {inv.investment_type} + + + {inv.institution || '-'} + + {fmt(inv.current_value || inv.principal)} + + + {parseFloat(inv.interest_rate || '0').toFixed(2)}% + + + {inv.interest_earned !== null ? fmt(inv.interest_earned) : '-'} + + + {inv.maturity_date ? ( + + {new Date(inv.maturity_date).toLocaleDateString()} + {inv.days_remaining !== null && ( + + {inv.days_remaining}d + + )} + + ) : ( + '-' + )} + + + ))} + +
+
+ ); +}