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);
// 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 = [
{ value: 'asset', label: 'Asset' },
{ value: 'asset', label: 'Asset (Bank Account)' },
{ value: 'liability', label: 'Liability' },
{ value: 'equity', label: 'Equity' },
{ value: 'income', label: 'Income' },
{ value: 'expense', label: 'Expense' },
{ value: '__divider', label: '── Investment Accounts ──', disabled: true },
{ value: 'inv_cd', label: 'Investment — CD' },
{ value: 'inv_money_market', label: 'Investment — Money Market' },
@@ -161,7 +161,7 @@ export function AccountsPage() {
accountNumber: '',
name: '',
description: '',
accountType: 'expense',
accountType: 'asset',
fundType: 'operating',
is1099Reportable: false,
initialBalance: 0,
@@ -390,9 +390,13 @@ export function AccountsPage() {
};
// ── 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) => {
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 (filterType && a.account_type !== filterType) 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);
// ── 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(
(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');
}
return acc;
@@ -418,14 +423,17 @@ export function AccountsPage() {
{} as Record<string, number>,
);
// Include investment principal in the asset totals
// Include investment current value in a separate summary card
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;
totalsByType['investments'] = investmentTotal;
}
// Net position = assets + investments - liabilities
const netPosition = (totalsByType['asset'] || 0) + investmentTotal - (totalsByType['liability'] || 0);
// ── Adjust modal: current balance from trial balance ──
const adjustCurrentBalance = adjustingAccount
? parseFloat(
@@ -461,17 +469,27 @@ export function AccountsPage() {
</Group>
</Group>
<SimpleGrid cols={{ base: 2, sm: 5 }}>
{Object.entries(totalsByType).map(([type, total]) => (
<Card withBorder p="xs" key={type}>
<Text size="xs" c="dimmed" tt="capitalize">
{type}
</Text>
<Text fw={700} size="sm" c={accountTypeColors[type]}>
{fmt(total)}
</Text>
<SimpleGrid cols={{ base: 2, sm: 4 }}>
<Card withBorder p="xs">
<Text size="xs" c="dimmed">Cash on Hand</Text>
<Text fw={700} size="sm" c="green">{fmt(totalsByType['asset'] || 0)}</Text>
</Card>
{investmentTotal > 0 && (
<Card withBorder p="xs">
<Text size="xs" c="dimmed">Investments</Text>
<Text fw={700} size="sm" c="teal">{fmt(investmentTotal)}</Text>
</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>
<Group>
@@ -485,7 +503,10 @@ export function AccountsPage() {
<Select
placeholder="Type"
clearable
data={['asset', 'liability', 'equity', 'income', 'expense']}
data={[
{ value: 'asset', label: 'Asset' },
{ value: 'liability', label: 'Liability' },
]}
value={filterType}
onChange={setFilterType}
w={150}
@@ -616,11 +637,8 @@ export function AccountsPage() {
label="Account Type"
required
data={[
{ value: 'asset', label: 'Asset' },
{ value: 'asset', label: 'Asset (Bank Account)' },
{ value: 'liability', label: 'Liability' },
{ value: 'equity', label: 'Equity' },
{ value: 'income', label: 'Income' },
{ value: 'expense', label: 'Expense' },
]}
{...form.getInputProps('accountType')}
/>