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 <noreply@anthropic.com>
This commit is contained in:
@@ -47,7 +47,57 @@ export class InvestmentsService {
|
|||||||
dto.fund_type || 'reserve', dto.principal, dto.interest_rate || 0,
|
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],
|
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) {
|
async update(id: string, dto: any) {
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ export function AccountsPage() {
|
|||||||
maturityDate: null as Date | null,
|
maturityDate: null as Date | null,
|
||||||
purchaseDate: null as Date | null,
|
purchaseDate: null as Date | null,
|
||||||
investmentNotes: '',
|
investmentNotes: '',
|
||||||
|
withdrawFromPrimary: true,
|
||||||
},
|
},
|
||||||
validate: {
|
validate: {
|
||||||
accountNumber: (v, values) => isInvestmentType(values.accountType) ? null : (v > 0 ? null : 'Required'),
|
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,
|
purchase_date: values.purchaseDate ? values.purchaseDate.toISOString().split('T')[0] : null,
|
||||||
current_value: values.principal,
|
current_value: values.principal,
|
||||||
notes: values.investmentNotes || null,
|
notes: values.investmentNotes || null,
|
||||||
|
withdraw_from_primary: values.withdrawFromPrimary,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return api.post('/accounts', values);
|
return api.post('/accounts', values);
|
||||||
@@ -515,13 +517,21 @@ export function AccountsPage() {
|
|||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|
||||||
<Tabs.Panel value="all" pt="sm">
|
<Tabs.Panel value="all" pt="sm">
|
||||||
<AccountTable
|
<Stack>
|
||||||
accounts={activeAccounts}
|
<AccountTable
|
||||||
onEdit={handleEdit}
|
accounts={activeAccounts}
|
||||||
onArchive={archiveMutation.mutate}
|
onEdit={handleEdit}
|
||||||
onSetPrimary={(id) => setPrimaryMutation.mutate(id)}
|
onArchive={archiveMutation.mutate}
|
||||||
onAdjustBalance={handleAdjustBalance}
|
onSetPrimary={(id) => setPrimaryMutation.mutate(id)}
|
||||||
/>
|
onAdjustBalance={handleAdjustBalance}
|
||||||
|
/>
|
||||||
|
{investments.filter(i => i.is_active).length > 0 && (
|
||||||
|
<>
|
||||||
|
<Divider label="Investment Accounts" labelPosition="center" my="xs" />
|
||||||
|
<InvestmentMiniTable investments={investments.filter(i => i.is_active)} onEdit={handleEditInvestment} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value="operating" pt="sm">
|
<Tabs.Panel value="operating" pt="sm">
|
||||||
<Stack>
|
<Stack>
|
||||||
@@ -577,6 +587,7 @@ export function AccountsPage() {
|
|||||||
onClose={close}
|
onClose={close}
|
||||||
title={editing ? 'Edit Account' : isInvestmentType(form.values.accountType) ? 'New Investment Account' : 'New Account'}
|
title={editing ? 'Edit Account' : isInvestmentType(form.values.accountType) ? 'New Investment Account' : 'New Account'}
|
||||||
size="md"
|
size="md"
|
||||||
|
closeOnClickOutside={false}
|
||||||
>
|
>
|
||||||
<form onSubmit={form.onSubmit((values) => createMutation.mutate(values))}>
|
<form onSubmit={form.onSubmit((values) => createMutation.mutate(values))}>
|
||||||
<Stack>
|
<Stack>
|
||||||
@@ -680,6 +691,20 @@ export function AccountsPage() {
|
|||||||
maxRows={4}
|
maxRows={4}
|
||||||
{...form.getInputProps('investmentNotes')}
|
{...form.getInputProps('investmentNotes')}
|
||||||
/>
|
/>
|
||||||
|
<Divider my="xs" />
|
||||||
|
{(() => {
|
||||||
|
const primaryAcct = accounts.find(
|
||||||
|
(a) => a.is_primary && a.fund_type === form.values.fundType && !a.is_system,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
label={`Withdraw from primary account${primaryAcct ? ` (${primaryAcct.name})` : ''}`}
|
||||||
|
description="Creates a journal entry to deduct the principal from your primary account"
|
||||||
|
{...form.getInputProps('withdrawFromPrimary', { type: 'checkbox' })}
|
||||||
|
disabled={!primaryAcct}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -707,7 +732,7 @@ export function AccountsPage() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Balance Adjustment Modal */}
|
{/* Balance Adjustment Modal */}
|
||||||
<Modal opened={adjustOpened} onClose={closeAdjust} title="Adjust Balance" size="md">
|
<Modal opened={adjustOpened} onClose={closeAdjust} title="Adjust Balance" size="md" closeOnClickOutside={false}>
|
||||||
{adjustingAccount && (
|
{adjustingAccount && (
|
||||||
<form onSubmit={adjustForm.onSubmit(handleAdjustSubmit)}>
|
<form onSubmit={adjustForm.onSubmit(handleAdjustSubmit)}>
|
||||||
<Stack>
|
<Stack>
|
||||||
@@ -762,7 +787,7 @@ export function AccountsPage() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Investment Edit Modal */}
|
{/* Investment Edit Modal */}
|
||||||
<Modal opened={invEditOpened} onClose={closeInvEdit} title="Edit Investment Account" size="md">
|
<Modal opened={invEditOpened} onClose={closeInvEdit} title="Edit Investment Account" size="md" closeOnClickOutside={false}>
|
||||||
{editingInvestment && (
|
{editingInvestment && (
|
||||||
<form onSubmit={invForm.onSubmit((values) => updateInvestmentMutation.mutate(values))}>
|
<form onSubmit={invForm.onSubmit((values) => updateInvestmentMutation.mutate(values))}>
|
||||||
<Stack>
|
<Stack>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
Center,
|
||||||
Container,
|
Container,
|
||||||
Paper,
|
Paper,
|
||||||
Title,
|
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
@@ -16,6 +16,7 @@ import { IconAlertCircle } from '@tabler/icons-react';
|
|||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import api from '../../services/api';
|
import api from '../../services/api';
|
||||||
import { useAuthStore } from '../../stores/authStore';
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
|
import logoSrc from '../../assets/logo.svg';
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -52,9 +53,9 @@ export function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size={420} my={80}>
|
<Container size={420} my={80}>
|
||||||
<Title ta="center" order={2}>
|
<Center>
|
||||||
HOA LedgerIQ
|
<img src={logoSrc} alt="HOA LedgerIQ" style={{ height: 60 }} />
|
||||||
</Title>
|
</Center>
|
||||||
<Text c="dimmed" size="sm" ta="center" mt={5}>
|
<Text c="dimmed" size="sm" ta="center" mt={5}>
|
||||||
Don't have an account?{' '}
|
Don't have an account?{' '}
|
||||||
<Anchor component={Link} to="/register" size="sm">
|
<Anchor component={Link} to="/register" size="sm">
|
||||||
|
|||||||
Reference in New Issue
Block a user