@@ -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" ;
2727import { 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" ;
2930import { ExpenseForm } from "./income-expense/ExpenseForm" ;
3031import { ExpenseList } from "./income-expense/ExpenseList" ;
3132import { IncomeForm } from "./income-expense/IncomeForm" ;
3233import { IncomeList } from "./income-expense/IncomeList" ;
3334import { createInitialExpenseForm , createInitialIncomeForm , mapExpenseToForm , mapIncomeToForm } from "./income-expense/formState" ;
3435import { useAutoScrollIntoView , useScrollToCard } from "./income-expense/useScrollHelpers" ;
35- import { convertAmountForDisplay , formatCurrencyAmount , resolveTrustedDisplayCurrency } from "./settingsUtils" ;
36+ import { formatCurrencyAmount , resolveTrustedDisplayCurrency } from "./settingsUtils" ;
3637import { SettingsLoadingState , SettingsPageCard , SettingsPageHeader } from "./settingsShared" ;
3738import 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-
5140export 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