add icons to lists and solve money tracker bugs
All checks were successful
Test and deploy staging / staging (push) Successful in 2m30s

This commit is contained in:
2026-09-24 19:24:09 +00:00
parent a10585a4d2
commit c757d0882a
18 changed files with 1358 additions and 731 deletions

View File

@@ -4,13 +4,21 @@ export type ChartSeries = {
color: string;
points: ChartPoint[];
emphasis?: boolean;
/** Points after this date are drawn dashed (projected). */
dashedFrom?: string;
};
const palette = ['#0070f3', '#e5484d', '#46a758', '#ad7f00', '#8e4ec6', '#0091ad', '#e56910'];
/** The account's chosen color, or a palette color based on its position in the list. */
export function accountColor(account: { color?: string | null }, index: number): string {
return account.color || palette[index % palette.length];
}
export function balanceSeries(
accounts: { id: number; name: string; openingBalanceCents: number }[],
accounts: { id: number; name: string; openingBalanceCents: number; color?: string | null }[],
transactions: { accountId: number; date: string; amountCents: number }[]
): ChartSeries[] {
const palette = ['#0070f3', '#e5484d', '#46a758', '#ad7f00', '#8e4ec6', '#0091ad', '#e56910'];
const ordered = [...transactions].sort((a, b) => a.date.localeCompare(b.date));
const balances = new Map(accounts.map((account) => [account.id, account.openingBalanceCents]));
const accountPoints = new Map(accounts.map((account) => [account.id, [] as ChartPoint[]]));
@@ -34,7 +42,7 @@ export function balanceSeries(
return [
...accounts.map((account, index) => ({
label: account.name,
color: palette[index % palette.length],
color: accountColor(account, index),
points: accountPoints.get(account.id)!
})),
{ label: 'All accounts', color: 'var(--site-muted)', points: combinedPoints }
@@ -163,7 +171,7 @@ export function wealthProjection(
} else spendable += value;
}
}
if (validInvestmentId !== null && index > 0) {
if (index > 0) {
const growth = Math.round(indexBalance * monthlyGrowth);
invested += growth;
indexBalance += growth;
@@ -194,6 +202,82 @@ export function wealthProjection(
];
}
export type TimelineView = 'total' | 'split' | 'accounts';
/**
* Joins actual history (solid, through today) with the wealth projection (dashed, after
* today) so both read as one continuous line per measure.
*/
export function wealthTimeline(
accounts: {
id: number;
name: string;
kind: string;
openingBalanceCents: number;
color?: string | null;
}[],
actualTransactions: { accountId: number; date: string; amountCents: number }[],
projection: ChartSeries[],
today: string,
view: TimelineView
): ChartSeries[] {
const history = balanceSeries(accounts, actualTransactions);
const accountLines = history.slice(0, accounts.length);
const dates = history.at(-1)!.points.map((point) => point.date);
const sumKind = (investment: boolean) =>
dates.map((date, index) => ({
date,
value: accounts.reduce(
(sum, account, accountIndex) =>
(account.kind === 'investment') === investment
? sum + accountLines[accountIndex].points[index].value
: sum,
0
)
}));
const untilToday = (points: ChartPoint[]) => {
const last = points.at(-1);
return last && last.date < today ? [...points, { date: today, value: last.value }] : points;
};
const join = (actual: ChartPoint[], projected: ChartPoint[]) => {
const solid = untilToday(actual);
const cutoff = solid.at(-1)?.date ?? '';
return [...solid, ...projected.filter((point) => point.date > cutoff)];
};
const [totalProjection, spendableProjection, investedProjection] = projection;
const total: ChartSeries = {
label: 'Total',
color: 'var(--site-text)',
emphasis: true,
dashedFrom: today,
points: join(history.at(-1)!.points, totalProjection?.points ?? [])
};
if (view === 'accounts')
return [
...accountLines
.filter((line) => line.points.some((point) => point.value !== 0))
.map((line) => ({ ...line, points: untilToday(line.points) })),
total
];
if (view === 'split')
return [
total,
{
label: 'Spendable',
color: '#46a758',
dashedFrom: today,
points: join(sumKind(false), spendableProjection?.points ?? [])
},
{
label: 'Invested',
color: '#8e4ec6',
dashedFrom: today,
points: join(sumKind(true), investedProjection?.points ?? [])
}
];
return [total];
}
export function utilityEstimate(
readings: { date: string; reading: string; kind: string }[],
kind: string,