From 1e408482220c6b94cfa5a5c071808d32508ffde7 Mon Sep 17 00:00:00 2001 From: olsch01 Date: Sat, 21 Feb 2026 13:57:40 -0500 Subject: [PATCH] Add investments to All tab, withdrawal on create, modal fixes, and login logo - Show investment subtable under All accounts tab (was only under Operating/Reserve) - Add "Withdraw from primary account" switch (on by default) when creating investments; creates a journal entry to credit the primary asset account and debit the equity offset, keeping the books balanced - Prevent all account modals from closing on outside click to avoid data loss - Replace login page text title with the SVG logo Co-Authored-By: Claude Opus 4.6 --- .../investments/investments.service.ts | 52 ++++++++++++++++++- frontend/src/pages/accounts/AccountsPage.tsx | 43 +++++++++++---- frontend/src/pages/auth/LoginPage.tsx | 9 ++-- 3 files changed, 90 insertions(+), 14 deletions(-) diff --git a/backend/src/modules/investments/investments.service.ts b/backend/src/modules/investments/investments.service.ts index 9379d75..7a97968 100644 --- a/backend/src/modules/investments/investments.service.ts +++ b/backend/src/modules/investments/investments.service.ts @@ -47,7 +47,57 @@ export class InvestmentsService { dto.fund_type || 'reserve', dto.principal, dto.interest_rate || 0, dto.maturity_date || null, dto.purchase_date || null, dto.current_value || dto.principal, dto.notes], ); - return rows[0]; + const investment = rows[0]; + + // If withdraw_from_primary is true, create a journal entry to debit the primary account + if (dto.withdraw_from_primary && dto.principal > 0) { + const fundType = dto.fund_type || 'reserve'; + const primaryRows = await this.tenant.query( + `SELECT id, name FROM accounts WHERE is_primary = true AND fund_type = $1 AND is_active = true LIMIT 1`, + [fundType], + ); + if (primaryRows.length) { + const primaryAccount = primaryRows[0]; + const equityAccountNumber = fundType === 'reserve' ? 3100 : 3000; + const equityRows = await this.tenant.query( + 'SELECT id FROM accounts WHERE account_number = $1', + [equityAccountNumber], + ); + if (equityRows.length) { + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth() + 1; + const periods = await this.tenant.query( + 'SELECT id FROM fiscal_periods WHERE year = $1 AND month = $2', + [year, month], + ); + if (periods.length) { + const memo = `Transfer to investment: ${dto.name}`; + const jeRows = await this.tenant.query( + `INSERT INTO journal_entries (entry_date, description, entry_type, fiscal_period_id, is_posted, posted_at, created_by) + VALUES (CURRENT_DATE, $1, 'transfer', $2, true, NOW(), '00000000-0000-0000-0000-000000000000') + RETURNING *`, + [memo, periods[0].id], + ); + const je = jeRows[0]; + // Credit the primary asset account (reduces cash) + await this.tenant.query( + `INSERT INTO journal_entry_lines (journal_entry_id, account_id, debit, credit, memo) + VALUES ($1, $2, 0, $3, $4)`, + [je.id, primaryAccount.id, dto.principal, memo], + ); + // Debit the equity offset account (reduces fund balance) + await this.tenant.query( + `INSERT INTO journal_entry_lines (journal_entry_id, account_id, debit, credit, memo) + VALUES ($1, $2, $3, 0, $4)`, + [je.id, equityRows[0].id, dto.principal, memo], + ); + } + } + } + } + + return investment; } async update(id: string, dto: any) { diff --git a/frontend/src/pages/accounts/AccountsPage.tsx b/frontend/src/pages/accounts/AccountsPage.tsx index 73647d3..9dfd23e 100644 --- a/frontend/src/pages/accounts/AccountsPage.tsx +++ b/frontend/src/pages/accounts/AccountsPage.tsx @@ -173,6 +173,7 @@ export function AccountsPage() { maturityDate: null as Date | null, purchaseDate: null as Date | null, investmentNotes: '', + withdrawFromPrimary: true, }, validate: { accountNumber: (v, values) => isInvestmentType(values.accountType) ? null : (v > 0 ? null : 'Required'), @@ -218,6 +219,7 @@ export function AccountsPage() { purchase_date: values.purchaseDate ? values.purchaseDate.toISOString().split('T')[0] : null, current_value: values.principal, notes: values.investmentNotes || null, + withdraw_from_primary: values.withdrawFromPrimary, }); } return api.post('/accounts', values); @@ -515,13 +517,21 @@ export function AccountsPage() { - setPrimaryMutation.mutate(id)} - onAdjustBalance={handleAdjustBalance} - /> + + setPrimaryMutation.mutate(id)} + onAdjustBalance={handleAdjustBalance} + /> + {investments.filter(i => i.is_active).length > 0 && ( + <> + + i.is_active)} onEdit={handleEditInvestment} /> + + )} + @@ -577,6 +587,7 @@ export function AccountsPage() { onClose={close} title={editing ? 'Edit Account' : isInvestmentType(form.values.accountType) ? 'New Investment Account' : 'New Account'} size="md" + closeOnClickOutside={false} >
createMutation.mutate(values))}> @@ -680,6 +691,20 @@ export function AccountsPage() { maxRows={4} {...form.getInputProps('investmentNotes')} /> + + {(() => { + const primaryAcct = accounts.find( + (a) => a.is_primary && a.fund_type === form.values.fundType && !a.is_system, + ); + return ( + + ); + })()} )} @@ -707,7 +732,7 @@ export function AccountsPage() { {/* Balance Adjustment Modal */} - + {adjustingAccount && ( @@ -762,7 +787,7 @@ export function AccountsPage() { {/* Investment Edit Modal */} - + {editingInvestment && ( updateInvestmentMutation.mutate(values))}> diff --git a/frontend/src/pages/auth/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx index 2b644d6..5b3e122 100644 --- a/frontend/src/pages/auth/LoginPage.tsx +++ b/frontend/src/pages/auth/LoginPage.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; import { + Center, Container, Paper, - Title, Text, TextInput, PasswordInput, @@ -16,6 +16,7 @@ import { IconAlertCircle } from '@tabler/icons-react'; import { useNavigate, Link } from 'react-router-dom'; import api from '../../services/api'; import { useAuthStore } from '../../stores/authStore'; +import logoSrc from '../../assets/logo.svg'; export function LoginPage() { const [loading, setLoading] = useState(false); @@ -52,9 +53,9 @@ export function LoginPage() { return ( - - HOA LedgerIQ - +
+ HOA LedgerIQ +
Don't have an account?{' '}