feat: investment chart alignment, auto-renew records, fund transfers, capital planning report, and upcoming activities (v2026.3.24)
- Lock InvestmentTimeline and ProjectionChart to shared X axis range - Auto-create renewal scenario_investments records when auto_renew is true - Add fund transfer mechanism between asset accounts with journal entries - Add Capital Planning Report (5-year forecast grouped by category) - Add Upcoming Investment Activities dashboard card (maturities + planned purchases) - Bump version to 2026.3.24 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,7 @@ import {
|
||||
IconStarFilled,
|
||||
IconAdjustments,
|
||||
IconInfoCircle,
|
||||
IconArrowsTransferDown,
|
||||
} from '@tabler/icons-react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '../../services/api';
|
||||
@@ -126,6 +127,7 @@ export function AccountsPage() {
|
||||
const [search, setSearch] = useState('');
|
||||
const [filterType, setFilterType] = useState<string | null>(null);
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [transferOpened, { open: openTransfer, close: closeTransfer }] = useDisclosure(false);
|
||||
const queryClient = useQueryClient();
|
||||
const isReadOnly = useIsReadOnly();
|
||||
|
||||
@@ -283,6 +285,39 @@ export function AccountsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
// ── Transfer form ──
|
||||
const transferForm = useForm({
|
||||
initialValues: {
|
||||
fromAccountId: '',
|
||||
toAccountId: '',
|
||||
amount: 0,
|
||||
transferDate: new Date() as Date | null,
|
||||
memo: '',
|
||||
},
|
||||
validate: {
|
||||
fromAccountId: (v) => (v ? null : 'Required'),
|
||||
toAccountId: (v, values) => !v ? 'Required' : v === values.fromAccountId ? 'Must be different from source' : null,
|
||||
amount: (v) => (v > 0 ? null : 'Must be greater than 0'),
|
||||
transferDate: (v) => (v ? null : 'Required'),
|
||||
},
|
||||
});
|
||||
|
||||
const transferMutation = useMutation({
|
||||
mutationFn: (values: { fromAccountId: string; toAccountId: string; amount: number; transferDate: string; memo: string }) =>
|
||||
api.post('/accounts/transfer', values),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['accounts'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['trial-balance'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
|
||||
notifications.show({ message: 'Transfer completed successfully', color: 'green' });
|
||||
closeTransfer();
|
||||
transferForm.reset();
|
||||
},
|
||||
onError: (err: any) => {
|
||||
notifications.show({ message: err.response?.data?.message || 'Transfer failed', color: 'red' });
|
||||
},
|
||||
});
|
||||
|
||||
// ── Investment edit form ──
|
||||
const invForm = useForm({
|
||||
initialValues: {
|
||||
@@ -408,6 +443,9 @@ export function AccountsPage() {
|
||||
const activeAccounts = filtered.filter((a) => a.is_active);
|
||||
const archivedAccounts = filtered.filter((a) => !a.is_active);
|
||||
|
||||
// Asset accounts for transfer modal (all active asset accounts, not just filtered by search)
|
||||
const assetAccounts = accounts.filter((a) => a.is_active && !a.is_system && a.account_type === 'asset');
|
||||
|
||||
// ── 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);
|
||||
@@ -505,9 +543,14 @@ export function AccountsPage() {
|
||||
size="sm"
|
||||
/>
|
||||
{!isReadOnly && (
|
||||
<Button leftSection={<IconPlus size={16} />} onClick={handleNew}>
|
||||
Add Account
|
||||
</Button>
|
||||
<>
|
||||
<Button variant="light" leftSection={<IconArrowsTransferDown size={16} />} onClick={openTransfer}>
|
||||
Transfer Funds
|
||||
</Button>
|
||||
<Button leftSection={<IconPlus size={16} />} onClick={handleNew}>
|
||||
Add Account
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
@@ -854,6 +897,69 @@ export function AccountsPage() {
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Transfer Funds Modal */}
|
||||
<Modal opened={transferOpened} onClose={closeTransfer} title="Transfer Funds Between Accounts" size="md" closeOnClickOutside={false}>
|
||||
<form onSubmit={transferForm.onSubmit((values) => {
|
||||
transferMutation.mutate({
|
||||
...values,
|
||||
transferDate: values.transferDate ? values.transferDate.toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
|
||||
});
|
||||
})}>
|
||||
<Stack>
|
||||
<Alert icon={<IconInfoCircle size={16} />} color="blue" variant="light">
|
||||
This creates a journal entry transferring funds between asset accounts.
|
||||
Both accounts will be updated in the general ledger.
|
||||
</Alert>
|
||||
<Select
|
||||
label="From Account"
|
||||
placeholder="Select source account"
|
||||
required
|
||||
data={assetAccounts.map((a) => ({
|
||||
value: a.id,
|
||||
label: `${a.name} (${a.fund_type}) — ${fmt(a.balance)}`,
|
||||
}))}
|
||||
searchable
|
||||
{...transferForm.getInputProps('fromAccountId')}
|
||||
/>
|
||||
<Select
|
||||
label="To Account"
|
||||
placeholder="Select destination account"
|
||||
required
|
||||
data={assetAccounts
|
||||
.filter((a) => a.id !== transferForm.values.fromAccountId)
|
||||
.map((a) => ({
|
||||
value: a.id,
|
||||
label: `${a.name} (${a.fund_type}) — ${fmt(a.balance)}`,
|
||||
}))}
|
||||
searchable
|
||||
{...transferForm.getInputProps('toAccountId')}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Amount"
|
||||
required
|
||||
prefix="$"
|
||||
decimalScale={2}
|
||||
thousandSeparator=","
|
||||
min={0.01}
|
||||
{...transferForm.getInputProps('amount')}
|
||||
/>
|
||||
<DateInput
|
||||
label="Transfer Date"
|
||||
required
|
||||
{...transferForm.getInputProps('transferDate')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Memo (optional)"
|
||||
placeholder="e.g. Monthly reserve contribution"
|
||||
{...transferForm.getInputProps('memo')}
|
||||
/>
|
||||
<Button type="submit" leftSection={<IconArrowsTransferDown size={16} />} loading={transferMutation.isPending}>
|
||||
Complete Transfer
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{/* Investment Edit Modal */}
|
||||
<Modal opened={invEditOpened} onClose={closeInvEdit} title="Edit Investment Account" size="md" closeOnClickOutside={false}>
|
||||
{editingInvestment && (
|
||||
|
||||
Reference in New Issue
Block a user