Filter Accounts page to show only cash positions (asset, liability, investment)

- Hide income, expense, and equity accounts from Accounts view — they are
  internal bookkeeping managed via budget import and system operations
- Restrict Add Account form to asset and liability types plus investments
- Update summary cards to show Cash on Hand, Investments, Liabilities, Net Position
- Reports (Income Statement, Balance Sheet, Budget vs Actual, etc.) are unchanged
  and continue to use all account types from their own API endpoints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 18:53:15 -05:00
parent 017421a85a
commit b5861de609

View File

@@ -45,12 +45,12 @@ const INVESTMENT_TYPES = ['inv_cd', 'inv_money_market', 'inv_treasury', 'inv_sav
const isInvestmentType = (t: string) => INVESTMENT_TYPES.includes(t); const isInvestmentType = (t: string) => INVESTMENT_TYPES.includes(t);
// Only show account types that represent real cash / financial positions.
// Income, expense, and equity accounts are internal bookkeeping — managed via
// budget import and system operations, not manually on this page.
const ACCOUNT_TYPE_OPTIONS = [ const ACCOUNT_TYPE_OPTIONS = [
{ value: 'asset', label: 'Asset' }, { value: 'asset', label: 'Asset (Bank Account)' },
{ value: 'liability', label: 'Liability' }, { value: 'liability', label: 'Liability' },
{ value: 'equity', label: 'Equity' },
{ value: 'income', label: 'Income' },
{ value: 'expense', label: 'Expense' },
{ value: '__divider', label: '── Investment Accounts ──', disabled: true }, { value: '__divider', label: '── Investment Accounts ──', disabled: true },
{ value: 'inv_cd', label: 'Investment — CD' }, { value: 'inv_cd', label: 'Investment — CD' },
{ value: 'inv_money_market', label: 'Investment — Money Market' }, { value: 'inv_money_market', label: 'Investment — Money Market' },
@@ -161,7 +161,7 @@ export function AccountsPage() {
accountNumber: '', accountNumber: '',
name: '', name: '',
description: '', description: '',
accountType: 'expense', accountType: 'asset',
fundType: 'operating', fundType: 'operating',
is1099Reportable: false, is1099Reportable: false,
initialBalance: 0, initialBalance: 0,
@@ -390,9 +390,13 @@ export function AccountsPage() {
}; };
// ── Filtering ── // ── Filtering ──
// Hide system accounts (auto-created equity offsets) from the accounts view // Only show asset and liability accounts — these represent real cash positions.
// Income, expense, and equity accounts are internal bookkeeping managed via
// budget import, transactions, and system operations.
const VISIBLE_ACCOUNT_TYPES = ['asset', 'liability'];
const filtered = accounts.filter((a) => { const filtered = accounts.filter((a) => {
if (a.is_system) return false; if (a.is_system) return false;
if (!VISIBLE_ACCOUNT_TYPES.includes(a.account_type)) return false;
if (search && !a.name.toLowerCase().includes(search.toLowerCase()) && !String(a.account_number).includes(search)) return false; if (search && !a.name.toLowerCase().includes(search.toLowerCase()) && !String(a.account_number).includes(search)) return false;
if (filterType && a.account_type !== filterType) return false; if (filterType && a.account_type !== filterType) return false;
if (filterFund && a.fund_type !== filterFund) return false; if (filterFund && a.fund_type !== filterFund) return false;
@@ -407,10 +411,11 @@ export function AccountsPage() {
const reserveInvestments = investments.filter((i) => i.fund_type === 'reserve' && i.is_active); const reserveInvestments = investments.filter((i) => i.fund_type === 'reserve' && i.is_active);
// ── Summary cards ── // ── Summary cards ──
// Exclude system accounts from summaries to avoid double-counting // Only show asset and liability totals — these are real cash positions.
// Income/expense/equity are internal bookkeeping and don't belong here.
const totalsByType = accounts.reduce( const totalsByType = accounts.reduce(
(acc, a) => { (acc, a) => {
if (a.is_active && !a.is_system) { if (a.is_active && !a.is_system && VISIBLE_ACCOUNT_TYPES.includes(a.account_type)) {
acc[a.account_type] = (acc[a.account_type] || 0) + parseFloat(a.balance || '0'); acc[a.account_type] = (acc[a.account_type] || 0) + parseFloat(a.balance || '0');
} }
return acc; return acc;
@@ -418,14 +423,17 @@ export function AccountsPage() {
{} as Record<string, number>, {} as Record<string, number>,
); );
// Include investment principal in the asset totals // Include investment current value in a separate summary card
const investmentTotal = investments const investmentTotal = investments
.filter((i) => i.is_active) .filter((i) => i.is_active)
.reduce((s, i) => s + parseFloat(i.current_value || i.principal || '0'), 0); .reduce((s, i) => s + parseFloat(i.current_value || i.principal || '0'), 0);
if (investmentTotal > 0) { if (investmentTotal > 0) {
totalsByType['asset (investments)'] = investmentTotal; totalsByType['investments'] = investmentTotal;
} }
// Net position = assets + investments - liabilities
const netPosition = (totalsByType['asset'] || 0) + investmentTotal - (totalsByType['liability'] || 0);
// ── Adjust modal: current balance from trial balance ── // ── Adjust modal: current balance from trial balance ──
const adjustCurrentBalance = adjustingAccount const adjustCurrentBalance = adjustingAccount
? parseFloat( ? parseFloat(
@@ -461,17 +469,27 @@ export function AccountsPage() {
</Group> </Group>
</Group> </Group>
<SimpleGrid cols={{ base: 2, sm: 5 }}> <SimpleGrid cols={{ base: 2, sm: 4 }}>
{Object.entries(totalsByType).map(([type, total]) => ( <Card withBorder p="xs">
<Card withBorder p="xs" key={type}> <Text size="xs" c="dimmed">Cash on Hand</Text>
<Text size="xs" c="dimmed" tt="capitalize"> <Text fw={700} size="sm" c="green">{fmt(totalsByType['asset'] || 0)}</Text>
{type} </Card>
</Text> {investmentTotal > 0 && (
<Text fw={700} size="sm" c={accountTypeColors[type]}> <Card withBorder p="xs">
{fmt(total)} <Text size="xs" c="dimmed">Investments</Text>
</Text> <Text fw={700} size="sm" c="teal">{fmt(investmentTotal)}</Text>
</Card> </Card>
))} )}
{(totalsByType['liability'] || 0) > 0 && (
<Card withBorder p="xs">
<Text size="xs" c="dimmed">Liabilities</Text>
<Text fw={700} size="sm" c="red">{fmt(totalsByType['liability'] || 0)}</Text>
</Card>
)}
<Card withBorder p="xs">
<Text size="xs" c="dimmed">Net Position</Text>
<Text fw={700} size="sm" c={netPosition >= 0 ? 'green' : 'red'}>{fmt(netPosition)}</Text>
</Card>
</SimpleGrid> </SimpleGrid>
<Group> <Group>
@@ -485,7 +503,10 @@ export function AccountsPage() {
<Select <Select
placeholder="Type" placeholder="Type"
clearable clearable
data={['asset', 'liability', 'equity', 'income', 'expense']} data={[
{ value: 'asset', label: 'Asset' },
{ value: 'liability', label: 'Liability' },
]}
value={filterType} value={filterType}
onChange={setFilterType} onChange={setFilterType}
w={150} w={150}
@@ -616,11 +637,8 @@ export function AccountsPage() {
label="Account Type" label="Account Type"
required required
data={[ data={[
{ value: 'asset', label: 'Asset' }, { value: 'asset', label: 'Asset (Bank Account)' },
{ value: 'liability', label: 'Liability' }, { value: 'liability', label: 'Liability' },
{ value: 'equity', label: 'Equity' },
{ value: 'income', label: 'Income' },
{ value: 'expense', label: 'Expense' },
]} ]}
{...form.getInputProps('accountType')} {...form.getInputProps('accountType')}
/> />