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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -104,6 +104,7 @@ interface TrialBalanceEntry {
|
||||
|
||||
const accountTypeColors: Record<string, string> = {
|
||||
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<string, number>,
|
||||
);
|
||||
|
||||
// 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() {
|
||||
|
||||
<Tabs defaultValue="all">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="all">All ({activeAccounts.length})</Tabs.Tab>
|
||||
<Tabs.Tab value="operating">Operating</Tabs.Tab>
|
||||
<Tabs.Tab value="reserve">Reserve</Tabs.Tab>
|
||||
<Tabs.Tab value="investments">Investments</Tabs.Tab>
|
||||
<Tabs.Tab value="all">All ({activeAccounts.length + investments.filter(i => i.is_active).length})</Tabs.Tab>
|
||||
<Tabs.Tab value="operating">
|
||||
Operating ({activeAccounts.filter(a => a.fund_type === 'operating').length + operatingInvestments.length})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="reserve">
|
||||
Reserve ({activeAccounts.filter(a => a.fund_type === 'reserve').length + reserveInvestments.length})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="investments">Investments ({investments.filter(i => i.is_active).length})</Tabs.Tab>
|
||||
{showArchived && archivedAccounts.length > 0 && (
|
||||
<Tabs.Tab value="archived" color="gray">
|
||||
Archived ({archivedAccounts.length})
|
||||
@@ -437,22 +454,38 @@ export function AccountsPage() {
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="operating" pt="sm">
|
||||
<AccountTable
|
||||
accounts={activeAccounts.filter((a) => a.fund_type === 'operating')}
|
||||
onEdit={handleEdit}
|
||||
onArchive={archiveMutation.mutate}
|
||||
onSetPrimary={(id) => setPrimaryMutation.mutate(id)}
|
||||
onAdjustBalance={handleAdjustBalance}
|
||||
/>
|
||||
<Stack>
|
||||
<AccountTable
|
||||
accounts={activeAccounts.filter((a) => a.fund_type === 'operating')}
|
||||
onEdit={handleEdit}
|
||||
onArchive={archiveMutation.mutate}
|
||||
onSetPrimary={(id) => setPrimaryMutation.mutate(id)}
|
||||
onAdjustBalance={handleAdjustBalance}
|
||||
/>
|
||||
{operatingInvestments.length > 0 && (
|
||||
<>
|
||||
<Divider label="Operating Investment Accounts" labelPosition="center" my="xs" />
|
||||
<InvestmentMiniTable investments={operatingInvestments} />
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="reserve" pt="sm">
|
||||
<AccountTable
|
||||
accounts={activeAccounts.filter((a) => a.fund_type === 'reserve')}
|
||||
onEdit={handleEdit}
|
||||
onArchive={archiveMutation.mutate}
|
||||
onSetPrimary={(id) => setPrimaryMutation.mutate(id)}
|
||||
onAdjustBalance={handleAdjustBalance}
|
||||
/>
|
||||
<Stack>
|
||||
<AccountTable
|
||||
accounts={activeAccounts.filter((a) => a.fund_type === 'reserve')}
|
||||
onEdit={handleEdit}
|
||||
onArchive={archiveMutation.mutate}
|
||||
onSetPrimary={(id) => setPrimaryMutation.mutate(id)}
|
||||
onAdjustBalance={handleAdjustBalance}
|
||||
/>
|
||||
{reserveInvestments.length > 0 && (
|
||||
<>
|
||||
<Divider label="Reserve Investment Accounts" labelPosition="center" my="xs" />
|
||||
<InvestmentMiniTable investments={reserveInvestments} />
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="investments" pt="sm">
|
||||
<InvestmentsTab investments={investments} isLoading={investmentsLoading} />
|
||||
@@ -916,3 +949,75 @@ function InvestmentsTab({
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<Stack gap="xs">
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" fw={600} c="dimmed">
|
||||
{investments.length} investment{investments.length !== 1 ? 's' : ''} — Total: {fmt(totalValue)}
|
||||
</Text>
|
||||
</Group>
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Name</Table.Th>
|
||||
<Table.Th>Type</Table.Th>
|
||||
<Table.Th>Institution</Table.Th>
|
||||
<Table.Th ta="right">Principal / Value</Table.Th>
|
||||
<Table.Th ta="right">Rate</Table.Th>
|
||||
<Table.Th ta="right">Interest Earned</Table.Th>
|
||||
<Table.Th>Maturity</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{investments.map((inv) => (
|
||||
<Table.Tr key={inv.id}>
|
||||
<Table.Td fw={500}>{inv.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm" variant="light" color="teal">
|
||||
{inv.investment_type}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{inv.institution || '-'}</Table.Td>
|
||||
<Table.Td ta="right" ff="monospace">
|
||||
{fmt(inv.current_value || inv.principal)}
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
{parseFloat(inv.interest_rate || '0').toFixed(2)}%
|
||||
</Table.Td>
|
||||
<Table.Td ta="right" ff="monospace">
|
||||
{inv.interest_earned !== null ? fmt(inv.interest_earned) : '-'}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{inv.maturity_date ? (
|
||||
<Group gap={4}>
|
||||
<Text size="sm">{new Date(inv.maturity_date).toLocaleDateString()}</Text>
|
||||
{inv.days_remaining !== null && (
|
||||
<Badge
|
||||
size="xs"
|
||||
color={inv.days_remaining <= 30 ? 'red' : inv.days_remaining <= 90 ? 'yellow' : 'gray'}
|
||||
variant="light"
|
||||
>
|
||||
{inv.days_remaining}d
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user