mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-30 10:14:53 -05:00
Add projected net worth to crossover point report (#6384)
This commit is contained in:
@@ -27,6 +27,7 @@ type CrossoverGraphProps = {
|
|||||||
x: string;
|
x: string;
|
||||||
investmentIncome: number;
|
investmentIncome: number;
|
||||||
expenses: number;
|
expenses: number;
|
||||||
|
nestEgg: number;
|
||||||
isProjection?: boolean;
|
isProjection?: boolean;
|
||||||
}>;
|
}>;
|
||||||
start: string;
|
start: string;
|
||||||
@@ -60,6 +61,7 @@ export function CrossoverGraph({
|
|||||||
x: string;
|
x: string;
|
||||||
investmentIncome: number | string;
|
investmentIncome: number | string;
|
||||||
expenses: number | string;
|
expenses: number | string;
|
||||||
|
nestEgg: number | string;
|
||||||
isProjection?: boolean;
|
isProjection?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -118,6 +120,21 @@ export function CrossoverGraph({
|
|||||||
</div>
|
</div>
|
||||||
<div>{format(payload[0].payload.expenses, 'financial')}</div>
|
<div>{format(payload[0].payload.expenses, 'financial')}</div>
|
||||||
</View>
|
</View>
|
||||||
|
<View
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{payload[0].payload.isProjection ? (
|
||||||
|
<Trans>Target Nest Egg:</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Nest Egg:</Trans>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>{format(payload[0].payload.nestEgg, 'financial')}</div>
|
||||||
|
</View>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ type CrossoverData = {
|
|||||||
x: string;
|
x: string;
|
||||||
investmentIncome: number;
|
investmentIncome: number;
|
||||||
expenses: number;
|
expenses: number;
|
||||||
|
nestEgg: number;
|
||||||
isProjection?: boolean;
|
isProjection?: boolean;
|
||||||
}>;
|
}>;
|
||||||
start: string;
|
start: string;
|
||||||
@@ -69,6 +70,7 @@ type CrossoverData = {
|
|||||||
historicalReturn: number | null;
|
historicalReturn: number | null;
|
||||||
yearsToRetire: number | null;
|
yearsToRetire: number | null;
|
||||||
targetMonthlyIncome: number | null;
|
targetMonthlyIncome: number | null;
|
||||||
|
targetNestEgg: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Crossover() {
|
export function Crossover() {
|
||||||
@@ -299,6 +301,9 @@ function CrossoverInner({ widget }: CrossoverInnerProps) {
|
|||||||
// Get target monthly income from spreadsheet data
|
// Get target monthly income from spreadsheet data
|
||||||
const targetMonthlyIncome = data?.targetMonthlyIncome ?? null;
|
const targetMonthlyIncome = data?.targetMonthlyIncome ?? null;
|
||||||
|
|
||||||
|
// Get target nest egg from spreadsheet data
|
||||||
|
const targetNestEgg = data?.targetNestEgg ?? null;
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isNarrowWidth } = useResponsive();
|
const { isNarrowWidth } = useResponsive();
|
||||||
|
|
||||||
@@ -791,6 +796,20 @@ function CrossoverInner({ widget }: CrossoverInnerProps) {
|
|||||||
</PrivacyFilter>
|
</PrivacyFilter>
|
||||||
</span>
|
</span>
|
||||||
</View>
|
</View>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Trans>Target Nest Egg</Trans>:{' '}
|
||||||
|
<PrivacyFilter>
|
||||||
|
{targetNestEgg != null && !isNaN(targetNestEgg)
|
||||||
|
? format(targetNestEgg, 'financial')
|
||||||
|
: t('N/A')}
|
||||||
|
</PrivacyFilter>
|
||||||
|
</span>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type CrossoverData = {
|
|||||||
x: string;
|
x: string;
|
||||||
investmentIncome: number;
|
investmentIncome: number;
|
||||||
expenses: number;
|
expenses: number;
|
||||||
|
nestEgg: number;
|
||||||
isProjection?: boolean;
|
isProjection?: boolean;
|
||||||
}>;
|
}>;
|
||||||
start: string;
|
start: string;
|
||||||
@@ -44,6 +45,7 @@ type CrossoverData = {
|
|||||||
historicalReturn: number | null;
|
historicalReturn: number | null;
|
||||||
yearsToRetire: number | null;
|
yearsToRetire: number | null;
|
||||||
targetMonthlyIncome: number | null;
|
targetMonthlyIncome: number | null;
|
||||||
|
targetNestEgg: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CrossoverCardProps = {
|
type CrossoverCardProps = {
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export function createCrossoverSpreadsheet({
|
|||||||
historicalReturn: null,
|
historicalReturn: null,
|
||||||
yearsToRetire: null,
|
yearsToRetire: null,
|
||||||
targetMonthlyIncome: null,
|
targetMonthlyIncome: null,
|
||||||
|
targetNestEgg: null,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -231,6 +232,7 @@ function recalculate(
|
|||||||
x: string;
|
x: string;
|
||||||
investmentIncome: number;
|
investmentIncome: number;
|
||||||
expenses: number;
|
expenses: number;
|
||||||
|
nestEgg: number;
|
||||||
isProjection?: boolean;
|
isProjection?: boolean;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
@@ -245,6 +247,7 @@ function recalculate(
|
|||||||
x: d.format(d.parseISO(month + '-01'), 'MMM yyyy'),
|
x: d.format(d.parseISO(month + '-01'), 'MMM yyyy'),
|
||||||
investmentIncome: Math.round(monthlyIncome),
|
investmentIncome: Math.round(monthlyIncome),
|
||||||
expenses: spend,
|
expenses: spend,
|
||||||
|
nestEgg: balance,
|
||||||
});
|
});
|
||||||
lastBalance = balance;
|
lastBalance = balance;
|
||||||
lastExpense = spend;
|
lastExpense = spend;
|
||||||
@@ -355,6 +358,7 @@ function recalculate(
|
|||||||
x: d.format(monthCursor, 'MMM yyyy'),
|
x: d.format(monthCursor, 'MMM yyyy'),
|
||||||
investmentIncome: Math.round(projectedIncome),
|
investmentIncome: Math.round(projectedIncome),
|
||||||
expenses: Math.round(projectedExpenses),
|
expenses: Math.round(projectedExpenses),
|
||||||
|
nestEgg: Math.round(projectedBalance),
|
||||||
isProjection: true,
|
isProjection: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -370,6 +374,7 @@ function recalculate(
|
|||||||
// Calculate years to retire based on crossover point
|
// Calculate years to retire based on crossover point
|
||||||
let yearsToRetire: number | null = null;
|
let yearsToRetire: number | null = null;
|
||||||
let targetMonthlyIncome: number | null = null;
|
let targetMonthlyIncome: number | null = null;
|
||||||
|
let targetNestEgg: number | null = null;
|
||||||
|
|
||||||
if (crossoverIndex != null && crossoverIndex < data.length) {
|
if (crossoverIndex != null && crossoverIndex < data.length) {
|
||||||
const crossoverData = data[crossoverIndex];
|
const crossoverData = data[crossoverIndex];
|
||||||
@@ -379,6 +384,8 @@ function recalculate(
|
|||||||
const monthsDiff = d.differenceInMonths(crossoverDate, currentDate);
|
const monthsDiff = d.differenceInMonths(crossoverDate, currentDate);
|
||||||
yearsToRetire = monthsDiff > 0 ? monthsDiff / 12 : 0;
|
yearsToRetire = monthsDiff > 0 ? monthsDiff / 12 : 0;
|
||||||
targetMonthlyIncome = crossoverData.expenses;
|
targetMonthlyIncome = crossoverData.expenses;
|
||||||
|
// Calculate target nest egg: target monthly income / monthly safe withdrawal rate
|
||||||
|
targetNestEgg = Math.round(targetMonthlyIncome / monthlySWR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,5 +412,7 @@ function recalculate(
|
|||||||
yearsToRetire,
|
yearsToRetire,
|
||||||
// Target monthly income at crossover point
|
// Target monthly income at crossover point
|
||||||
targetMonthlyIncome,
|
targetMonthlyIncome,
|
||||||
|
// Target nest egg at crossover point
|
||||||
|
targetNestEgg,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
6
upcoming-release-notes/6384.md
Normal file
6
upcoming-release-notes/6384.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
category: Enhancements
|
||||||
|
authors: [sjones512]
|
||||||
|
---
|
||||||
|
|
||||||
|
Add a nest egg field to show projected net worth on the crossover point report
|
||||||
Reference in New Issue
Block a user