Include investment holding values in net worth and account balances
- Net worth report, balance sheet, and daily snapshots now add holding
market values (falling back to cost basis) to investment-type account
balances (investment, pension, stocks_shares_isa, crypto_wallet)
- Accounts list shows total value for investment accounts with a
breakdown line ("£X cash + £Y holdings") when both are non-zero
- Add Holding modal gains a "Debit account for this purchase" toggle
that creates a matching withdrawal transaction, enabling proper cash
flow tracking for users who fund their brokerage via transfer first
- Both simple (holdings-only) and full cash-flow workflows produce
correct net worth figures without double-counting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cdc1e67321
commit
312594f3d2
5 changed files with 139 additions and 11 deletions
|
|
@ -2,6 +2,7 @@ import { useState } from "react";
|
|||
import { Link } from "react-router-dom";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { getAccounts, createAccount, updateAccount, deleteAccount, getNetWorth, type Account } from "@/api/accounts";
|
||||
import { getPortfolio } from "@/api/investments";
|
||||
import { formatCurrency } from "@/utils/currency";
|
||||
import { cn } from "@/utils/cn";
|
||||
import {
|
||||
|
|
@ -57,6 +58,17 @@ export default function AccountList() {
|
|||
queryFn: getNetWorth,
|
||||
});
|
||||
|
||||
const { data: portfolio } = useQuery({
|
||||
queryKey: ["portfolio"],
|
||||
queryFn: getPortfolio,
|
||||
});
|
||||
|
||||
const holdingValueByAccount = (portfolio?.holdings ?? []).reduce<Record<string, number>>((acc, h) => {
|
||||
const v = Number(h.current_value || h.cost_basis_total);
|
||||
acc[h.account_id] = (acc[h.account_id] ?? 0) + v;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const invalidate = () => {
|
||||
qc.invalidateQueries({ queryKey: ["accounts"] });
|
||||
qc.invalidateQueries({ queryKey: ["net-worth"] });
|
||||
|
|
@ -129,6 +141,7 @@ export default function AccountList() {
|
|||
accounts={assets}
|
||||
onEdit={setEditing}
|
||||
onDelete={id => deleteMutation.mutate(id)}
|
||||
holdingValueByAccount={holdingValueByAccount}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -147,6 +160,7 @@ export default function AccountList() {
|
|||
accounts={inactive}
|
||||
onEdit={setEditing}
|
||||
onDelete={id => deleteMutation.mutate(id)}
|
||||
holdingValueByAccount={holdingValueByAccount}
|
||||
muted
|
||||
/>
|
||||
)}
|
||||
|
|
@ -179,17 +193,21 @@ export default function AccountList() {
|
|||
);
|
||||
}
|
||||
|
||||
const INVESTMENT_TYPES = new Set(["investment", "pension", "stocks_shares_isa", "crypto_wallet"]);
|
||||
|
||||
function AccountGroup({
|
||||
title,
|
||||
accounts,
|
||||
onEdit,
|
||||
onDelete,
|
||||
holdingValueByAccount = {},
|
||||
muted = false,
|
||||
}: {
|
||||
title: string;
|
||||
accounts: Account[];
|
||||
onEdit: (a: Account) => void;
|
||||
onDelete: (id: string) => void;
|
||||
holdingValueByAccount?: Record<string, number>;
|
||||
muted?: boolean;
|
||||
}) {
|
||||
return (
|
||||
|
|
@ -243,14 +261,31 @@ function AccountGroup({
|
|||
</div>
|
||||
|
||||
<div className="text-right shrink-0">
|
||||
<p className={cn("font-semibold tabular-nums", isLiability ? "text-destructive" : "text-foreground")}>
|
||||
{formatCurrency(account.current_balance, account.currency)}
|
||||
</p>
|
||||
{account.credit_limit != null && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
limit {formatCurrency(account.credit_limit, account.currency)}
|
||||
</p>
|
||||
)}
|
||||
{(() => {
|
||||
const holdingVal = INVESTMENT_TYPES.has(account.type) ? (holdingValueByAccount[account.id] ?? 0) : 0;
|
||||
const cashBal = Number(account.current_balance);
|
||||
const total = cashBal + holdingVal;
|
||||
return (
|
||||
<>
|
||||
<p className={cn("font-semibold tabular-nums", isLiability ? "text-destructive" : "text-foreground")}>
|
||||
{formatCurrency(holdingVal > 0 ? total : cashBal, account.currency)}
|
||||
</p>
|
||||
{holdingVal > 0 && cashBal !== 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatCurrency(cashBal, account.currency)} cash + {formatCurrency(holdingVal, account.currency)} holdings
|
||||
</p>
|
||||
)}
|
||||
{holdingVal > 0 && cashBal === 0 && (
|
||||
<p className="text-xs text-muted-foreground">holdings value</p>
|
||||
)}
|
||||
{account.credit_limit != null && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
limit {formatCurrency(account.credit_limit, account.currency)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity shrink-0">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue