Skip to content

Commit ffbe9fa

Browse files
Copilotfrostaura
andauthored
refactor: extract budget display helpers
Agent-Logs-Url: https://github.com/frostaura/fa.lifeos/sessions/ccc9865a-8d2f-42d6-a0f2-fbf85eae716b Co-authored-by: frostaura <8956241+frostaura@users.noreply.github.com>
1 parent 44d56e7 commit ffbe9fa

3 files changed

Lines changed: 267 additions & 164 deletions

File tree

src/frontend/src/pages/settings/IncomeExpenseSettings.tsx

Lines changed: 39 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,20 @@ import {
2323
useUpdateExpenseDefinitionMutation,
2424
useUpdateIncomeSourceMutation,
2525
} from "@/services/endpoints";
26-
import type { CreateExpenseDefinitionRequest, CreateIncomeSourceRequest, ExpenseDefinition, IncomeSource, InvestmentContribution } from "@/types";
26+
import type { CreateExpenseDefinitionRequest, CreateIncomeSourceRequest, ExpenseDefinition, IncomeSource } from "@/types";
2727
import { BudgetCharts, ExpenseCategoryChart } from "./income-expense/BudgetCharts";
28-
import { calculateProjectedValues, getExpenseCategoryPieData, getMonthlyAmount, getTargetMonthLabel, isInCurrentMonth, isScheduledDatePast, sortExpenseDefinitions, sortIncomeSources, sortOptions, type SortOption } from "./income-expense/calculations";
28+
import { isScheduledDatePast, sortExpenseDefinitions, sortIncomeSources, sortOptions, type SortOption } from "./income-expense/calculations";
29+
import { buildBudgetDisplayState } from "./income-expense/budgetDisplay";
2930
import { ExpenseForm } from "./income-expense/ExpenseForm";
3031
import { ExpenseList } from "./income-expense/ExpenseList";
3132
import { IncomeForm } from "./income-expense/IncomeForm";
3233
import { IncomeList } from "./income-expense/IncomeList";
3334
import { createInitialExpenseForm, createInitialIncomeForm, mapExpenseToForm, mapIncomeToForm } from "./income-expense/formState";
3435
import { useAutoScrollIntoView, useScrollToCard } from "./income-expense/useScrollHelpers";
35-
import { convertAmountForDisplay, formatCurrencyAmount, resolveTrustedDisplayCurrency } from "./settingsUtils";
36+
import { formatCurrencyAmount, resolveTrustedDisplayCurrency } from "./settingsUtils";
3637
import { SettingsLoadingState, SettingsPageCard, SettingsPageHeader } from "./settingsShared";
3738
import type { RootState } from "@store/index";
3839

39-
function sumConvertedMonthlyAmounts<T extends { currency: string }>(
40-
items: T[],
41-
getAmount: (item: T) => number,
42-
displayCurrency: string,
43-
fxRates: Array<{ pair: string; rate: number; change: number; timestamp: string }>,
44-
) {
45-
return items.reduce(
46-
(sum, item) => sum + convertAmountForDisplay(getAmount(item), item.currency, displayCurrency, fxRates),
47-
0,
48-
);
49-
}
50-
5140
export function IncomeExpenseSettings() {
5241
const { data: incomeData, isLoading: incomeLoading } = useGetIncomeSourcesWithSummaryQuery();
5342
const { data: expenseDefinitions, isLoading: expenseLoading } = useGetExpenseDefinitionsQuery();
@@ -199,10 +188,27 @@ export function IncomeExpenseSettings() {
199188
const activeExpenses = useMemo(() => sortedExpenseDefinitions.filter((expense) => expense.frequency.toLowerCase() !== "once" || !isScheduledDatePast(expense.startDate)), [sortedExpenseDefinitions]);
200189
const completedOneTimeExpenses = useMemo(() => sortedExpenseDefinitions.filter((expense) => expense.frequency.toLowerCase() === "once" && isScheduledDatePast(expense.startDate)), [sortedExpenseDefinitions]);
201190

202-
const projectedValues = useMemo(
191+
const {
192+
targetMonthLabel,
193+
isProjectingFuture,
194+
totalMonthlyGross,
195+
totalMonthlyTax,
196+
totalMonthlyUif,
197+
totalMonthlyNet,
198+
totalMonthlyExpenses,
199+
totalMonthlyInterest,
200+
totalMonthlyFees,
201+
netCashFlow,
202+
budgetBalance,
203+
hasSurplus,
204+
hasDeficit,
205+
isBalanced,
206+
pieChartData,
207+
expenseCategoryPieData,
208+
} = useMemo(
203209
() =>
204-
calculateProjectedValues({
205-
monthOffset: timelineMonth,
210+
buildBudgetDisplayState({
211+
timelineMonth,
206212
incomeSources,
207213
incomeSummary,
208214
expenseDefinitions,
@@ -212,141 +218,10 @@ export function IncomeExpenseSettings() {
212218
displayCurrency: effectiveDisplayCurrency,
213219
sourceCurrency,
214220
fxRates,
221+
pieSeriesColors: SETTINGS_BUDGET_PIE_SERIES_COLORS,
222+
expenseCategoryColors: SETTINGS_EXPENSE_CATEGORY_COLORS,
215223
}),
216-
[timelineMonth, incomeSources, incomeSummary, expenseDefinitions, investmentData, accountsMeta, effectiveDisplayCurrency, sourceCurrency, fxRates],
217-
);
218-
const targetMonthLabel = useMemo(() => getTargetMonthLabel(projectedValues.targetDate), [projectedValues.targetDate]);
219-
const isProjectingFuture = timelineMonth > 0;
220-
221-
const currentMonthlyGross = useMemo(
222-
() => sumConvertedMonthlyAmounts(
223-
(incomeSources || []).filter((income) => income.isActive),
224-
(income) => getMonthlyAmount(income.baseAmount, income.paymentFrequency, income.nextPaymentDate),
225-
effectiveDisplayCurrency,
226-
fxRates,
227-
),
228-
[incomeSources, effectiveDisplayCurrency, fxRates],
229-
);
230-
const currentMonthlyTax = convertAmountForDisplay(
231-
incomeSummary?.totalMonthlyTax || 0,
232-
sourceCurrency,
233-
effectiveDisplayCurrency,
234-
fxRates,
235-
);
236-
const currentMonthlyUif = convertAmountForDisplay(
237-
incomeSummary?.totalMonthlyUif || 0,
238-
sourceCurrency,
239-
effectiveDisplayCurrency,
240-
fxRates,
241-
);
242-
const currentMonthlyNet = currentMonthlyGross - currentMonthlyTax - currentMonthlyUif;
243-
const currentMonthlyExpenses = useMemo(
244-
() => sumConvertedMonthlyAmounts(
245-
(expenseDefinitions || []).filter((expense) => expense.isActive),
246-
(expense) => getMonthlyAmount(expense.amountValue || 0, expense.frequency, expense.startDate),
247-
effectiveDisplayCurrency,
248-
fxRates,
249-
),
250-
[expenseDefinitions, effectiveDisplayCurrency, fxRates],
251-
);
252-
const currentMonthlyInvestments = useMemo(
253-
() => sumConvertedMonthlyAmounts(
254-
(investmentData?.sources || []).filter((investment) => investment.isActive),
255-
(investment: InvestmentContribution) =>
256-
investment.frequency.toLowerCase() === "once"
257-
? (isInCurrentMonth(investment.startDate) ? investment.amount : 0)
258-
: getMonthlyAmount(investment.amount, investment.frequency, investment.startDate),
259-
effectiveDisplayCurrency,
260-
fxRates,
261-
),
262-
[investmentData?.sources, effectiveDisplayCurrency, fxRates],
263-
);
264-
const currentMonthlyInterest = convertAmountForDisplay(
265-
accountsMeta?.totalMonthlyInterest || 0,
266-
sourceCurrency,
267-
effectiveDisplayCurrency,
268-
fxRates,
269-
);
270-
const currentMonthlyFees = convertAmountForDisplay(
271-
accountsMeta?.totalMonthlyFees || 0,
272-
sourceCurrency,
273-
effectiveDisplayCurrency,
274-
fxRates,
275-
);
276-
277-
const totalMonthlyGross = isProjectingFuture ? projectedValues.grossIncome : currentMonthlyGross;
278-
const totalMonthlyTax = isProjectingFuture ? projectedValues.tax : currentMonthlyTax;
279-
const totalMonthlyUif = isProjectingFuture ? projectedValues.uif : currentMonthlyUif;
280-
const totalMonthlyNet = isProjectingFuture ? projectedValues.netIncome : currentMonthlyNet;
281-
const totalMonthlyExpenses = isProjectingFuture ? projectedValues.expenses : currentMonthlyExpenses;
282-
const totalMonthlyInvestments = isProjectingFuture ? projectedValues.investments : currentMonthlyInvestments;
283-
const totalMonthlyInterest = isProjectingFuture ? projectedValues.interest : currentMonthlyInterest;
284-
const totalMonthlyFees = isProjectingFuture ? projectedValues.fees : currentMonthlyFees;
285-
const netCashFlow = totalMonthlyNet - totalMonthlyExpenses - totalMonthlyInterest - totalMonthlyFees;
286-
287-
const totalAllocated = totalMonthlyExpenses + totalMonthlyInterest + totalMonthlyFees + totalMonthlyInvestments + totalMonthlyTax + totalMonthlyUif;
288-
const budgetBalance = totalMonthlyGross - totalAllocated;
289-
const hasSurplus = budgetBalance > 0;
290-
const hasDeficit = budgetBalance < 0;
291-
const isBalanced = budgetBalance === 0;
292-
293-
const pieChartData = useMemo(() => {
294-
const data: Array<{ name: string; value: number; color: string }> = [
295-
{
296-
name: "Expenses",
297-
value: totalMonthlyExpenses,
298-
color: SETTINGS_BUDGET_PIE_SERIES_COLORS.expenses,
299-
},
300-
{
301-
name: "Interest",
302-
value: totalMonthlyInterest,
303-
color: SETTINGS_BUDGET_PIE_SERIES_COLORS.interest,
304-
},
305-
{
306-
name: "Account Fees",
307-
value: totalMonthlyFees,
308-
color: SETTINGS_BUDGET_PIE_SERIES_COLORS.accountFees,
309-
},
310-
{
311-
name: "Investments",
312-
value: totalMonthlyInvestments,
313-
color: SETTINGS_BUDGET_PIE_SERIES_COLORS.investments,
314-
},
315-
{
316-
name: "Tax (PAYE)",
317-
value: totalMonthlyTax,
318-
color: SETTINGS_BUDGET_PIE_SERIES_COLORS.tax,
319-
},
320-
{
321-
name: "UIF",
322-
value: totalMonthlyUif,
323-
color: SETTINGS_BUDGET_PIE_SERIES_COLORS.uif,
324-
},
325-
].filter((item) => item.value > 0);
326-
327-
if (hasSurplus) {
328-
data.push({
329-
name: "Unallocated",
330-
value: budgetBalance,
331-
color: SETTINGS_BUDGET_PIE_SERIES_COLORS.unallocated,
332-
});
333-
}
334-
335-
return data;
336-
}, [totalMonthlyExpenses, totalMonthlyInterest, totalMonthlyFees, totalMonthlyInvestments, totalMonthlyTax, totalMonthlyUif, hasSurplus, budgetBalance]);
337-
338-
const expenseCategoryPieData = useMemo(
339-
() =>
340-
getExpenseCategoryPieData({
341-
expenseDefinitions: expenseDefinitions || [],
342-
isProjectingFuture,
343-
timelineMonth,
344-
inflationRate: defaultInflationRate,
345-
categoryColors: SETTINGS_EXPENSE_CATEGORY_COLORS,
346-
displayCurrency: effectiveDisplayCurrency,
347-
fxRates,
348-
}),
349-
[expenseDefinitions, isProjectingFuture, timelineMonth, effectiveDisplayCurrency, fxRates],
224+
[timelineMonth, incomeSources, incomeSummary, expenseDefinitions, investmentData?.sources, accountsMeta, effectiveDisplayCurrency, sourceCurrency, fxRates],
350225
);
351226

352227
if (incomeLoading || expenseLoading) {
@@ -390,22 +265,22 @@ export function IncomeExpenseSettings() {
390265

391266
<div className="mb-6 border-b border-glass-border/60 pb-4">
392267
<div className="ds-segmented-control w-fit" data-testid="finances-income-expense-tabs">
393-
<button
394-
data-testid="finances-income-tab"
395-
onClick={() => setActiveTab("income")}
268+
<button
269+
data-testid="finances-income-tab"
270+
onClick={() => setActiveTab("income")}
396271
data-active={activeTab === "income" ? true : undefined}
397272
className="ds-segmented-item"
398-
>
399-
Income Sources ({incomeSources?.length || 0})
400-
</button>
401-
<button
402-
data-testid="finances-expense-tab"
403-
onClick={() => setActiveTab("expenses")}
273+
>
274+
Income Sources ({incomeSources?.length || 0})
275+
</button>
276+
<button
277+
data-testid="finances-expense-tab"
278+
onClick={() => setActiveTab("expenses")}
404279
data-active={activeTab === "expenses" ? true : undefined}
405280
className="ds-segmented-item"
406-
>
407-
Expenses ({expenseDefinitions?.length || 0})
408-
</button>
281+
>
282+
Expenses ({expenseDefinitions?.length || 0})
283+
</button>
409284
</div>
410285
</div>
411286

0 commit comments

Comments
 (0)